Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

@@ -0,0 +1,24 @@
{
"name": "@hivegrid/activity",
"version": "0.0.0",
"private": true,
"type": "module",
"sideEffects": false,
"exports": {
".": "./src/index.ts",
"./types": "./src/types.ts"
},
"scripts": {
"build": "pnpm run typecheck",
"test": "vitest run src",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@toolctrl/db": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.0.0",
"typescript": "^5.3.2",
"vitest": "^3.2.4"
}
}
@@ -0,0 +1,6 @@
// @hivegrid/activity — central Activity-Stream cross-cutting service (M045 / M00).
// Server entry: `import { logActivity } from "@hivegrid/activity"`.
// Browser-safe types only: `import type { ActivityType } from "@hivegrid/activity/types"`.
export * from "./types.js";
export { logActivity } from "./logActivity.server.js";
export type { ActivityWriteClient, LogActivityOptions } from "./logActivity.server.js";
@@ -0,0 +1,54 @@
import { getDbClient } from "@toolctrl/db/client.server";
import type { Prisma } from "@toolctrl/db/prisma-browser";
import type { LogActivityInput } from "./types.js";
/**
* Minimal write surface `logActivity` needs. Satisfied by the full Prisma client
* AND by interactive-transaction clients, so callers can append an activity inside
* the same transaction as the action that triggered it.
*/
export type ActivityWriteClient = {
activity: {
create: (args: { data: Prisma.ActivityUncheckedCreateInput }) => Promise<{ id: string }>;
};
};
export type LogActivityOptions = {
/** Transaction/client to write through. Defaults to the shared db singleton. */
client?: ActivityWriteClient;
};
/**
* Append a user-facing activity to the central `Activity` stream (M045 / DoD #14).
*
* This is NOT the audit log — the tamper-evident `AuditEvent` (M001) is written
* separately (auto via Prisma middleware). `logActivity` records the human-readable,
* per-entity timeline shown to tenant users.
*/
export async function logActivity(input: LogActivityInput, options: LogActivityOptions = {}): Promise<{ id: string }> {
if (!input.tenantId) {
throw new Error("logActivity: tenantId is required");
}
if (!input.subjectId) {
throw new Error("logActivity: subjectId is required");
}
if (!input.summaryText || input.summaryText.trim().length === 0) {
throw new Error("logActivity: summaryText (Klartext-Zusammenfassung) is required");
}
const client = options.client ?? (getDbClient() as unknown as ActivityWriteClient);
return client.activity.create({
data: {
tenantId: input.tenantId,
actorUserId: input.actorUserId ?? null,
actorKind: input.actorKind,
activityType: input.activityType,
subjectType: input.subjectType,
subjectId: input.subjectId,
summaryText: input.summaryText,
relatedEntityRefs: input.relatedEntityRefs ? (input.relatedEntityRefs as unknown as Prisma.InputJsonValue) : undefined,
metadata: input.metadata ? (input.metadata as Prisma.InputJsonValue) : undefined,
},
});
}
@@ -0,0 +1,71 @@
import { describe, expect, it, vi } from "vitest";
import type { Prisma } from "@toolctrl/db/prisma-browser";
import { logActivity, type ActivityWriteClient } from "./logActivity.server.js";
import type { LogActivityInput } from "./types.js";
function makeMockClient() {
const create = vi.fn<(args: { data: Prisma.ActivityUncheckedCreateInput }) => Promise<{ id: string }>>(() =>
Promise.resolve({ id: "act_1" })
);
const client: ActivityWriteClient = { activity: { create } };
return { client, create };
}
const base: LogActivityInput = {
tenantId: "t1",
actorUserId: "u1",
actorKind: "user",
activityType: "note_added",
subjectType: "contact",
subjectId: "c1",
summaryText: "Notiz hinzugefügt",
};
describe("logActivity", () => {
it("writes a tenant-scoped activity row with mapped fields through the given client", async () => {
const { client, create } = makeMockClient();
const result = await logActivity(base, { client });
expect(result).toEqual({ id: "act_1" });
expect(create).toHaveBeenCalledTimes(1);
expect(create.mock.calls[0]?.[0].data).toMatchObject({
tenantId: "t1",
actorUserId: "u1",
actorKind: "user",
activityType: "note_added",
subjectType: "contact",
subjectId: "c1",
summaryText: "Notiz hinzugefügt",
});
});
it("passes relatedEntityRefs and metadata through as JSON", async () => {
const { client, create } = makeMockClient();
await logActivity(
{ ...base, relatedEntityRefs: [{ type: "company", id: "co1" }], metadata: { from: "neu", to: "kontaktiert" } },
{ client }
);
const data = create.mock.calls[0]?.[0].data;
expect(data?.relatedEntityRefs).toEqual([{ type: "company", id: "co1" }]);
expect(data?.metadata).toEqual({ from: "neu", to: "kontaktiert" });
});
it("defaults a missing actorUserId to null (system/automation actors)", async () => {
const { client, create } = makeMockClient();
await logActivity({ ...base, actorUserId: undefined, actorKind: "system" }, { client });
expect(create.mock.calls[0]?.[0].data.actorUserId).toBeNull();
});
it("requires tenantId, subjectId and a non-empty summaryText", async () => {
const { client } = makeMockClient();
await expect(logActivity({ ...base, tenantId: "" }, { client })).rejects.toThrow("tenantId");
await expect(logActivity({ ...base, subjectId: "" }, { client })).rejects.toThrow("subjectId");
await expect(logActivity({ ...base, summaryText: " " }, { client })).rejects.toThrow("summaryText");
});
});
@@ -0,0 +1,57 @@
// Public, browser-safe types for the HiveGrid Activity-Stream (M045 / M01 Block E).
// No runtime/server imports here so view-models and UI can import the contract.
/** Actor that produced an activity. Mirrors the `ActivityActorKind` Prisma enum. */
export type ActivityActorKind = "user" | "system" | "automation" | "ki_agent";
/**
* Canonical activity types (M01 Block E). Closed union = spec "Enum" + compile-time
* safety; the DB column is a plain string, so adding a new type is a one-line union
* extension here (no schema migration). Phase-2 types (whatsapp_*) are included so
* callers can reference them early.
*/
export type ActivityType =
| "call_inbound"
| "call_outbound"
| "email_sent"
| "email_received"
| "whatsapp_sent"
| "whatsapp_received"
| "chat_message"
| "note_added"
| "tag_added"
| "tag_removed"
| "assignee_changed"
| "deal_stage_changed"
| "project_stage_changed"
| "file_uploaded"
| "form_submitted"
| "task_created"
| "task_completed"
| "appointment_scheduled"
| "appointment_held"
| "lead_promoted"
| "deal_won"
| "deal_lost"
| "automation_triggered"
| "ki_action"
| "audit_event";
/** Main object a stream hangs off (DoD #14: Kontakt/Firma/Lead/Verkaufsfall/Auftrag …). */
export type ActivitySubjectType = "contact" | "company" | "lead" | "deal" | "project" | "task";
/** Additional entities referenced by an activity (denormalized for the timeline). */
export type ActivityRelatedRef = { type: ActivitySubjectType; id: string };
/** Input for {@link logActivity}. `summaryText` is the mandatory DE Klartext line. */
export type LogActivityInput = {
tenantId: string;
actorUserId?: string | null;
actorKind: ActivityActorKind;
activityType: ActivityType;
subjectType: ActivitySubjectType;
subjectId: string;
summaryText: string;
relatedEntityRefs?: ActivityRelatedRef[];
metadata?: Record<string, unknown>;
};
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"allowJs": false,
"allowImportingTsExtensions": false,
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmit": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}