@byline/core 3.0.2 → 3.1.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/collection-types.d.ts +39 -1
- package/dist/@types/db-types.d.ts +15 -0
- package/dist/services/collection-bootstrap.test.node.js +2 -0
- package/dist/services/discover-counter-groups.test.node.js +1 -0
- package/dist/services/document-lifecycle.js +35 -2
- package/dist/services/document-lifecycle.test.node.js +75 -0
- package/dist/services/field-upload.test.node.js +1 -0
- package/dist/services/populate.test.node.js +1 -0
- package/package.json +2 -2
|
@@ -323,6 +323,12 @@ export interface BeforeCreateContext {
|
|
|
323
323
|
* Includes the `documentId` and `documentVersionId` returned by storage
|
|
324
324
|
* so the hook can reference the persisted document.
|
|
325
325
|
*
|
|
326
|
+
* `path` is the document's canonical (source-locale) routing path as
|
|
327
|
+
* written into `byline_document_paths` — the value a consumer needs to
|
|
328
|
+
* invalidate a cache key, purge a CDN URL, or fire a webhook against the
|
|
329
|
+
* specific document. (Resolved *after* `beforeCreate`, since that hook may
|
|
330
|
+
* mutate the source field that path derivation reads.)
|
|
331
|
+
*
|
|
326
332
|
* `duplicate` mirrors `BeforeCreateContext.duplicate` — present only when
|
|
327
333
|
* the create was triggered by `duplicateDocument`.
|
|
328
334
|
*/
|
|
@@ -331,6 +337,8 @@ export interface AfterCreateContext {
|
|
|
331
337
|
collectionPath: string;
|
|
332
338
|
documentId: string;
|
|
333
339
|
documentVersionId: string;
|
|
340
|
+
/** The document's canonical (source-locale) routing path. */
|
|
341
|
+
path: string;
|
|
334
342
|
duplicate?: {
|
|
335
343
|
sourceDocumentId: string;
|
|
336
344
|
};
|
|
@@ -371,6 +379,11 @@ export interface BeforeUpdateContext {
|
|
|
371
379
|
* Includes the `documentId` and `documentVersionId` of the newly created
|
|
372
380
|
* version so the hook can reference the persisted document.
|
|
373
381
|
*
|
|
382
|
+
* `path` is the document's canonical (source-locale) routing path after the
|
|
383
|
+
* update — surfaced explicitly so cache-invalidation / CDN-purge / webhook
|
|
384
|
+
* hooks need not dig it out of `originalData`. (Previously available only
|
|
385
|
+
* implicitly via `originalData.path`.)
|
|
386
|
+
*
|
|
374
387
|
* `restore` mirrors `BeforeUpdateContext.restore` — present only when the
|
|
375
388
|
* update was triggered by restoring a historical version.
|
|
376
389
|
* `copyToLocale` mirrors `BeforeUpdateContext.copyToLocale` — present only
|
|
@@ -382,6 +395,8 @@ export interface AfterUpdateContext {
|
|
|
382
395
|
collectionPath: string;
|
|
383
396
|
documentId: string;
|
|
384
397
|
documentVersionId: string;
|
|
398
|
+
/** The document's canonical (source-locale) routing path. */
|
|
399
|
+
path: string;
|
|
385
400
|
restore?: {
|
|
386
401
|
sourceVersionId: string;
|
|
387
402
|
};
|
|
@@ -396,37 +411,60 @@ export interface AfterUpdateContext {
|
|
|
396
411
|
}
|
|
397
412
|
/**
|
|
398
413
|
* Context passed to `beforeStatusChange` / `afterStatusChange` hooks.
|
|
414
|
+
*
|
|
415
|
+
* `path` is the document's canonical (source-locale) routing path — present
|
|
416
|
+
* so a status-change hook (publish → purge CDN, archive → drop cache key) can
|
|
417
|
+
* act on the specific document/URL rather than invalidating the whole
|
|
418
|
+
* collection.
|
|
399
419
|
*/
|
|
400
420
|
export interface StatusChangeContext {
|
|
401
421
|
documentId: string;
|
|
402
422
|
documentVersionId: string;
|
|
403
423
|
collectionPath: string;
|
|
424
|
+
/** The document's canonical (source-locale) routing path. */
|
|
425
|
+
path: string;
|
|
404
426
|
previousStatus: string;
|
|
405
427
|
nextStatus: string;
|
|
406
428
|
}
|
|
407
429
|
/**
|
|
408
430
|
* Context passed to `beforeUnpublish` hooks.
|
|
431
|
+
*
|
|
432
|
+
* `path` is the document's canonical (source-locale) routing path — present
|
|
433
|
+
* so an unpublish hook can target the specific document/URL.
|
|
409
434
|
*/
|
|
410
435
|
export interface BeforeUnpublishContext {
|
|
411
436
|
documentId: string;
|
|
412
437
|
collectionPath: string;
|
|
438
|
+
/** The document's canonical (source-locale) routing path. */
|
|
439
|
+
path: string;
|
|
413
440
|
}
|
|
414
441
|
/**
|
|
415
442
|
* Context passed to `afterUnpublish` hooks.
|
|
416
443
|
*
|
|
417
444
|
* `archivedCount` indicates how many published versions were archived.
|
|
445
|
+
*
|
|
446
|
+
* `path` is the document's canonical (source-locale) routing path — present
|
|
447
|
+
* so an unpublish hook can target the specific document/URL.
|
|
418
448
|
*/
|
|
419
449
|
export interface AfterUnpublishContext {
|
|
420
450
|
documentId: string;
|
|
421
451
|
collectionPath: string;
|
|
452
|
+
/** The document's canonical (source-locale) routing path. */
|
|
453
|
+
path: string;
|
|
422
454
|
archivedCount: number;
|
|
423
455
|
}
|
|
424
456
|
/**
|
|
425
|
-
* Context passed to `beforeDelete` / `afterDelete` hooks
|
|
457
|
+
* Context passed to `beforeDelete` / `afterDelete` hooks.
|
|
458
|
+
*
|
|
459
|
+
* `path` is the document's canonical (source-locale) routing path — present
|
|
460
|
+
* so a delete hook can purge the specific document/URL (cache key, CDN, search
|
|
461
|
+
* index) rather than the entire collection.
|
|
426
462
|
*/
|
|
427
463
|
export interface DeleteContext {
|
|
428
464
|
documentId: string;
|
|
429
465
|
collectionPath: string;
|
|
466
|
+
/** The document's canonical (source-locale) routing path. */
|
|
467
|
+
path: string;
|
|
430
468
|
}
|
|
431
469
|
/**
|
|
432
470
|
* Context passed to `beforeStore` hooks (configured on
|
|
@@ -468,6 +468,21 @@ export interface IDocumentQueries {
|
|
|
468
468
|
created_at: Date;
|
|
469
469
|
updated_at: Date;
|
|
470
470
|
} | null>;
|
|
471
|
+
/**
|
|
472
|
+
* Resolve a document's canonical (source-locale) routing path.
|
|
473
|
+
*
|
|
474
|
+
* Returns the `byline_document_paths` row for the document under its own
|
|
475
|
+
* `source_locale` anchor (falling back to the configured default content
|
|
476
|
+
* locale for rows predating `source_locale`). Narrow by design — used by
|
|
477
|
+
* the lifecycle to populate `path` on the status-change / unpublish hook
|
|
478
|
+
* contexts without widening `getCurrentVersionMetadata`.
|
|
479
|
+
*
|
|
480
|
+
* Returns `null` when the document has no path row (or does not exist).
|
|
481
|
+
*/
|
|
482
|
+
getCurrentPath(params: {
|
|
483
|
+
collection_id: string;
|
|
484
|
+
document_id: string;
|
|
485
|
+
}): Promise<string | null>;
|
|
471
486
|
getDocumentByPath(params: {
|
|
472
487
|
collection_id: string;
|
|
473
488
|
path: string;
|
|
@@ -59,6 +59,7 @@ function createMockDb(options) {
|
|
|
59
59
|
documents: {
|
|
60
60
|
getDocumentById: vi.fn(fail),
|
|
61
61
|
getCurrentVersionMetadata: vi.fn(fail),
|
|
62
|
+
getCurrentPath: vi.fn(fail),
|
|
62
63
|
getDocumentByPath: vi.fn(fail),
|
|
63
64
|
getDocumentByVersion: vi.fn(fail),
|
|
64
65
|
getDocumentsByVersionIds: vi.fn(fail),
|
|
@@ -201,6 +202,7 @@ describe('ensureCollections', () => {
|
|
|
201
202
|
documents: {
|
|
202
203
|
getDocumentById: vi.fn(),
|
|
203
204
|
getCurrentVersionMetadata: vi.fn(),
|
|
205
|
+
getCurrentPath: vi.fn(),
|
|
204
206
|
getDocumentByPath: vi.fn(),
|
|
205
207
|
getDocumentByVersion: vi.fn(),
|
|
206
208
|
getDocumentsByVersionIds: vi.fn(),
|
|
@@ -44,6 +44,7 @@ function makeAdapter(options) {
|
|
|
44
44
|
documents: {
|
|
45
45
|
getDocumentById: vi.fn(fail),
|
|
46
46
|
getCurrentVersionMetadata: vi.fn(fail),
|
|
47
|
+
getCurrentPath: vi.fn(fail),
|
|
47
48
|
getDocumentByPath: vi.fn(fail),
|
|
48
49
|
getDocumentByVersion: vi.fn(fail),
|
|
49
50
|
getDocumentsByVersionIds: vi.fn(fail),
|
|
@@ -255,6 +255,7 @@ export async function createDocument(ctx, params) {
|
|
|
255
255
|
collectionPath,
|
|
256
256
|
documentId,
|
|
257
257
|
documentVersionId,
|
|
258
|
+
path: resolvedPath,
|
|
258
259
|
});
|
|
259
260
|
return { documentId, documentVersionId };
|
|
260
261
|
});
|
|
@@ -340,6 +341,7 @@ export async function updateDocument(ctx, params) {
|
|
|
340
341
|
collectionPath,
|
|
341
342
|
documentId,
|
|
342
343
|
documentVersionId,
|
|
344
|
+
path: pathForCommand ?? originalData.path,
|
|
343
345
|
});
|
|
344
346
|
return { documentId, documentVersionId };
|
|
345
347
|
});
|
|
@@ -453,6 +455,7 @@ export async function updateDocumentWithPatches(ctx, params) {
|
|
|
453
455
|
collectionPath,
|
|
454
456
|
documentId,
|
|
455
457
|
documentVersionId,
|
|
458
|
+
path: pathForCommand ?? originalData.path,
|
|
456
459
|
});
|
|
457
460
|
return { documentId, documentVersionId };
|
|
458
461
|
});
|
|
@@ -514,10 +517,18 @@ export async function changeDocumentStatus(ctx, params) {
|
|
|
514
517
|
details: { currentStatus, nextStatus: params.nextStatus },
|
|
515
518
|
}).log(ctx.logger);
|
|
516
519
|
}
|
|
520
|
+
// Resolve the document's canonical path so the hooks can act on the
|
|
521
|
+
// specific document/URL (CDN purge, cache-key drop). Narrow lookup —
|
|
522
|
+
// getCurrentVersionMetadata deliberately omits the path subquery.
|
|
523
|
+
const path = (await ctx.db.queries.documents.getCurrentPath({
|
|
524
|
+
collection_id: collectionId,
|
|
525
|
+
document_id: params.documentId,
|
|
526
|
+
})) ?? '';
|
|
517
527
|
const hookCtx = {
|
|
518
528
|
documentId: params.documentId,
|
|
519
529
|
documentVersionId,
|
|
520
530
|
collectionPath,
|
|
531
|
+
path,
|
|
521
532
|
previousStatus: currentStatus,
|
|
522
533
|
nextStatus: params.nextStatus,
|
|
523
534
|
};
|
|
@@ -550,7 +561,7 @@ export async function changeDocumentStatus(ctx, params) {
|
|
|
550
561
|
*/
|
|
551
562
|
export async function unpublishDocument(ctx, params) {
|
|
552
563
|
return withLogContext({ domain: 'services', module: 'lifecycle', function: 'unpublishDocument' }, async () => {
|
|
553
|
-
const { db, collectionPath, definition } = ctx;
|
|
564
|
+
const { db, collectionId, collectionPath, definition } = ctx;
|
|
554
565
|
// Unpublish is a workflow transition out of `published` — reuse the
|
|
555
566
|
// changeStatus gate rather than a separate ability.
|
|
556
567
|
assertActorCanPerform(ctx.requestContext, collectionPath, 'changeStatus');
|
|
@@ -563,9 +574,16 @@ export async function unpublishDocument(ctx, params) {
|
|
|
563
574
|
}).log(ctx.logger);
|
|
564
575
|
}
|
|
565
576
|
const hooks = definition.hooks;
|
|
577
|
+
// Resolve the document's canonical path so the hooks can target the
|
|
578
|
+
// specific document/URL (CDN purge, cache-key drop).
|
|
579
|
+
const path = (await db.queries.documents.getCurrentPath({
|
|
580
|
+
collection_id: collectionId,
|
|
581
|
+
document_id: params.documentId,
|
|
582
|
+
})) ?? '';
|
|
566
583
|
await invokeHook(hooks?.beforeUnpublish, {
|
|
567
584
|
documentId: params.documentId,
|
|
568
585
|
collectionPath,
|
|
586
|
+
path,
|
|
569
587
|
});
|
|
570
588
|
const archivedCount = await db.commands.documents.archivePublishedVersions({
|
|
571
589
|
document_id: params.documentId,
|
|
@@ -573,6 +591,7 @@ export async function unpublishDocument(ctx, params) {
|
|
|
573
591
|
await invokeHook(hooks?.afterUnpublish, {
|
|
574
592
|
documentId: params.documentId,
|
|
575
593
|
collectionPath,
|
|
594
|
+
path,
|
|
576
595
|
archivedCount,
|
|
577
596
|
});
|
|
578
597
|
return { archivedCount };
|
|
@@ -709,13 +728,15 @@ export async function restoreDocumentVersion(ctx, params) {
|
|
|
709
728
|
});
|
|
710
729
|
const documentId = extractDocumentId(result.document) || params.documentId;
|
|
711
730
|
const documentVersionId = extractVersionId(result.document);
|
|
712
|
-
// 7. afterUpdate.
|
|
731
|
+
// 7. afterUpdate. Restore is path-sticky: the canonical path comes
|
|
732
|
+
// from the current version's envelope (originalData), not the source.
|
|
713
733
|
await invokeHook(hooks?.afterUpdate, {
|
|
714
734
|
data: sourceFields,
|
|
715
735
|
originalData,
|
|
716
736
|
collectionPath,
|
|
717
737
|
documentId,
|
|
718
738
|
documentVersionId,
|
|
739
|
+
path: originalData.path ?? '',
|
|
719
740
|
restore: restoreContext,
|
|
720
741
|
});
|
|
721
742
|
return {
|
|
@@ -797,6 +818,11 @@ export async function deleteDocument(ctx, params) {
|
|
|
797
818
|
const hookCtx = {
|
|
798
819
|
documentId: params.documentId,
|
|
799
820
|
collectionPath,
|
|
821
|
+
// The current document was fetched above (reconstructed only for
|
|
822
|
+
// upload collections, but the envelope carries the locale-resolved
|
|
823
|
+
// `path` projection either way). Surface it so delete hooks can purge
|
|
824
|
+
// the specific document/URL.
|
|
825
|
+
path: latest.path ?? '',
|
|
800
826
|
};
|
|
801
827
|
// 2. beforeDelete hook.
|
|
802
828
|
await invokeHook(hooks?.beforeDelete, hookCtx);
|
|
@@ -1041,6 +1067,7 @@ export async function duplicateDocument(ctx, params) {
|
|
|
1041
1067
|
collectionPath,
|
|
1042
1068
|
documentId: newDocumentId,
|
|
1043
1069
|
documentVersionId: newDocumentVersionId,
|
|
1070
|
+
path: finalPath,
|
|
1044
1071
|
duplicate: duplicateMarker,
|
|
1045
1072
|
});
|
|
1046
1073
|
return {
|
|
@@ -1330,6 +1357,9 @@ export async function copyToLocale(ctx, params) {
|
|
|
1330
1357
|
collectionPath,
|
|
1331
1358
|
documentId: params.documentId,
|
|
1332
1359
|
documentVersionId,
|
|
1360
|
+
// Path is sticky and source-locale-anchored; copy-to-locale never
|
|
1361
|
+
// touches it. Read it off the target envelope.
|
|
1362
|
+
path: targetRecord.path ?? '',
|
|
1333
1363
|
copyToLocale: copyToLocaleMarker,
|
|
1334
1364
|
});
|
|
1335
1365
|
return {
|
|
@@ -1436,6 +1466,9 @@ export async function deleteLocale(ctx, params) {
|
|
|
1436
1466
|
collectionPath,
|
|
1437
1467
|
documentId: params.documentId,
|
|
1438
1468
|
documentVersionId: result.newVersionId,
|
|
1469
|
+
// Path is sticky and source-locale-anchored; deleting a translation
|
|
1470
|
+
// never touches it. Read it off the target envelope.
|
|
1471
|
+
path: targetRecord.path ?? '',
|
|
1439
1472
|
deleteLocale: deleteLocaleMarker,
|
|
1440
1473
|
});
|
|
1441
1474
|
return {
|
|
@@ -35,6 +35,7 @@ function createMockDb() {
|
|
|
35
35
|
const softDeleteDocument = vi.fn().mockResolvedValue(1);
|
|
36
36
|
const getDocumentById = vi.fn().mockResolvedValue(null);
|
|
37
37
|
const getCurrentVersionMetadata = vi.fn().mockResolvedValue(null);
|
|
38
|
+
const getCurrentPath = vi.fn().mockResolvedValue('current-path');
|
|
38
39
|
const db = {
|
|
39
40
|
commands: {
|
|
40
41
|
collections: {
|
|
@@ -64,6 +65,7 @@ function createMockDb() {
|
|
|
64
65
|
documents: {
|
|
65
66
|
getDocumentById,
|
|
66
67
|
getCurrentVersionMetadata,
|
|
68
|
+
getCurrentPath,
|
|
67
69
|
getDocumentByPath: vi.fn(),
|
|
68
70
|
getDocumentByVersion: vi.fn(),
|
|
69
71
|
getDocumentsByVersionIds: vi.fn(),
|
|
@@ -87,6 +89,7 @@ function createMockDb() {
|
|
|
87
89
|
softDeleteDocument,
|
|
88
90
|
getDocumentById,
|
|
89
91
|
getCurrentVersionMetadata,
|
|
92
|
+
getCurrentPath,
|
|
90
93
|
};
|
|
91
94
|
}
|
|
92
95
|
const noopLogger = {
|
|
@@ -165,6 +168,14 @@ describe('Document lifecycle service', () => {
|
|
|
165
168
|
documentVersionId: 'ver-1',
|
|
166
169
|
}));
|
|
167
170
|
});
|
|
171
|
+
it('afterCreate receives the resolved canonical path', async () => {
|
|
172
|
+
const afterCreate = vi.fn();
|
|
173
|
+
const { db } = createMockDb();
|
|
174
|
+
const definition = { ...minimalCollection, hooks: { afterCreate } };
|
|
175
|
+
const ctx = buildCtx(db, definition);
|
|
176
|
+
await createDocument(ctx, { data: { title: 'X' }, path: 'my-explicit-path' });
|
|
177
|
+
expect(afterCreate).toHaveBeenCalledWith(expect.objectContaining({ path: 'my-explicit-path' }));
|
|
178
|
+
});
|
|
168
179
|
it('beforeCreate can mutate data before persistence', async () => {
|
|
169
180
|
const { db, createDocumentVersion } = createMockDb();
|
|
170
181
|
const definition = {
|
|
@@ -328,6 +339,36 @@ describe('Document lifecycle service', () => {
|
|
|
328
339
|
documentVersionId: 'ver-1',
|
|
329
340
|
}));
|
|
330
341
|
});
|
|
342
|
+
it('afterUpdate carries the sticky path forward when none is supplied', async () => {
|
|
343
|
+
const afterUpdate = vi.fn();
|
|
344
|
+
const { db, getDocumentById } = createMockDb();
|
|
345
|
+
getDocumentById.mockResolvedValue({
|
|
346
|
+
document_version_id: 'prev-ver',
|
|
347
|
+
path: 'sticky-path',
|
|
348
|
+
fields: { title: 'Old' },
|
|
349
|
+
});
|
|
350
|
+
const definition = { ...minimalCollection, hooks: { afterUpdate } };
|
|
351
|
+
const ctx = buildCtx(db, definition);
|
|
352
|
+
await updateDocument(ctx, { documentId: 'doc-1', data: { title: 'New' } });
|
|
353
|
+
expect(afterUpdate).toHaveBeenCalledWith(expect.objectContaining({ path: 'sticky-path' }));
|
|
354
|
+
});
|
|
355
|
+
it('afterUpdate surfaces an explicitly-supplied path', async () => {
|
|
356
|
+
const afterUpdate = vi.fn();
|
|
357
|
+
const { db, getDocumentById } = createMockDb();
|
|
358
|
+
getDocumentById.mockResolvedValue({
|
|
359
|
+
document_version_id: 'prev-ver',
|
|
360
|
+
path: 'sticky-path',
|
|
361
|
+
fields: { title: 'Old' },
|
|
362
|
+
});
|
|
363
|
+
const definition = { ...minimalCollection, hooks: { afterUpdate } };
|
|
364
|
+
const ctx = buildCtx(db, definition);
|
|
365
|
+
await updateDocument(ctx, {
|
|
366
|
+
documentId: 'doc-1',
|
|
367
|
+
data: { title: 'New' },
|
|
368
|
+
path: 'new-path',
|
|
369
|
+
});
|
|
370
|
+
expect(afterUpdate).toHaveBeenCalledWith(expect.objectContaining({ path: 'new-path' }));
|
|
371
|
+
});
|
|
331
372
|
it('does not pass path to the storage primitive when no explicit path is supplied', async () => {
|
|
332
373
|
const { db, getDocumentById, createDocumentVersion } = createMockDb();
|
|
333
374
|
getDocumentById.mockResolvedValue({
|
|
@@ -641,7 +682,9 @@ describe('Document lifecycle service', () => {
|
|
|
641
682
|
nextStatus: 'published',
|
|
642
683
|
documentId: 'doc-1',
|
|
643
684
|
documentVersionId: 'ver-1',
|
|
685
|
+
path: 'current-path',
|
|
644
686
|
}));
|
|
687
|
+
expect(hooks.afterStatusChange).toHaveBeenCalledWith(expect.objectContaining({ path: 'current-path' }));
|
|
645
688
|
});
|
|
646
689
|
it('does not invoke hooks when transition is invalid', async () => {
|
|
647
690
|
const hooks = {
|
|
@@ -728,9 +771,11 @@ describe('Document lifecycle service', () => {
|
|
|
728
771
|
const ctx = buildCtx(db, definition);
|
|
729
772
|
await unpublishDocument(ctx, { documentId: 'doc-1' });
|
|
730
773
|
expect(callOrder).toEqual(['before', 'archive', 'after']);
|
|
774
|
+
expect(hooks.beforeUnpublish).toHaveBeenCalledWith(expect.objectContaining({ documentId: 'doc-1', path: 'current-path' }));
|
|
731
775
|
expect(hooks.afterUnpublish).toHaveBeenCalledWith(expect.objectContaining({
|
|
732
776
|
documentId: 'doc-1',
|
|
733
777
|
archivedCount: 2,
|
|
778
|
+
path: 'current-path',
|
|
734
779
|
}));
|
|
735
780
|
});
|
|
736
781
|
it('works when no hooks are defined', async () => {
|
|
@@ -772,6 +817,27 @@ describe('Document lifecycle service', () => {
|
|
|
772
817
|
});
|
|
773
818
|
});
|
|
774
819
|
// -----------------------------------------------------------------------
|
|
820
|
+
// deleteDocument
|
|
821
|
+
// -----------------------------------------------------------------------
|
|
822
|
+
describe('deleteDocument', () => {
|
|
823
|
+
it('invokes beforeDelete / afterDelete with the document path', async () => {
|
|
824
|
+
const beforeDelete = vi.fn();
|
|
825
|
+
const afterDelete = vi.fn();
|
|
826
|
+
const { db, getDocumentById } = createMockDb();
|
|
827
|
+
getDocumentById.mockResolvedValue({
|
|
828
|
+
document_version_id: 'ver-1',
|
|
829
|
+
document_id: 'doc-1',
|
|
830
|
+
path: 'doc-to-delete',
|
|
831
|
+
fields: {},
|
|
832
|
+
});
|
|
833
|
+
const definition = { ...minimalCollection, hooks: { beforeDelete, afterDelete } };
|
|
834
|
+
const ctx = buildCtx(db, definition);
|
|
835
|
+
await deleteDocument(ctx, { documentId: 'doc-1' });
|
|
836
|
+
expect(beforeDelete).toHaveBeenCalledWith(expect.objectContaining({ documentId: 'doc-1', path: 'doc-to-delete' }));
|
|
837
|
+
expect(afterDelete).toHaveBeenCalledWith(expect.objectContaining({ documentId: 'doc-1', path: 'doc-to-delete' }));
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
// -----------------------------------------------------------------------
|
|
775
841
|
// restoreDocumentVersion
|
|
776
842
|
// -----------------------------------------------------------------------
|
|
777
843
|
describe('restoreDocumentVersion', () => {
|
|
@@ -916,6 +982,8 @@ describe('Document lifecycle service', () => {
|
|
|
916
982
|
expect(afterUpdate).toHaveBeenCalledWith(expect.objectContaining({
|
|
917
983
|
documentId: 'doc-1',
|
|
918
984
|
documentVersionId: 'ver-restored',
|
|
985
|
+
// Sticky path comes from the current version's envelope, not the source.
|
|
986
|
+
path: 'sticky-path',
|
|
919
987
|
restore: { sourceVersionId },
|
|
920
988
|
}));
|
|
921
989
|
});
|
|
@@ -1195,6 +1263,11 @@ describe('Document lifecycle service', () => {
|
|
|
1195
1263
|
expect(afterCtx.duplicate).toEqual({ sourceDocumentId: 'doc-source' });
|
|
1196
1264
|
expect(afterCtx.documentId).toBe('doc-new');
|
|
1197
1265
|
expect(afterCtx.documentVersionId).toBe('ver-new');
|
|
1266
|
+
// afterCreate carries the final path written for the duplicate — the
|
|
1267
|
+
// same value handed to createDocumentVersion.
|
|
1268
|
+
const writtenPath = mocks.createDocumentVersion.mock.calls[0]?.[0]?.path;
|
|
1269
|
+
expect(typeof afterCtx.path).toBe('string');
|
|
1270
|
+
expect(afterCtx.path).toBe(writtenPath);
|
|
1198
1271
|
});
|
|
1199
1272
|
it('enforces collections.<path>.create — rejects an admin actor missing the ability', async () => {
|
|
1200
1273
|
const mocks = createMockDb();
|
|
@@ -1484,6 +1557,8 @@ describe('Document lifecycle service', () => {
|
|
|
1484
1557
|
sourceLocale: 'en',
|
|
1485
1558
|
targetLocale: 'fr',
|
|
1486
1559
|
});
|
|
1560
|
+
// Sticky path read off the target-locale envelope.
|
|
1561
|
+
expect(afterUpdate.mock.calls[0]?.[0].path).toBe('hello');
|
|
1487
1562
|
});
|
|
1488
1563
|
it('enforces collections.<path>.update — rejects an admin actor missing the ability', async () => {
|
|
1489
1564
|
const { mocks } = setupSourceTarget();
|
|
@@ -161,6 +161,7 @@ function makeMockAdapter(store = {}, pathByCollectionId = {}) {
|
|
|
161
161
|
documents: {
|
|
162
162
|
getDocumentById: vi.fn(),
|
|
163
163
|
getCurrentVersionMetadata: vi.fn(),
|
|
164
|
+
getCurrentPath: vi.fn(),
|
|
164
165
|
getDocumentByPath: vi.fn(),
|
|
165
166
|
getDocumentByVersion: vi.fn(),
|
|
166
167
|
getDocumentsByVersionIds: vi.fn(),
|
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.0
|
|
5
|
+
"version": "3.1.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.0
|
|
82
|
+
"@byline/auth": "3.1.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@biomejs/biome": "2.4.15",
|