@orthacms/activity-server 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/lib/activity/activity-filter.d.ts.map +1 -1
  2. package/dist/lib/activity/activity-filter.js +2 -0
  3. package/dist/lib/activity/activity.constants.d.ts +8 -3
  4. package/dist/lib/activity/activity.constants.d.ts.map +1 -1
  5. package/dist/lib/activity/activity.constants.js +9 -3
  6. package/dist/lib/activity/controllers/dead-letters.controller.d.ts +36 -0
  7. package/dist/lib/activity/controllers/dead-letters.controller.d.ts.map +1 -0
  8. package/dist/lib/activity/controllers/dead-letters.controller.js +61 -0
  9. package/dist/lib/activity/controllers/entry-activity.controller.d.ts +32 -0
  10. package/dist/lib/activity/controllers/entry-activity.controller.d.ts.map +1 -0
  11. package/dist/lib/activity/controllers/entry-activity.controller.js +69 -0
  12. package/dist/lib/activity/dto/dead-letters-query.dto.d.ts +14 -0
  13. package/dist/lib/activity/dto/dead-letters-query.dto.d.ts.map +1 -0
  14. package/dist/lib/activity/dto/dead-letters-query.dto.js +46 -0
  15. package/dist/lib/activity/dto/entry-activity-query.dto.d.ts +14 -0
  16. package/dist/lib/activity/dto/entry-activity-query.dto.d.ts.map +1 -0
  17. package/dist/lib/activity/dto/entry-activity-query.dto.js +50 -0
  18. package/dist/lib/activity/dto/list-activity-query.dto.d.ts +16 -0
  19. package/dist/lib/activity/dto/list-activity-query.dto.d.ts.map +1 -1
  20. package/dist/lib/activity/dto/list-activity-query.dto.js +38 -0
  21. package/dist/lib/activity/infrastructure/audit-event-mapping.d.ts +46 -0
  22. package/dist/lib/activity/infrastructure/audit-event-mapping.d.ts.map +1 -1
  23. package/dist/lib/activity/infrastructure/audit-event-mapping.js +515 -34
  24. package/dist/lib/activity/services/activity.service.d.ts.map +1 -1
  25. package/dist/lib/activity/services/activity.service.js +6 -0
  26. package/dist/lib/activity/types/activity-view.d.ts +18 -1
  27. package/dist/lib/activity/types/activity-view.d.ts.map +1 -1
  28. package/dist/lib/activity.module.d.ts.map +1 -1
  29. package/dist/lib/activity.module.js +12 -1
  30. package/dist/lib/docs/activity-schemas.d.ts +19 -0
  31. package/dist/lib/docs/activity-schemas.d.ts.map +1 -0
  32. package/dist/lib/docs/activity-schemas.js +167 -0
  33. package/dist/lib/docs/describe-activity-api.d.ts +17 -0
  34. package/dist/lib/docs/describe-activity-api.d.ts.map +1 -0
  35. package/dist/lib/docs/describe-activity-api.js +85 -0
  36. package/dist/lib/schema/activity-events.d.ts +56 -2
  37. package/dist/lib/schema/activity-events.d.ts.map +1 -1
  38. package/dist/lib/schema/activity-events.js +26 -3
  39. package/dist/lib/utils/activity-plugin.d.ts.map +1 -1
  40. package/dist/lib/utils/activity-plugin.js +5 -1
  41. package/migrations/0001_actor_type_and_workspace.sql +8 -0
  42. package/migrations/meta/0001_snapshot.json +192 -0
  43. package/migrations/meta/_journal.json +7 -0
  44. package/package.json +7 -8
