@byline/core 3.7.0 → 3.9.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.
- package/dist/@types/db-types.d.ts +20 -0
- package/dist/schemas/zod/builder.d.ts +42 -0
- package/dist/schemas/zod/builder.js +5 -0
- package/dist/services/document-lifecycle/copy-to-locale.js +2 -1
- package/dist/services/document-lifecycle/create.js +2 -1
- package/dist/services/document-lifecycle/delete-locale.js +2 -1
- package/dist/services/document-lifecycle/duplicate.js +3 -1
- package/dist/services/document-lifecycle/internals.d.ts +16 -0
- package/dist/services/document-lifecycle/internals.js +24 -0
- package/dist/services/document-lifecycle/restore.js +2 -1
- package/dist/services/document-lifecycle/update.js +3 -1
- package/dist/services/document-lifecycle.test.node.js +37 -1
- package/package.json +2 -2
|
@@ -208,6 +208,24 @@ export interface IDbAdapter {
|
|
|
208
208
|
collections: ICollectionQueries;
|
|
209
209
|
documents: IDocumentQueries;
|
|
210
210
|
};
|
|
211
|
+
/**
|
|
212
|
+
* Optional capability: run `fn` inside a single database transaction so the
|
|
213
|
+
* writes it performs commit or roll back atomically. The adapter propagates
|
|
214
|
+
* the transaction to every `commands.*` call made within `fn` (see
|
|
215
|
+
* docs/TRANSACTIONS.md — AsyncLocalStorage propagation), so a service can
|
|
216
|
+
* compose multiple commands into one unit of work without threading a
|
|
217
|
+
* transaction handle through their signatures.
|
|
218
|
+
*
|
|
219
|
+
* **Loud-failure contract.** Optional because not every adapter can provide
|
|
220
|
+
* interactive transactions — a pure HTTP-gateway serverless driver (Neon
|
|
221
|
+
* HTTP, Cloudflare D1, …) cannot. An adapter that cannot **must omit this
|
|
222
|
+
* method** (or implement it to throw); a consumer that requires atomicity
|
|
223
|
+
* (e.g. the audit log) MUST assert its presence and throw — never silently
|
|
224
|
+
* run non-atomically, which would defeat the very guarantee it provides. See
|
|
225
|
+
* docs/TRANSACTIONS.md ("Serverless / HTTP-gateway databases — the contract
|
|
226
|
+
* seam").
|
|
227
|
+
*/
|
|
228
|
+
withTransaction?: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
211
229
|
/**
|
|
212
230
|
* Optional maintenance: stamp `source_locale` (the per-document content
|
|
213
231
|
* anchor) on documents created before the column existed, setting NULL rows
|
|
@@ -415,6 +433,8 @@ export interface IDocumentCommands {
|
|
|
415
433
|
documentId: string;
|
|
416
434
|
locale: string;
|
|
417
435
|
status?: string;
|
|
436
|
+
/** Acting user id for the version audit trail (`created_by`). See docs/AUDIT.md. */
|
|
437
|
+
createdBy?: string;
|
|
418
438
|
}): Promise<{
|
|
419
439
|
newVersionId: string;
|
|
420
440
|
previousVersionId: string;
|
|
@@ -12,6 +12,8 @@ export declare const createBaseSchema: (collection?: CollectionDefinition) => z.
|
|
|
12
12
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
13
13
|
createdAt: z.ZodISODateTime;
|
|
14
14
|
updatedAt: z.ZodISODateTime;
|
|
15
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
16
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
15
17
|
}, z.core.$strip>;
|
|
16
18
|
export declare const createFieldsSchema: (fields: Field[], strict?: boolean) => z.ZodObject<{
|
|
17
19
|
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
@@ -44,6 +46,8 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
44
46
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
45
47
|
createdAt: z.ZodISODateTime;
|
|
46
48
|
updatedAt: z.ZodISODateTime;
|
|
49
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
50
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
47
51
|
}, z.core.$strip>;
|
|
48
52
|
fields: z.ZodObject<{
|
|
49
53
|
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
@@ -62,6 +66,8 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
62
66
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
63
67
|
createdAt: z.ZodISODateTime;
|
|
64
68
|
updatedAt: z.ZodISODateTime;
|
|
69
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
70
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
65
71
|
}, z.core.$strip>;
|
|
66
72
|
list: z.ZodObject<{
|
|
67
73
|
docs: z.ZodArray<z.ZodObject<{
|
|
@@ -78,6 +84,8 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
78
84
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
79
85
|
createdAt: z.ZodISODateTime;
|
|
80
86
|
updatedAt: z.ZodISODateTime;
|
|
87
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
88
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
81
89
|
}, z.core.$strip>>;
|
|
82
90
|
meta: z.ZodObject<{
|
|
83
91
|
page: z.ZodNumber;
|
|
@@ -113,6 +121,8 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
113
121
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
114
122
|
createdAt: z.ZodISODateTime;
|
|
115
123
|
updatedAt: z.ZodISODateTime;
|
|
124
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
125
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
116
126
|
}, z.core.$strip>>;
|
|
117
127
|
meta: z.ZodObject<{
|
|
118
128
|
page: z.ZodNumber;
|
|
@@ -140,6 +150,8 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
140
150
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
141
151
|
createdAt: z.ZodISODateTime;
|
|
142
152
|
updatedAt: z.ZodISODateTime;
|
|
153
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
154
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
143
155
|
}, z.core.$strip>;
|
|
144
156
|
update: z.ZodObject<{
|
|
145
157
|
[x: string]: z.ZodOptional<z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>>;
|
|
@@ -157,6 +169,8 @@ export declare const createCollectionSchemas: (collection: CollectionDefinition)
|
|
|
157
169
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
158
170
|
createdAt: z.ZodISODateTime;
|
|
159
171
|
updatedAt: z.ZodISODateTime;
|
|
172
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
173
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
160
174
|
}, z.core.$strip>;
|
|
161
175
|
fields: z.ZodObject<{
|
|
162
176
|
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
@@ -175,6 +189,8 @@ export declare const createCollectionSchemas: (collection: CollectionDefinition)
|
|
|
175
189
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
176
190
|
createdAt: z.ZodISODateTime;
|
|
177
191
|
updatedAt: z.ZodISODateTime;
|
|
192
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
193
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
178
194
|
}, z.core.$strip>;
|
|
179
195
|
list: z.ZodObject<{
|
|
180
196
|
docs: z.ZodArray<z.ZodObject<{
|
|
@@ -191,6 +207,8 @@ export declare const createCollectionSchemas: (collection: CollectionDefinition)
|
|
|
191
207
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
192
208
|
createdAt: z.ZodISODateTime;
|
|
193
209
|
updatedAt: z.ZodISODateTime;
|
|
210
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
211
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
194
212
|
}, z.core.$strip>>;
|
|
195
213
|
meta: z.ZodObject<{
|
|
196
214
|
page: z.ZodNumber;
|
|
@@ -226,6 +244,8 @@ export declare const createCollectionSchemas: (collection: CollectionDefinition)
|
|
|
226
244
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
227
245
|
createdAt: z.ZodISODateTime;
|
|
228
246
|
updatedAt: z.ZodISODateTime;
|
|
247
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
248
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
229
249
|
}, z.core.$strip>>;
|
|
230
250
|
meta: z.ZodObject<{
|
|
231
251
|
page: z.ZodNumber;
|
|
@@ -253,6 +273,8 @@ export declare const createCollectionSchemas: (collection: CollectionDefinition)
|
|
|
253
273
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
254
274
|
createdAt: z.ZodISODateTime;
|
|
255
275
|
updatedAt: z.ZodISODateTime;
|
|
276
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
277
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
256
278
|
}, z.core.$strip>;
|
|
257
279
|
update: z.ZodObject<{
|
|
258
280
|
[x: string]: z.ZodOptional<z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>>;
|
|
@@ -270,6 +292,8 @@ export declare const createTypedCollectionSchemas: (collection: CollectionDefini
|
|
|
270
292
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
271
293
|
createdAt: z.ZodISODateTime;
|
|
272
294
|
updatedAt: z.ZodISODateTime;
|
|
295
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
296
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
273
297
|
}, z.core.$strip>;
|
|
274
298
|
fields: z.ZodObject<{
|
|
275
299
|
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
@@ -288,6 +312,8 @@ export declare const createTypedCollectionSchemas: (collection: CollectionDefini
|
|
|
288
312
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
289
313
|
createdAt: z.ZodISODateTime;
|
|
290
314
|
updatedAt: z.ZodISODateTime;
|
|
315
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
316
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
291
317
|
}, z.core.$strip>;
|
|
292
318
|
list: z.ZodObject<{
|
|
293
319
|
docs: z.ZodArray<z.ZodObject<{
|
|
@@ -304,6 +330,8 @@ export declare const createTypedCollectionSchemas: (collection: CollectionDefini
|
|
|
304
330
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
305
331
|
createdAt: z.ZodISODateTime;
|
|
306
332
|
updatedAt: z.ZodISODateTime;
|
|
333
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
334
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
307
335
|
}, z.core.$strip>>;
|
|
308
336
|
meta: z.ZodObject<{
|
|
309
337
|
page: z.ZodNumber;
|
|
@@ -339,6 +367,8 @@ export declare const createTypedCollectionSchemas: (collection: CollectionDefini
|
|
|
339
367
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
340
368
|
createdAt: z.ZodISODateTime;
|
|
341
369
|
updatedAt: z.ZodISODateTime;
|
|
370
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
371
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
342
372
|
}, z.core.$strip>>;
|
|
343
373
|
meta: z.ZodObject<{
|
|
344
374
|
page: z.ZodNumber;
|
|
@@ -366,6 +396,8 @@ export declare const createTypedCollectionSchemas: (collection: CollectionDefini
|
|
|
366
396
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
367
397
|
createdAt: z.ZodISODateTime;
|
|
368
398
|
updatedAt: z.ZodISODateTime;
|
|
399
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
400
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
369
401
|
}, z.core.$strip>;
|
|
370
402
|
update: z.ZodObject<{
|
|
371
403
|
[x: string]: z.ZodOptional<z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>>;
|
|
@@ -383,6 +415,8 @@ export declare const createTypedCollectionSchemasForPath: (path: string) => {
|
|
|
383
415
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
384
416
|
createdAt: z.ZodISODateTime;
|
|
385
417
|
updatedAt: z.ZodISODateTime;
|
|
418
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
419
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
386
420
|
}, z.core.$strip>;
|
|
387
421
|
fields: z.ZodObject<{
|
|
388
422
|
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
@@ -401,6 +435,8 @@ export declare const createTypedCollectionSchemasForPath: (path: string) => {
|
|
|
401
435
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
402
436
|
createdAt: z.ZodISODateTime;
|
|
403
437
|
updatedAt: z.ZodISODateTime;
|
|
438
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
439
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
404
440
|
}, z.core.$strip>;
|
|
405
441
|
list: z.ZodObject<{
|
|
406
442
|
docs: z.ZodArray<z.ZodObject<{
|
|
@@ -417,6 +453,8 @@ export declare const createTypedCollectionSchemasForPath: (path: string) => {
|
|
|
417
453
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
418
454
|
createdAt: z.ZodISODateTime;
|
|
419
455
|
updatedAt: z.ZodISODateTime;
|
|
456
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
457
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
420
458
|
}, z.core.$strip>>;
|
|
421
459
|
meta: z.ZodObject<{
|
|
422
460
|
page: z.ZodNumber;
|
|
@@ -452,6 +490,8 @@ export declare const createTypedCollectionSchemasForPath: (path: string) => {
|
|
|
452
490
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
453
491
|
createdAt: z.ZodISODateTime;
|
|
454
492
|
updatedAt: z.ZodISODateTime;
|
|
493
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
494
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
455
495
|
}, z.core.$strip>>;
|
|
456
496
|
meta: z.ZodObject<{
|
|
457
497
|
page: z.ZodNumber;
|
|
@@ -479,6 +519,8 @@ export declare const createTypedCollectionSchemasForPath: (path: string) => {
|
|
|
479
519
|
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
480
520
|
createdAt: z.ZodISODateTime;
|
|
481
521
|
updatedAt: z.ZodISODateTime;
|
|
522
|
+
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
523
|
+
eventType: z.ZodOptional<z.ZodString>;
|
|
482
524
|
}, z.core.$strip>;
|
|
483
525
|
update: z.ZodObject<{
|
|
484
526
|
[x: string]: z.ZodOptional<z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>>;
|
|
@@ -205,6 +205,11 @@ export const createBaseSchema = (collection) => {
|
|
|
205
205
|
hasPublishedVersion: z.boolean().optional(),
|
|
206
206
|
createdAt: z.iso.datetime(),
|
|
207
207
|
updatedAt: z.iso.datetime(),
|
|
208
|
+
// Version audit metadata — acting user + action (see docs/AUDIT.md — Workstream 1).
|
|
209
|
+
// Declared so list/get/history responses carry them through the
|
|
210
|
+
// server-fn parse; Zod would otherwise strip them as undeclared keys.
|
|
211
|
+
createdBy: z.uuid().optional(),
|
|
212
|
+
eventType: z.string().optional(),
|
|
208
213
|
});
|
|
209
214
|
};
|
|
210
215
|
// Create field schemas for a collection.
|
|
@@ -10,7 +10,7 @@ import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
|
10
10
|
import { ERR_NOT_FOUND, ERR_VALIDATION } from '../../lib/errors.js';
|
|
11
11
|
import { withLogContext } from '../../lib/logger.js';
|
|
12
12
|
import { getDefaultStatus } from '../../workflow/workflow.js';
|
|
13
|
-
import { applyRichTextEmbed, extractVersionId, invokeHook } from './internals.js';
|
|
13
|
+
import { actorId, applyRichTextEmbed, extractVersionId, invokeHook } from './internals.js';
|
|
14
14
|
import { mergeLocaleData } from './merge-locale-data.js';
|
|
15
15
|
/**
|
|
16
16
|
* Copy a document's content from one locale into another, in place on
|
|
@@ -133,6 +133,7 @@ export async function copyToLocale(ctx, params) {
|
|
|
133
133
|
status: getDefaultStatus(definition),
|
|
134
134
|
locale: params.targetLocale,
|
|
135
135
|
previousVersionId,
|
|
136
|
+
createdBy: actorId(ctx),
|
|
136
137
|
});
|
|
137
138
|
const documentVersionId = extractVersionId(writeResult.document);
|
|
138
139
|
await invokeHook(hooks?.afterUpdate, {
|
|
@@ -13,7 +13,7 @@ import { normaliseDateFields } from '../../utils/normalise-dates.js';
|
|
|
13
13
|
import { slugify } from '../../utils/slugify.js';
|
|
14
14
|
import { getDefaultStatus } from '../../workflow/workflow.js';
|
|
15
15
|
import { assignCounterValues } from '../assign-counter-values.js';
|
|
16
|
-
import { applyRichTextEmbed, derivePath, extractDocumentId, extractVersionId, invokeHook, maybeAppendOrderKey, rethrowPathConflict, } from './internals.js';
|
|
16
|
+
import { actorId, applyRichTextEmbed, derivePath, extractDocumentId, extractVersionId, invokeHook, maybeAppendOrderKey, rethrowPathConflict, } from './internals.js';
|
|
17
17
|
/**
|
|
18
18
|
* Create a new document.
|
|
19
19
|
*
|
|
@@ -75,6 +75,7 @@ export async function createDocument(ctx, params) {
|
|
|
75
75
|
status: params.status ?? data.status ?? getDefaultStatus(definition),
|
|
76
76
|
locale: params.locale ?? defaultLocale,
|
|
77
77
|
orderKey,
|
|
78
|
+
createdBy: actorId(ctx),
|
|
78
79
|
})
|
|
79
80
|
.catch((err) => rethrowPathConflict(err, resolvedPath, defaultLocale));
|
|
80
81
|
const documentId = extractDocumentId(result.document);
|
|
@@ -10,7 +10,7 @@ import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
|
10
10
|
import { ERR_NOT_FOUND, ERR_VALIDATION } from '../../lib/errors.js';
|
|
11
11
|
import { withLogContext } from '../../lib/logger.js';
|
|
12
12
|
import { getDefaultStatus } from '../../workflow/workflow.js';
|
|
13
|
-
import { invokeHook } from './internals.js';
|
|
13
|
+
import { actorId, invokeHook } from './internals.js';
|
|
14
14
|
/**
|
|
15
15
|
* Remove one content locale's data from a document, in place on the same
|
|
16
16
|
* document, by writing a new immutable version that omits that locale's
|
|
@@ -90,6 +90,7 @@ export async function deleteLocale(ctx, params) {
|
|
|
90
90
|
documentId: params.documentId,
|
|
91
91
|
locale: params.locale,
|
|
92
92
|
status: getDefaultStatus(definition),
|
|
93
|
+
createdBy: actorId(ctx),
|
|
93
94
|
});
|
|
94
95
|
if (result == null) {
|
|
95
96
|
throw ERR_NOT_FOUND({
|
|
@@ -12,7 +12,7 @@ import { withLogContext } from '../../lib/logger.js';
|
|
|
12
12
|
import { slugify } from '../../utils/slugify.js';
|
|
13
13
|
import { getDefaultStatus } from '../../workflow/workflow.js';
|
|
14
14
|
import { assignCounterValues } from '../assign-counter-values.js';
|
|
15
|
-
import { applyRichTextEmbed, derivePath, extractDocumentId, extractVersionId, invokeHook, isPathConflictError, maybeAppendOrderKey, rethrowPathConflict, stripMetaIdsInPlace, } from './internals.js';
|
|
15
|
+
import { actorId, applyRichTextEmbed, derivePath, extractDocumentId, extractVersionId, invokeHook, isPathConflictError, maybeAppendOrderKey, rethrowPathConflict, stripMetaIdsInPlace, } from './internals.js';
|
|
16
16
|
/**
|
|
17
17
|
* Apply the `" (copy)"` suffix to the configured `useAsTitle` field on a
|
|
18
18
|
* duplicate's data tree. Handles both shapes:
|
|
@@ -170,6 +170,7 @@ export async function duplicateDocument(ctx, params) {
|
|
|
170
170
|
status: defaultStatus,
|
|
171
171
|
locale: 'all',
|
|
172
172
|
orderKey,
|
|
173
|
+
createdBy: actorId(ctx),
|
|
173
174
|
})
|
|
174
175
|
.catch((err) => rethrowPathConflict(err, finalPath, defaultLocale));
|
|
175
176
|
}
|
|
@@ -194,6 +195,7 @@ export async function duplicateDocument(ctx, params) {
|
|
|
194
195
|
status: defaultStatus,
|
|
195
196
|
locale: 'all',
|
|
196
197
|
orderKey,
|
|
198
|
+
createdBy: actorId(ctx),
|
|
197
199
|
})
|
|
198
200
|
.catch((retryErr) => rethrowPathConflict(retryErr, finalPath, defaultLocale));
|
|
199
201
|
}
|
|
@@ -15,6 +15,22 @@ import { type CollectionDefinition, type CollectionHookSlot } from '../../@types
|
|
|
15
15
|
import type { BylineLogger } from '../../lib/logger.js';
|
|
16
16
|
import type { SlugifierFn } from '../../utils/slugify.js';
|
|
17
17
|
import type { DocumentLifecycleContext } from './context.js';
|
|
18
|
+
/**
|
|
19
|
+
* The acting user's id for the version audit trail (`created_by` on
|
|
20
|
+
* `byline_document_versions`).
|
|
21
|
+
*
|
|
22
|
+
* Returns the id only when it is a real **persisted user id** — i.e. a UUID.
|
|
23
|
+
* Synthetic actors used by scripts, seeds, and tests (e.g.
|
|
24
|
+
* `createSuperAdminContext({ id: 'import-docs-script' })`, or the default
|
|
25
|
+
* `'super-admin'`) are **not** users: their non-UUID ids would be rejected by
|
|
26
|
+
* the `uuid` column outright, and the correct audit value for a system/tooling
|
|
27
|
+
* write is NULL regardless. So a non-UUID id — and a missing `requestContext`
|
|
28
|
+
* (the seeds/migrations escape hatch) — both yield `undefined` → NULL
|
|
29
|
+
* `created_by`, which the history strip renders as "unknown". Real
|
|
30
|
+
* `AdminAuth` / `UserAuth` actors always carry UUID ids, so their attribution
|
|
31
|
+
* is unaffected. See docs/AUDIT.md — Workstream 1.
|
|
32
|
+
*/
|
|
33
|
+
export declare function actorId(ctx: DocumentLifecycleContext): string | undefined;
|
|
18
34
|
/**
|
|
19
35
|
* Safely invoke an optional hook slot, awaiting the result if it returns a
|
|
20
36
|
* Promise. When the slot is an array of functions they are executed
|
|
@@ -17,6 +17,30 @@ import { ERR_PATH_CONFLICT, ErrorCodes } from '../../lib/errors.js';
|
|
|
17
17
|
import { generateKeyBetween } from '../../lib/fractional-index.js';
|
|
18
18
|
import { createReadContext } from '../populate.js';
|
|
19
19
|
import { embedRichTextFields } from '../richtext-embed.js';
|
|
20
|
+
/**
|
|
21
|
+
* Matches a canonical UUID (any version). Real admin / end-user actors carry
|
|
22
|
+
* UUID ids (`uuidv7`); synthetic actors do not.
|
|
23
|
+
*/
|
|
24
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
25
|
+
/**
|
|
26
|
+
* The acting user's id for the version audit trail (`created_by` on
|
|
27
|
+
* `byline_document_versions`).
|
|
28
|
+
*
|
|
29
|
+
* Returns the id only when it is a real **persisted user id** — i.e. a UUID.
|
|
30
|
+
* Synthetic actors used by scripts, seeds, and tests (e.g.
|
|
31
|
+
* `createSuperAdminContext({ id: 'import-docs-script' })`, or the default
|
|
32
|
+
* `'super-admin'`) are **not** users: their non-UUID ids would be rejected by
|
|
33
|
+
* the `uuid` column outright, and the correct audit value for a system/tooling
|
|
34
|
+
* write is NULL regardless. So a non-UUID id — and a missing `requestContext`
|
|
35
|
+
* (the seeds/migrations escape hatch) — both yield `undefined` → NULL
|
|
36
|
+
* `created_by`, which the history strip renders as "unknown". Real
|
|
37
|
+
* `AdminAuth` / `UserAuth` actors always carry UUID ids, so their attribution
|
|
38
|
+
* is unaffected. See docs/AUDIT.md — Workstream 1.
|
|
39
|
+
*/
|
|
40
|
+
export function actorId(ctx) {
|
|
41
|
+
const id = ctx.requestContext?.actor?.id;
|
|
42
|
+
return id != null && UUID_RE.test(id) ? id : undefined;
|
|
43
|
+
}
|
|
20
44
|
/**
|
|
21
45
|
* Safely invoke an optional hook slot, awaiting the result if it returns a
|
|
22
46
|
* Promise. When the slot is an array of functions they are executed
|
|
@@ -10,7 +10,7 @@ import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
|
10
10
|
import { ERR_INVALID_TRANSITION, ERR_NOT_FOUND, ERR_VALIDATION } from '../../lib/errors.js';
|
|
11
11
|
import { withLogContext } from '../../lib/logger.js';
|
|
12
12
|
import { getDefaultStatus } from '../../workflow/workflow.js';
|
|
13
|
-
import { applyRichTextEmbed, extractDocumentId, extractVersionId, invokeHook } from './internals.js';
|
|
13
|
+
import { actorId, applyRichTextEmbed, extractDocumentId, extractVersionId, invokeHook, } from './internals.js';
|
|
14
14
|
/**
|
|
15
15
|
* Restore a historical document version as the new current version.
|
|
16
16
|
*
|
|
@@ -139,6 +139,7 @@ export async function restoreDocumentVersion(ctx, params) {
|
|
|
139
139
|
status: getDefaultStatus(definition),
|
|
140
140
|
locale: 'all',
|
|
141
141
|
previousVersionId: currentMeta.document_version_id,
|
|
142
|
+
createdBy: actorId(ctx),
|
|
142
143
|
});
|
|
143
144
|
const documentId = extractDocumentId(result.document) || params.documentId;
|
|
144
145
|
const documentVersionId = extractVersionId(result.document);
|
|
@@ -13,7 +13,7 @@ import { applyPatches } from '../../patches/index.js';
|
|
|
13
13
|
import { normaliseDateFields } from '../../utils/normalise-dates.js';
|
|
14
14
|
import { getDefaultStatus } from '../../workflow/workflow.js';
|
|
15
15
|
import { assignCounterValues } from '../assign-counter-values.js';
|
|
16
|
-
import { applyRichTextEmbed, extractDocumentId, extractVersionId, invokeHook, resolvePathForUpdate, rethrowPathConflict, } from './internals.js';
|
|
16
|
+
import { actorId, applyRichTextEmbed, extractDocumentId, extractVersionId, invokeHook, resolvePathForUpdate, rethrowPathConflict, } from './internals.js';
|
|
17
17
|
/**
|
|
18
18
|
* Update a document via full replacement (PUT semantics).
|
|
19
19
|
*
|
|
@@ -85,6 +85,7 @@ export async function updateDocument(ctx, params) {
|
|
|
85
85
|
status: defaultStatus,
|
|
86
86
|
locale: requestLocale,
|
|
87
87
|
previousVersionId: originalData.document_version_id,
|
|
88
|
+
createdBy: actorId(ctx),
|
|
88
89
|
})
|
|
89
90
|
.catch((err) => rethrowPathConflict(err, pathForCommand ?? '', defaultLocale));
|
|
90
91
|
const documentId = extractDocumentId(result.document) || params.documentId;
|
|
@@ -198,6 +199,7 @@ export async function updateDocumentWithPatches(ctx, params) {
|
|
|
198
199
|
status: defaultStatus,
|
|
199
200
|
locale: requestLocale,
|
|
200
201
|
previousVersionId: originalData.document_version_id,
|
|
202
|
+
createdBy: actorId(ctx),
|
|
201
203
|
})
|
|
202
204
|
.catch((err) => rethrowPathConflict(err, pathForCommand ?? '', defaultLocale));
|
|
203
205
|
const documentId = extractDocumentId(result.document) || params.documentId;
|
|
@@ -104,6 +104,10 @@ const noopLogger = {
|
|
|
104
104
|
trace: vi.fn(),
|
|
105
105
|
silent: vi.fn(),
|
|
106
106
|
};
|
|
107
|
+
// A real persisted-user id is a UUID; `actorId()` only attributes UUIDs (see
|
|
108
|
+
// the regression note below). Use a valid UUID so the default context's
|
|
109
|
+
// writes are attributed.
|
|
110
|
+
const TEST_ACTOR_ID = '01901234-0000-7000-8000-000000000001';
|
|
107
111
|
function buildCtx(db, definition = minimalCollection) {
|
|
108
112
|
return {
|
|
109
113
|
db,
|
|
@@ -117,7 +121,7 @@ function buildCtx(db, definition = minimalCollection) {
|
|
|
117
121
|
// tests do not have to care about ability enforcement. The dedicated
|
|
118
122
|
// "enforcement" block below covers the missing-context / missing-ability
|
|
119
123
|
// negative cases.
|
|
120
|
-
requestContext: createSuperAdminContext({ id:
|
|
124
|
+
requestContext: createSuperAdminContext({ id: TEST_ACTOR_ID }),
|
|
121
125
|
};
|
|
122
126
|
}
|
|
123
127
|
// ---------------------------------------------------------------------------
|
|
@@ -139,6 +143,28 @@ describe('Document lifecycle service', () => {
|
|
|
139
143
|
expect(result.documentId).toBe('doc-1');
|
|
140
144
|
expect(result.documentVersionId).toBe('ver-1');
|
|
141
145
|
});
|
|
146
|
+
it('passes the acting user id as createdBy for the audit trail', async () => {
|
|
147
|
+
const { db, createDocumentVersion } = createMockDb();
|
|
148
|
+
const ctx = buildCtx(db);
|
|
149
|
+
await createDocument(ctx, {
|
|
150
|
+
data: { title: 'Hello' },
|
|
151
|
+
locale: 'en',
|
|
152
|
+
});
|
|
153
|
+
// Audit contract (docs/AUDIT.md — W1): every version row
|
|
154
|
+
// records the actor that created it.
|
|
155
|
+
expect(createDocumentVersion.mock.calls[0]?.[0].createdBy).toBe(TEST_ACTOR_ID);
|
|
156
|
+
});
|
|
157
|
+
it('writes NULL createdBy for a synthetic (non-UUID) script/seed actor', async () => {
|
|
158
|
+
// Regression guard (v3.8.0): a synthetic super-admin id such as
|
|
159
|
+
// `import-docs-script` is not a real user and is not a UUID — writing
|
|
160
|
+
// it into the `created_by` UUID column crashed every import/seed. Such
|
|
161
|
+
// system/tooling writes must attribute to NULL, not the synthetic id.
|
|
162
|
+
const { db, createDocumentVersion } = createMockDb();
|
|
163
|
+
const ctx = buildCtx(db);
|
|
164
|
+
ctx.requestContext = createSuperAdminContext({ id: 'import-docs-script' });
|
|
165
|
+
await createDocument(ctx, { data: { title: 'Hello' }, locale: 'en' });
|
|
166
|
+
expect(createDocumentVersion.mock.calls[0]?.[0].createdBy).toBeUndefined();
|
|
167
|
+
});
|
|
142
168
|
it('invokes beforeCreate and afterCreate hooks in order', async () => {
|
|
143
169
|
const callOrder = [];
|
|
144
170
|
const hooks = {
|
|
@@ -307,6 +333,16 @@ describe('Document lifecycle service', () => {
|
|
|
307
333
|
// updateDocument (PUT)
|
|
308
334
|
// -----------------------------------------------------------------------
|
|
309
335
|
describe('updateDocument', () => {
|
|
336
|
+
it('passes the acting user id as createdBy for the audit trail', async () => {
|
|
337
|
+
const { db, getDocumentById, createDocumentVersion } = createMockDb();
|
|
338
|
+
getDocumentById.mockResolvedValue({ status: 'draft', fields: { title: 'Old' } });
|
|
339
|
+
const ctx = buildCtx(db);
|
|
340
|
+
await updateDocument(ctx, {
|
|
341
|
+
documentId: 'doc-1',
|
|
342
|
+
data: { title: 'New' },
|
|
343
|
+
});
|
|
344
|
+
expect(createDocumentVersion.mock.calls[0]?.[0].createdBy).toBe(TEST_ACTOR_ID);
|
|
345
|
+
});
|
|
310
346
|
it('fetches the original before calling hooks', async () => {
|
|
311
347
|
const { db, getDocumentById, createDocumentVersion } = createMockDb();
|
|
312
348
|
getDocumentById.mockResolvedValue({ status: 'draft', fields: { title: 'Old' } });
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/core",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.9.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"sharp": "^0.34.5",
|
|
80
80
|
"uuid": "^14.0.0",
|
|
81
81
|
"zod": "^4.4.3",
|
|
82
|
-
"@byline/auth": "3.
|
|
82
|
+
"@byline/auth": "3.9.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@biomejs/biome": "2.4.15",
|