Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -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>;
|
||||
};
|
||||
Reference in new issue
Block a user