@@ -1 +1 @@
1
- {"version":3,"file":"activity.service.d.ts","sourceRoot":"","sources":["../../../../src/lib/activity/services/activity.service.ts"],"names":[],"mappings":"AAYA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,KAAK,EACR,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACtB,MAAM,2BAA2B,CAAC;AAInC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,KAAK,EAER,gBAAgB,EACnB,MAAM,wBAAwB,CAAC;AAQhC;;;;;;GAMG;AACH,qBACa,eAAgB,YAAW,gBAAgB;IACtB,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ;IAE3D;;;;;;;;;;OAUG;IACG,MAAM,CACR,KAAK,EAAE,mBAAmB,EAC1B,QAAQ,GAAE,gBAA0B,GACrC,OAAO,CAAC,IAAI,CAAC;IAYhB;;;;;OAKG;IACG,IAAI,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA8ClE;;;;;;OAMG;YACW,SAAS;IAWvB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAsBrB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;CAQ9B"}
1
+ {"version":3,"file":"activity.service.d.ts","sourceRoot":"","sources":["../../../../src/lib/activity/services/activity.service.ts"],"names":[],"mappings":"AAYA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,KAAK,EACR,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACtB,MAAM,2BAA2B,CAAC;AAInC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,KAAK,EAER,gBAAgB,EACnB,MAAM,wBAAwB,CAAC;AAQhC;;;;;;GAMG;AACH,qBACa,eAAgB,YAAW,gBAAgB;IACtB,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ;IAE3D;;;;;;;;;;OAUG;IACG,MAAM,CACR,KAAK,EAAE,mBAAmB,EAC1B,QAAQ,GAAE,gBAA0B,GACrC,OAAO,CAAC,IAAI,CAAC;IAYhB;;;;;OAKG;IACG,IAAI,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAgDlE;;;;;;OAMG;YACW,SAAS;IAWvB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IA4BrB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;CAQ9B"}
@@ -75,7 +75,9 @@ let ActivityService = class ActivityService {
75
75
  subjectType: schema_1.activityEvents.subjectType,
76
76
  subjectId: schema_1.activityEvents.subjectId,
77
77
  actorId: schema_1.activityEvents.actorId,
78
+ actorType: schema_1.activityEvents.actorType,
78
79
  actorEmail: schema_1.activityEvents.actorEmail,
80
+ workspaceId: schema_1.activityEvents.workspaceId,
79
81
  meta: schema_1.activityEvents.meta,
80
82
  at: schema_1.activityEvents.at
81
83
  })
@@ -119,6 +121,10 @@ let ActivityService = class ActivityService {
119
121
  ? (0, drizzle_orm_1.eq)(schema_1.activityEvents.subjectId, query.subjectId)
120
122
  : undefined, query.actorId
121
123
  ? (0, drizzle_orm_1.eq)(schema_1.activityEvents.actorId, query.actorId)
124
+ : undefined, query.actorType
125
+ ? (0, drizzle_orm_1.eq)(schema_1.activityEvents.actorType, query.actorType)
126
+ : undefined, query.workspaceId
127
+ ? (0, drizzle_orm_1.eq)(schema_1.activityEvents.workspaceId, query.workspaceId)
122
128
  : undefined, query.kind && query.kind.length > 0
123
129
  ? (0, drizzle_orm_1.inArray)(schema_1.activityEvents.kind, query.kind)
124
130
  : undefined, this.actorEmailPredicate(query.actorEmail), query.from
@@ -16,8 +16,25 @@ export interface ActivityEventView {
16
16
  subjectId: string;
17
17
  /** Who performed it, or `null` for a system-initiated event. */
18
18
  actorId: string | null;
19
- /** Frozen email snapshot of the actor, or `null`. */
19
+ /**
20
+ * What {@link actorId} names — `'user'` for a person, `'api_token'` for an
21
+ * external credential, `null` when there is no actor. A client rendering
22
+ * the actor needs this: the two ids come from different tables and lead to
23
+ * different pages, and an `actorEmail` that is really a token label would
24
+ * otherwise be indistinguishable from a person's address.
25
+ */
26
+ actorType: string | null;
27
+ /**
28
+ * Frozen email snapshot of the actor, or `null`. For an `api_token` actor
29
+ * this carries the token's **label** instead — a token has no email, and a
30
+ * row showing a bare uuid names nothing a reader recognises.
31
+ */
20
32
  actorEmail: string | null;
33
+ /**
34
+ * The workspace the action happened in, or `null` when it belongs to none
35
+ * (an invite, a role change, the creation of a workspace itself).
36
+ */
37
+ workspaceId: string | null;
21
38
  /** Open per-kind payload, or `null`. */
22
39
  meta: Record<string, unknown> | null;
23
40
  /** Logical event time. */
@@ -1 +1 @@
1
- {"version":3,"file":"activity-view.d.ts","sourceRoot":"","sources":["../../../../src/lib/activity/types/activity-view.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAC9B,yBAAyB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,qDAAqD;IACrD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrC,0BAA0B;IAC1B,EAAE,EAAE,IAAI,CAAC;CACZ;AAED,oEAAoE;AACpE,MAAM,WAAW,gBAAgB;IAC7B,+BAA+B;IAC/B,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"activity-view.d.ts","sourceRoot":"","sources":["../../../../src/lib/activity/types/activity-view.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAC9B,yBAAyB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;OAGG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrC,0BAA0B;IAC1B,EAAE,EAAE,IAAI,CAAC;CACZ;AAED,oEAAoE;AACpE,MAAM,WAAW,gBAAgB;IAC7B,+BAA+B;IAC/B,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;CACpB"}
@@ -1 +1 @@
1
- {"version":3,"file":"activity.module.d.ts","sourceRoot":"","sources":["../../src/lib/activity.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAU,MAAM,gBAAgB,CAAC;AAOvD;;;;;;;;;;;;;GAaG;AACH,qBACa,cAAc;IACvB,qFAAqF;IACrF,MAAM,CAAC,OAAO,IAAI,aAAa;CAiBlC"}
1
+ {"version":3,"file":"activity.module.d.ts","sourceRoot":"","sources":["../../src/lib/activity.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAU,MAAM,gBAAgB,CAAC;AASvD;;;;;;;;;;;;;GAaG;AACH,qBACa,cAAc;IACvB,qFAAqF;IACrF,MAAM,CAAC,OAAO,IAAI,aAAa;CA0BlC"}
@@ -6,6 +6,8 @@ const tslib_1 = require("tslib");
6
6
  const common_1 = require("@nestjs/common");
7
7
  const identity_server_1 = require("@orthacms/identity-server");
8
8
  const list_activity_controller_1 = require("./activity/controllers/list-activity.controller");
9
+ const dead_letters_controller_1 = require("./activity/controllers/dead-letters.controller");
10
+ const entry_activity_controller_1 = require("./activity/controllers/entry-activity.controller");
9
11
  const activity_service_1 = require("./activity/services/activity.service");
10
12
  const audit_event_subscriber_1 = require("./activity/infrastructure/audit-event.subscriber");
11
13
  const activity_tool_provider_1 = require("./copilot/activity-tool.provider");
@@ -29,7 +31,16 @@ let ActivityModule = ActivityModule_1 = class ActivityModule {
29
31
  return {
30
32
  module: ActivityModule_1,
31
33
  global: true,
32
- controllers: [list_activity_controller_1.ListActivityController],
34
+ controllers: [
35
+ // The scoped route is declared **before** the global one:
36
+ // `activity/entries/:id` and `activity` are different paths, but
37
+ // keeping the literal-prefixed controller first is the same
38
+ // ordering habit the content routes follow, and it stays true
39
+ // as the pattern set grows.
40
+ entry_activity_controller_1.EntryActivityController,
41
+ list_activity_controller_1.ListActivityController,
42
+ dead_letters_controller_1.DeadLettersController
43
+ ],
33
44
  providers: [
34
45
  activity_service_1.ActivityService,
35
46
  audit_event_subscriber_1.AuditEventSubscriber,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The response schemas of the activity plugin's three read routes, as plain
3
+ * OpenAPI objects.
4
+ *
5
+ * `ActivityEventView`, `ActivityListView` and `DeadLetterListView` are
6
+ * TypeScript `interface`s, so the OpenAPI scanner emits a bare
7
+ * `{ '200': { description: '' } }` for all three — see
8
+ * `packages/bootstrap/server/AGENTS.md` → "The response-schema gap". These are
9
+ * what the `decorate` pass writes in their place.
10
+ *
11
+ * Pure: no document, no Nest, no database.
12
+ */
13
+ /** A JSON Schema fragment, as it appears in the OpenAPI document. */
14
+ export type OpenApiSchema = Record<string, unknown>;
15
+ /** A `$ref` to one of {@link ACTIVITY_SCHEMAS}. */
16
+ export declare function ref(name: string): OpenApiSchema;
17
+ /** The activity plugin's response schemas, keyed by component name. */
18
+ export declare const ACTIVITY_SCHEMAS: Record<string, OpenApiSchema>;
19
+ //# sourceMappingURL=activity-schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-schemas.d.ts","sourceRoot":"","sources":["../../../src/lib/docs/activity-schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,qEAAqE;AACrE,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD,mDAAmD;AACnD,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAE/C;AAiBD,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAmJ1D,CAAC"}
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ /**
3
+ * The response schemas of the activity plugin's three read routes, as plain
4
+ * OpenAPI objects.
5
+ *
6
+ * `ActivityEventView`, `ActivityListView` and `DeadLetterListView` are
7
+ * TypeScript `interface`s, so the OpenAPI scanner emits a bare
8
+ * `{ '200': { description: '' } }` for all three — see
9
+ * `packages/bootstrap/server/AGENTS.md` → "The response-schema gap". These are
10
+ * what the `decorate` pass writes in their place.
11
+ *
12
+ * Pure: no document, no Nest, no database.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ACTIVITY_SCHEMAS = void 0;
16
+ exports.ref = ref;
17
+ /** A `$ref` to one of {@link ACTIVITY_SCHEMAS}. */
18
+ function ref(name) {
19
+ return { $ref: `#/components/schemas/${name}` };
20
+ }
21
+ /**
22
+ * The paging envelope both log routes answer in. Written twice rather than
23
+ * `$ref`-ed into one, because the two routes are two different questions and
24
+ * the envelope is the only thing they share.
25
+ */
26
+ const PAGE_PROPERTIES = {
27
+ total: {
28
+ type: 'integer',
29
+ description: 'Events matching the filters across every page, not just this one.'
30
+ },
31
+ page: { type: 'integer', description: 'The 1-based page number, echoed.' },
32
+ pageSize: { type: 'integer', description: 'The page size, echoed.' }
33
+ };
34
+ /** The activity plugin's response schemas, keyed by component name. */
35
+ exports.ACTIVITY_SCHEMAS = {
36
+ ActivityEvent: {
37
+ type: 'object',
38
+ description: 'One recorded action. Mirrors the `activity_events` row minus `createdAt` — the immutable write time is an internal detail and never reaches the wire.',
39
+ properties: {
40
+ id: { type: 'string', format: 'uuid' },
41
+ kind: {
42
+ type: 'string',
43
+ description: 'The `domain.action` kind, e.g. `user.signed_in` or `content.published`. Deliberately an open string: each emitting plugin owns its own kinds, so this is not an enum the document can close.'
44
+ },
45
+ subjectType: {
46
+ type: 'string',
47
+ description: 'What kind of thing was acted upon, e.g. `user`, `workspace`, `content_entry`.'
48
+ },
49
+ subjectId: {
50
+ type: 'string',
51
+ description: 'The acted-upon entity’s id. **Text, not a uuid** — subjects are not always uuid-keyed.'
52
+ },
53
+ actorId: {
54
+ type: 'string',
55
+ format: 'uuid',
56
+ nullable: true,
57
+ description: 'Who performed it, or `null` for a system-initiated event. Carries no foreign key: the actor may be deleted, and the audit row must outlive them.'
58
+ },
59
+ actorType: {
60
+ type: 'string',
61
+ nullable: true,
62
+ description: 'What `actorId` names — `user` for a person, `api_token` for an external credential, `null` when there is no actor. Not constrained to those two: the column is open text, and a row written before it existed carries `null`.'
63
+ },
64
+ actorEmail: {
65
+ type: 'string',
66
+ nullable: true,
67
+ description: 'Frozen email snapshot of the actor at record time, so the trail stays readable after the user row is gone. For an `api_token` actor this carries the token’s **label** instead — a token has no email.'
68
+ },
69
+ workspaceId: {
70
+ type: 'string',
71
+ format: 'uuid',
72
+ nullable: true,
73
+ description: 'The workspace the action happened in, or `null` when it belongs to none — an invite, a role change, the creation of a workspace itself.'
74
+ },
75
+ meta: {
76
+ type: 'object',
77
+ additionalProperties: true,
78
+ nullable: true,
79
+ description: 'Open per-kind payload, owned by the emitting plugin. No shape is promised here; a consumer branches on `kind` first.'
80
+ },
81
+ at: {
82
+ type: 'string',
83
+ format: 'date-time',
84
+ description: 'Logical event time — what the list sorts on.'
85
+ }
86
+ },
87
+ required: [
88
+ 'id',
89
+ 'kind',
90
+ 'subjectType',
91
+ 'subjectId',
92
+ 'actorId',
93
+ 'actorType',
94
+ 'actorEmail',
95
+ 'workspaceId',
96
+ 'meta',
97
+ 'at'
98
+ ]
99
+ },
100
+ ActivityListView: {
101
+ type: 'object',
102
+ description: 'One page of audit events, newest first by default.',
103
+ properties: {
104
+ items: { type: 'array', items: ref('ActivityEvent') },
105
+ ...PAGE_PROPERTIES
106
+ },
107
+ required: ['items', 'total', 'page', 'pageSize']
108
+ },
109
+ ActivityDeadLetter: {
110
+ type: 'object',
111
+ description: 'One outbox event that exhausted its delivery attempts and is no longer retried. **The payload is deliberately absent** — it is arbitrary domain data, some of it user-authored, and this route answers "what is stuck", not "replay it".',
112
+ properties: {
113
+ id: {
114
+ type: 'string',
115
+ format: 'uuid',
116
+ description: 'The event id — the handle for a manual replay (clearing `attempts`).'
117
+ },
118
+ kind: {
119
+ type: 'string',
120
+ description: 'The event kind that could not be delivered.'
121
+ },
122
+ aggregateType: {
123
+ type: 'string',
124
+ description: 'The aggregate root’s type.'
125
+ },
126
+ aggregateId: {
127
+ type: 'string',
128
+ description: 'The aggregate root’s id.'
129
+ },
130
+ occurredAt: {
131
+ type: 'string',
132
+ format: 'date-time',
133
+ description: 'When the fact occurred. The list orders on it.'
134
+ },
135
+ attempts: {
136
+ type: 'integer',
137
+ description: 'Delivery attempts spent before it parked — at or above the dispatcher’s ceiling, which is what makes it a dead letter.'
138
+ },
139
+ lastError: {
140
+ type: 'string',
141
+ nullable: true,
142
+ description: 'Why the last attempt failed, truncated to roughly a line. Stack traces stay in the logs.'
143
+ }
144
+ },
145
+ required: [
146
+ 'id',
147
+ 'kind',
148
+ 'aggregateType',
149
+ 'aggregateId',
150
+ 'occurredAt',
151
+ 'attempts',
152
+ 'lastError'
153
+ ]
154
+ },
155
+ ActivityDeadLetterListView: {
156
+ type: 'object',
157
+ description: 'The parked events. A non-zero `total` means the audit trail is incomplete.',
158
+ properties: {
159
+ total: {
160
+ type: 'integer',
161
+ description: 'How many events have given up in total, ignoring `limit`.'
162
+ },
163
+ items: { type: 'array', items: ref('ActivityDeadLetter') }
164
+ },
165
+ required: ['total', 'items']
166
+ }
167
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The activity plugin's pass over the host's OpenAPI document.
3
+ *
4
+ * Three read routes, three hand-written `interface`s, and therefore three
5
+ * operations the scanner leaves as `{ '200': { description: '' } }` — see
6
+ * `packages/bootstrap/server/AGENTS.md` → "The response-schema gap". This
7
+ * writes the schemas on the way past, the way `content-server`'s pass does.
8
+ *
9
+ * Pure: it takes the document and mutates only the paths this plugin owns.
10
+ */
11
+ import type { OpenApiDocument } from '@orthacms/bootstrap-server';
12
+ /**
13
+ * Adds this plugin's schemas to `document` and attaches them to its own three
14
+ * operations.
15
+ */
16
+ export declare function describeActivityApi(document: OpenApiDocument): void;
17
+ //# sourceMappingURL=describe-activity-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe-activity-api.d.ts","sourceRoot":"","sources":["../../../src/lib/docs/describe-activity-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAuElE;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CA0BnE"}
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ /**
3
+ * The activity plugin's pass over the host's OpenAPI document.
4
+ *
5
+ * Three read routes, three hand-written `interface`s, and therefore three
6
+ * operations the scanner leaves as `{ '200': { description: '' } }` — see
7
+ * `packages/bootstrap/server/AGENTS.md` → "The response-schema gap". This
8
+ * writes the schemas on the way past, the way `content-server`'s pass does.
9
+ *
10
+ * Pure: it takes the document and mutates only the paths this plugin owns.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.describeActivityApi = describeActivityApi;
14
+ const activity_schemas_1 = require("./activity-schemas");
15
+ /**
16
+ * Matches `<prefix>/activity<rest>`, capturing what follows.
17
+ *
18
+ * The empty capture is the log itself, so the group has to be optional rather
19
+ * than `(.*)`-greedy off a trailing slash the document does not write:
20
+ * `/api/activity` has no trailing slash, and the two sub-routes do.
21
+ */
22
+ const ACTIVITY_ROUTE_RE = /\/activity(\/.*)?$/;
23
+ /** The plugin's routes, keyed by what follows `/activity`. */
24
+ const ROUTES = {
25
+ '': {
26
+ get: {
27
+ schema: 'ActivityListView',
28
+ description: 'One page of the deployment-wide audit log, matching the filters.'
29
+ }
30
+ },
31
+ '/entries/{entryId}': {
32
+ get: {
33
+ schema: 'ActivityListView',
34
+ description: 'One page of this entry’s own history, scoped to the open workspace. An entry id from another workspace reads as an empty page rather than as somebody else’s history.'
35
+ }
36
+ },
37
+ '/dead-letters': {
38
+ get: {
39
+ schema: 'ActivityDeadLetterListView',
40
+ description: 'The parked events, newest first. `total` ignores `limit`, so a caller can tell "nothing is stuck" from "the first page is full".'
41
+ }
42
+ }
43
+ };
44
+ /**
45
+ * Writes a success schema onto whichever 2xx key the scanner already emitted,
46
+ * so this never invents a status code the API does not return.
47
+ */
48
+ function setSuccessResponse(operation, schema, description) {
49
+ const responses = operation.responses ?? {};
50
+ const key = Object.keys(responses).find((code) => /^2\d\d$/.test(code));
51
+ if (!key || key === '204') {
52
+ return;
53
+ }
54
+ responses[key] = {
55
+ description,
56
+ content: { 'application/json': { schema } }
57
+ };
58
+ operation.responses = responses;
59
+ }
60
+ /**
61
+ * Adds this plugin's schemas to `document` and attaches them to its own three
62
+ * operations.
63
+ */
64
+ function describeActivityApi(document) {
65
+ document.components ??= {};
66
+ document.components.schemas ??= {};
67
+ Object.assign(document.components.schemas, activity_schemas_1.ACTIVITY_SCHEMAS);
68
+ for (const [route, item] of Object.entries(document.paths)) {
69
+ const match = ACTIVITY_ROUTE_RE.exec(route);
70
+ if (!match) {
71
+ continue;
72
+ }
73
+ const byMethod = ROUTES[match[1] ?? ''];
74
+ if (!byMethod) {
75
+ continue;
76
+ }
77
+ for (const [method, operation] of Object.entries(item)) {
78
+ const spec = byMethod[method];
79
+ if (!spec || !operation || typeof operation !== 'object') {
80
+ continue;
81
+ }
82
+ setSuccessResponse(operation, (0, activity_schemas_1.ref)(spec.schema), spec.description);
83
+ }
84
+ }
85
+ }
@@ -4,16 +4,36 @@
4
4
  * Deliberate isolation choices (the audit log must outlive what it records):
5
5
  * - `actorId` is a uuid with **no FK** — the actor may later be deleted, but
6
6
  * the audit row must remain.
7
+ * - `actorType` says what `actorId` names — a person (`user`) or an API
8
+ * credential (`api_token`). Without it `actor_id` meant "a users row" and
9
+ * nothing else, so a write made with a bearer token had to pass **no** actor
10
+ * rather than name a person who did not do it: every write over the public
11
+ * REST API, GraphQL and MCP was recorded as "System", and which of a
12
+ * workspace's tokens did it was not recoverable from anywhere. It is nullable
13
+ * because a system-initiated event still has no actor at all, and because
14
+ * every row written before this column existed names a user.
7
15
  * - `actorEmail` is a frozen snapshot of the actor's email at record time, so
8
16
  * the trail stays readable even after the user row is gone or renamed.
9
17
  * - `subjectId` is **text**, not uuid — subjects are not always users and not
10
18
  * always uuid-keyed.
19
+ * - `workspaceId` is the workspace the action happened in, or `null` when it
20
+ * happened in none. The trail records invites, role changes and workspace
21
+ * lifecycle alongside content edits and **several of those belong to no
22
+ * workspace at all**, which is why this is nullable and why it is not a
23
+ * scoping boundary: the log stays deployment-wide and `activity:read`
24
+ * remains what bounds it. What the column buys is the ability to *ask* a
25
+ * workspace-shaped question — "what happened in this workspace" — which
26
+ * previously had no answer at any price, and which is why the content
27
+ * insights punchcard reads the revision table instead of this one. Populated
28
+ * from the emitting event's own `payload.workspaceId`; a producer that has a
29
+ * workspace puts it there.
11
30
  * - `meta` is an open jsonb payload; each emitting plugin owns its shape.
12
31
  * - `at` is the logical event time (defaults to now); `createdAt` is the
13
32
  * immutable write time, dropped from the read API.
14
33
  *
15
- * Indexed for the three query shapes the read API serves: a subject's history,
16
- * an actor's history, and a kind's history — each time-ordered.
34
+ * Indexed for the four query shapes the read API serves: a subject's history,
35
+ * an actor's history, a kind's history, and a workspace's history — each
36
+ * time-ordered.
17
37
  */
18
38
  export declare const activityEvents: import("drizzle-orm/pg-core").PgTableWithColumns<{
19
39
  name: "activity_events";
@@ -104,6 +124,23 @@ export declare const activityEvents: import("drizzle-orm/pg-core").PgTableWithCo
104
124
  identity: undefined;
105
125
  generated: undefined;
106
126
  }, {}, {}>;
127
+ actorType: import("drizzle-orm/pg-core").PgColumn<{
128
+ name: "actor_type";
129
+ tableName: "activity_events";
130
+ dataType: "string";
131
+ columnType: "PgText";
132
+ data: string;
133
+ driverParam: string;
134
+ notNull: false;
135
+ hasDefault: false;
136
+ isPrimaryKey: false;
137
+ isAutoincrement: false;
138
+ hasRuntimeDefault: false;
139
+ enumValues: [string, ...string[]];
140
+ baseColumn: never;
141
+ identity: undefined;
142
+ generated: undefined;
143
+ }, {}, {}>;
107
144
  actorEmail: import("drizzle-orm/pg-core").PgColumn<{
108
145
  name: "actor_email";
109
146
  tableName: "activity_events";
@@ -121,6 +158,23 @@ export declare const activityEvents: import("drizzle-orm/pg-core").PgTableWithCo
121
158
  identity: undefined;
122
159
  generated: undefined;
123
160
  }, {}, {}>;
161
+ workspaceId: import("drizzle-orm/pg-core").PgColumn<{
162
+ name: "workspace_id";
163
+ tableName: "activity_events";
164
+ dataType: "string";
165
+ columnType: "PgUUID";
166
+ data: string;
167
+ driverParam: string;
168
+ notNull: false;
169
+ hasDefault: false;
170
+ isPrimaryKey: false;
171
+ isAutoincrement: false;
172
+ hasRuntimeDefault: false;
173
+ enumValues: undefined;
174
+ baseColumn: never;
175
+ identity: undefined;
176
+ generated: undefined;
177
+ }, {}, {}>;
124
178
  meta: import("drizzle-orm/pg-core").PgColumn<{
125
179
  name: "meta";
126
180
  tableName: "activity_events";
@@ -1 +1 @@
1
- {"version":3,"file":"activity-events.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/activity-events.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwB1B,CAAC"}
1
+ {"version":3,"file":"activity-events.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/activity-events.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2B1B,CAAC"}
@@ -8,16 +8,36 @@ const pg_core_1 = require("drizzle-orm/pg-core");
8
8
  * Deliberate isolation choices (the audit log must outlive what it records):
9
9
  * - `actorId` is a uuid with **no FK** — the actor may later be deleted, but
10
10
  * the audit row must remain.
11
+ * - `actorType` says what `actorId` names — a person (`user`) or an API
12
+ * credential (`api_token`). Without it `actor_id` meant "a users row" and
13
+ * nothing else, so a write made with a bearer token had to pass **no** actor
14
+ * rather than name a person who did not do it: every write over the public
15
+ * REST API, GraphQL and MCP was recorded as "System", and which of a
16
+ * workspace's tokens did it was not recoverable from anywhere. It is nullable
17
+ * because a system-initiated event still has no actor at all, and because
18
+ * every row written before this column existed names a user.
11
19
  * - `actorEmail` is a frozen snapshot of the actor's email at record time, so
12
20
  * the trail stays readable even after the user row is gone or renamed.
13
21
  * - `subjectId` is **text**, not uuid — subjects are not always users and not
14
22
  * always uuid-keyed.
23
+ * - `workspaceId` is the workspace the action happened in, or `null` when it
24
+ * happened in none. The trail records invites, role changes and workspace
25
+ * lifecycle alongside content edits and **several of those belong to no
26
+ * workspace at all**, which is why this is nullable and why it is not a
27
+ * scoping boundary: the log stays deployment-wide and `activity:read`
28
+ * remains what bounds it. What the column buys is the ability to *ask* a
29
+ * workspace-shaped question — "what happened in this workspace" — which
30
+ * previously had no answer at any price, and which is why the content
31
+ * insights punchcard reads the revision table instead of this one. Populated
32
+ * from the emitting event's own `payload.workspaceId`; a producer that has a
33
+ * workspace puts it there.
15
34
  * - `meta` is an open jsonb payload; each emitting plugin owns its shape.
16
35
  * - `at` is the logical event time (defaults to now); `createdAt` is the
17
36
  * immutable write time, dropped from the read API.
18
37
  *
19
- * Indexed for the three query shapes the read API serves: a subject's history,
20
- * an actor's history, and a kind's history — each time-ordered.
38
+ * Indexed for the four query shapes the read API serves: a subject's history,
39
+ * an actor's history, a kind's history, and a workspace's history — each
40
+ * time-ordered.
21
41
  */
22
42
  exports.activityEvents = (0, pg_core_1.pgTable)('activity_events', {
23
43
  id: (0, pg_core_1.uuid)('id').primaryKey().defaultRandom(),
@@ -25,7 +45,9 @@ exports.activityEvents = (0, pg_core_1.pgTable)('activity_events', {
25
45
  subjectType: (0, pg_core_1.text)('subject_type').notNull(),
26
46
  subjectId: (0, pg_core_1.text)('subject_id').notNull(),
27
47
  actorId: (0, pg_core_1.uuid)('actor_id'),
48
+ actorType: (0, pg_core_1.text)('actor_type'),
28
49
  actorEmail: (0, pg_core_1.text)('actor_email'),
50
+ workspaceId: (0, pg_core_1.uuid)('workspace_id'),
29
51
  meta: (0, pg_core_1.jsonb)('meta'),
30
52
  at: (0, pg_core_1.timestamp)('at', { withTimezone: true }).notNull().defaultNow(),
31
53
  createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true })
@@ -34,5 +56,6 @@ exports.activityEvents = (0, pg_core_1.pgTable)('activity_events', {
34
56
  }, (table) => [
35
57
  (0, pg_core_1.index)('activity_events_subject_idx').on(table.subjectType, table.subjectId, table.at),
36
58
  (0, pg_core_1.index)('activity_events_actor_idx').on(table.actorId, table.at),
37
- (0, pg_core_1.index)('activity_events_kind_idx').on(table.kind, table.at)
59
+ (0, pg_core_1.index)('activity_events_kind_idx').on(table.kind, table.at),
60
+ (0, pg_core_1.index)('activity_events_workspace_idx').on(table.workspaceId, table.at)
38
61
  ]);
@@ -1 +1 @@
1
- {"version":3,"file":"activity-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/activity-plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG/D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,IAAI,YAAY,CAS7C"}
1
+ {"version":3,"file":"activity-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/activity-plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAI/D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,IAAI,YAAY,CAY7C"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ActivityPlugin = ActivityPlugin;
4
4
  const node_path_1 = require("node:path");
5
5
  const activity_module_1 = require("../activity.module");
6
+ const describe_activity_api_1 = require("../docs/describe-activity-api");
6
7
  /**
7
8
  * Server plugin for the audit log. A plain {@link ServerPlugin} — the feature
8
9
  * carries no host config.
@@ -33,6 +34,9 @@ function ActivityPlugin() {
33
34
  migrations: {
34
35
  dir: () => (0, node_path_1.join)(__dirname, '../../../migrations'),
35
36
  table: '__drizzle_migrations_activity'
36
- }
37
+ },
38
+ // The three read routes answer plain `interface`s, which the OpenAPI
39
+ // scanner cannot see and no configuration switch can infer.
40
+ docs: { decorate: describe_activity_api_1.describeActivityApi }
37
41
  };
38
42
  }
@@ -0,0 +1,8 @@
1
+ ALTER TABLE "activity_events" ADD COLUMN "actor_type" text;--> statement-breakpoint
2
+ ALTER TABLE "activity_events" ADD COLUMN "workspace_id" uuid;--> statement-breakpoint
3
+ CREATE INDEX "activity_events_workspace_idx" ON "activity_events" USING btree ("workspace_id","at");--> statement-breakpoint
4
+ -- Backfill: every actor recorded before this column existed is a person. Nothing
5
+ -- else could be one — a token-authenticated write passed no actor at all — so
6
+ -- this is a statement of fact rather than a guess, and it keeps a reader from
7
+ -- having to treat a null `actor_type` on an actored row as "unknown kind".
8
+ UPDATE "activity_events" SET "actor_type" = 'user' WHERE "actor_id" IS NOT NULL AND "actor_type" IS NULL;