@byline/core 4.18.0 → 5.0.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 +38 -4
- package/dist/@types/document-revision.d.ts +92 -0
- package/dist/@types/document-revision.js +8 -0
- package/dist/@types/index.d.ts +1 -0
- package/dist/@types/index.js +1 -0
- package/dist/config/attach-hooks.test.node.js +2 -1
- package/dist/config/config-hooks.test.node.js +2 -1
- package/dist/core.js +8 -0
- package/dist/core.test.node.js +27 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lib/errors.d.ts +25 -0
- package/dist/lib/errors.js +42 -0
- package/dist/lib/lock-conflict.test.node.d.ts +1 -0
- package/dist/lib/lock-conflict.test.node.js +31 -0
- package/dist/scheduler/run-due-tasks.test.node.d.ts +0 -7
- package/dist/scheduler/run-due-tasks.test.node.js +3 -2
- package/dist/scheduler/scheduled-publication.d.ts +16 -1
- package/dist/scheduler/scheduled-publication.js +27 -1
- package/dist/scheduler/scheduled-publication.test.node.d.ts +0 -7
- package/dist/scheduler/scheduled-publication.test.node.js +18 -3
- package/dist/scheduler/scheduler-boot.test.node.d.ts +0 -7
- package/dist/scheduler/scheduler-boot.test.node.js +40 -12
- package/dist/scheduler/ticker.test.node.d.ts +0 -7
- package/dist/scheduler/ticker.test.node.js +3 -2
- package/dist/services/collection-bootstrap.test.node.d.ts +0 -7
- package/dist/services/collection-bootstrap.test.node.js +12 -3
- package/dist/services/discover-counter-groups.test.node.d.ts +0 -7
- package/dist/services/discover-counter-groups.test.node.js +19 -3
- package/dist/services/document-lifecycle/audit.d.ts +1 -0
- package/dist/services/document-lifecycle/audit.js +1 -0
- package/dist/services/document-lifecycle/committed-hook.d.ts +25 -0
- package/dist/services/document-lifecycle/committed-hook.js +96 -0
- package/dist/services/document-lifecycle/copy-to-locale.d.ts +2 -0
- package/dist/services/document-lifecycle/copy-to-locale.js +30 -44
- package/dist/services/document-lifecycle/create.d.ts +1 -0
- package/dist/services/document-lifecycle/create.js +6 -21
- package/dist/services/document-lifecycle/delete-locale.d.ts +4 -2
- package/dist/services/document-lifecycle/delete-locale.js +42 -39
- package/dist/services/document-lifecycle/delete.d.ts +8 -2
- package/dist/services/document-lifecycle/delete.js +24 -15
- package/dist/services/document-lifecycle/duplicate.d.ts +3 -0
- package/dist/services/document-lifecycle/duplicate.js +55 -43
- package/dist/services/document-lifecycle/index.d.ts +5 -1
- package/dist/services/document-lifecycle/index.js +4 -1
- package/dist/services/document-lifecycle/persistence.js +18 -7
- package/dist/services/document-lifecycle/publish-schedule-consistency.d.ts +2 -0
- package/dist/services/document-lifecycle/publish-schedule-consistency.js +28 -24
- package/dist/services/document-lifecycle/reorder.d.ts +11 -0
- package/dist/services/document-lifecycle/reorder.js +83 -0
- package/dist/services/document-lifecycle/restore.d.ts +2 -0
- package/dist/services/document-lifecycle/restore.js +21 -26
- package/dist/services/document-lifecycle/rethrow-path-conflict.test.node.d.ts +0 -7
- package/dist/services/document-lifecycle/rethrow-path-conflict.test.node.js +2 -1
- package/dist/services/document-lifecycle/revision-guard.d.ts +34 -0
- package/dist/services/document-lifecycle/revision-guard.js +154 -0
- package/dist/services/document-lifecycle/same-document-data.d.ts +2 -0
- package/dist/services/document-lifecycle/same-document-data.js +17 -0
- package/dist/services/document-lifecycle/scheduled-publish.d.ts +20 -4
- package/dist/services/document-lifecycle/scheduled-publish.js +146 -84
- package/dist/services/document-lifecycle/stale-document.d.ts +13 -0
- package/dist/services/document-lifecycle/stale-document.js +95 -0
- package/dist/services/document-lifecycle/stale-document.test.node.d.ts +1 -0
- package/dist/services/document-lifecycle/stale-document.test.node.js +121 -0
- package/dist/services/document-lifecycle/status-transition.test.node.d.ts +0 -7
- package/dist/services/document-lifecycle/status-transition.test.node.js +3 -2
- package/dist/services/document-lifecycle/status.d.ts +5 -21
- package/dist/services/document-lifecycle/status.js +90 -133
- package/dist/services/document-lifecycle/system-fields.d.ts +21 -0
- package/dist/services/document-lifecycle/system-fields.js +118 -116
- package/dist/services/document-lifecycle/tree.d.ts +9 -6
- package/dist/services/document-lifecycle/tree.js +65 -39
- package/dist/services/document-lifecycle/tree.test.node.d.ts +0 -7
- package/dist/services/document-lifecycle/tree.test.node.js +152 -35
- package/dist/services/document-lifecycle/update.d.ts +14 -6
- package/dist/services/document-lifecycle/update.js +104 -82
- package/dist/services/document-lifecycle.test.node.js +556 -70
- package/dist/services/field-upload.d.ts +1 -0
- package/dist/services/field-upload.js +5 -1
- package/dist/services/field-upload.test.node.d.ts +0 -7
- package/dist/services/field-upload.test.node.js +38 -2
- package/dist/services/populate.test.node.d.ts +0 -7
- package/dist/services/populate.test.node.js +19 -3
- package/dist/services/richtext-populate.test.node.d.ts +0 -7
- package/dist/services/richtext-populate.test.node.js +3 -2
- package/dist/services/singleton-lifecycle/copy-to-locale.d.ts +1 -0
- package/dist/services/singleton-lifecycle/copy-to-locale.js +9 -2
- package/dist/services/singleton-lifecycle/index.d.ts +1 -1
- package/dist/services/singleton-lifecycle/internals.d.ts +4 -6
- package/dist/services/singleton-lifecycle/internals.js +75 -73
- package/dist/services/singleton-lifecycle/restore.d.ts +1 -0
- package/dist/services/singleton-lifecycle/restore.js +3 -1
- package/dist/services/singleton-lifecycle/update.d.ts +6 -5
- package/dist/services/singleton-lifecycle/update.js +158 -91
- package/dist/services/singleton-lifecycle.test.node.d.ts +0 -7
- package/dist/services/singleton-lifecycle.test.node.js +75 -23
- package/dist/storage/db-adapter.test-helper.d.ts +26 -0
- package/dist/storage/db-adapter.test-helper.js +117 -0
- package/dist/storage/document-revision-capability.d.ts +3 -0
- package/dist/storage/document-revision-capability.js +15 -0
- package/dist/storage/document-revision-capability.test.node.d.ts +1 -0
- package/dist/storage/document-revision-capability.test.node.js +53 -0
- package/dist/storage/document-revision.d.ts +12 -0
- package/dist/storage/document-revision.js +37 -0
- package/dist/storage/document-revision.test.node.d.ts +1 -0
- package/dist/storage/document-revision.test.node.js +48 -0
- package/dist/storage/document-version-parent.d.ts +20 -0
- package/dist/storage/document-version-parent.js +34 -0
- package/dist/storage/document-version-parent.test.node.d.ts +8 -0
- package/dist/storage/document-version-parent.test.node.js +60 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/read-snapshot.d.ts +3 -0
- package/dist/storage/read-snapshot.js +55 -0
- package/dist/storage/revision-store.test-helper.d.ts +3 -0
- package/dist/storage/revision-store.test-helper.js +16 -0
- package/package.json +2 -2
|
@@ -2,6 +2,7 @@ import type { RequestContext } from '@byline/auth';
|
|
|
2
2
|
import type { CollectionDefinition } from '@byline/core';
|
|
3
3
|
import type { DbErrorClassification } from '../lib/errors.js';
|
|
4
4
|
import type { ISchedulerStore } from '../scheduler/types.js';
|
|
5
|
+
import type { DocumentRevisionStore } from './document-revision.js';
|
|
5
6
|
/**
|
|
6
7
|
* Read mode for document queries.
|
|
7
8
|
*
|
|
@@ -202,7 +203,18 @@ export interface FieldSort {
|
|
|
202
203
|
/** Sort direction. */
|
|
203
204
|
direction: 'asc' | 'desc';
|
|
204
205
|
}
|
|
206
|
+
/** Query-only executor facade, valid only during its snapshot callback. */
|
|
207
|
+
export interface ReadSnapshotQueries {
|
|
208
|
+
collections: ICollectionQueries;
|
|
209
|
+
documents: Omit<IDocumentQueries, 'getDocumentSystemFieldsForUpdate'>;
|
|
210
|
+
audit: IAuditQueries;
|
|
211
|
+
singletons: ISingletonQueries;
|
|
212
|
+
}
|
|
205
213
|
export interface IDbAdapter {
|
|
214
|
+
/** Dedicated read-only repeatable-read transaction; never joins ambient writes. */
|
|
215
|
+
withReadSnapshot<T>(fn: (queries: ReadSnapshotQueries) => Promise<T>): Promise<T>;
|
|
216
|
+
/** Required revision schema and transaction-scoped guard capability. */
|
|
217
|
+
revisions: DocumentRevisionStore;
|
|
206
218
|
commands: {
|
|
207
219
|
collections: ICollectionCommands;
|
|
208
220
|
documents: IDocumentCommands;
|
|
@@ -444,6 +456,11 @@ export interface ICounterCommands {
|
|
|
444
456
|
nextScopedCounterValue(scopeName: string): Promise<number>;
|
|
445
457
|
}
|
|
446
458
|
export interface ICollectionCommands {
|
|
459
|
+
/** Acquire before document locks, inside an owned transaction. Shared locks
|
|
460
|
+
* admit independent writers; exclusive locks coordinate deletion/maintenance.
|
|
461
|
+
* Select the mode before locking documents; never upgrade a shared lock.
|
|
462
|
+
*/
|
|
463
|
+
lockCollectionRegistration(collectionId: string, mode: 'shared' | 'exclusive'): Promise<void>;
|
|
447
464
|
/**
|
|
448
465
|
* Insert a new collection row. `opts.version` and `opts.schemaHash` are
|
|
449
466
|
* used by the startup bootstrap to anchor initial values; omitted by
|
|
@@ -541,7 +558,7 @@ export interface TreeDeleteMutationResult {
|
|
|
541
558
|
/** Durable state of one document-grain scheduled-publication intent. */
|
|
542
559
|
export type DocumentPublishScheduleState = 'armed' | 'needs_reconfirm';
|
|
543
560
|
/** Why an armed schedule stopped being executable. */
|
|
544
|
-
type DocumentPublishScheduleSuspendedReason = 'content_edited';
|
|
561
|
+
type DocumentPublishScheduleSuspendedReason = 'content_edited' | 'document_metadata_changed' | 'upgrade_invalidated';
|
|
545
562
|
/**
|
|
546
563
|
* Adapter-neutral representation of `byline_document_publish_schedules`.
|
|
547
564
|
*
|
|
@@ -550,6 +567,8 @@ type DocumentPublishScheduleSuspendedReason = 'content_edited';
|
|
|
550
567
|
* know whether the backing engine uses `timestamptz` or `datetime(6)`.
|
|
551
568
|
*/
|
|
552
569
|
export interface DocumentPublishSchedule {
|
|
570
|
+
/** Revision authorized by the last arm/reconfirm; null for legacy suspended rows. */
|
|
571
|
+
authorizedRevision: number | null;
|
|
553
572
|
documentId: string;
|
|
554
573
|
collectionId: string;
|
|
555
574
|
targetVersionId: string;
|
|
@@ -613,6 +632,8 @@ export interface DocumentPublishSchedulePage {
|
|
|
613
632
|
* operations: core owns abilities, workflow validation, hooks, and audit.
|
|
614
633
|
*/
|
|
615
634
|
export interface IDocumentPublishScheduleCommands {
|
|
635
|
+
/** Lock existing schedules in stable identity order after every affected document lock. */
|
|
636
|
+
lockDocuments(documentIds: readonly string[]): Promise<void>;
|
|
616
637
|
/**
|
|
617
638
|
* Create or reschedule one document's intent.
|
|
618
639
|
*
|
|
@@ -635,6 +656,7 @@ export interface IDocumentPublishScheduleCommands {
|
|
|
635
656
|
documentId: string;
|
|
636
657
|
collectionId: string;
|
|
637
658
|
expectedVersionId: string;
|
|
659
|
+
authorizedRevision: number;
|
|
638
660
|
publishAt: Date;
|
|
639
661
|
actorId: string | null;
|
|
640
662
|
}): Promise<ScheduleDocumentPublishResult>;
|
|
@@ -649,6 +671,7 @@ export interface IDocumentPublishScheduleCommands {
|
|
|
649
671
|
documentId: string;
|
|
650
672
|
collectionId: string;
|
|
651
673
|
expectedVersionId: string;
|
|
674
|
+
authorizedRevision: number;
|
|
652
675
|
actorId: string | null;
|
|
653
676
|
}): Promise<ConfirmDocumentPublishScheduleResult>;
|
|
654
677
|
/**
|
|
@@ -670,6 +693,7 @@ export interface IDocumentPublishScheduleCommands {
|
|
|
670
693
|
suspendForContentEdit(params: {
|
|
671
694
|
documentId: string;
|
|
672
695
|
collectionId: string;
|
|
696
|
+
reason?: 'content_edited' | 'document_metadata_changed';
|
|
673
697
|
}): Promise<SuspendDocumentPublishScheduleResult>;
|
|
674
698
|
/**
|
|
675
699
|
* Atomically claim up to `batchSize` due rows, oldest `publishAt` first.
|
|
@@ -709,6 +733,7 @@ export interface IDocumentPublishScheduleCommands {
|
|
|
709
733
|
* stale token without mutating the newer claimant's row.
|
|
710
734
|
*/
|
|
711
735
|
suspendClaimForContentEdit(params: {
|
|
736
|
+
reason?: 'content_edited' | 'document_metadata_changed';
|
|
712
737
|
documentId: string;
|
|
713
738
|
executionToken: string;
|
|
714
739
|
}): Promise<boolean>;
|
|
@@ -786,9 +811,12 @@ export interface IDocumentCommands {
|
|
|
786
811
|
createdBy?: string;
|
|
787
812
|
/**
|
|
788
813
|
* When updating an existing document, the version ID of the version being
|
|
789
|
-
* replaced.
|
|
790
|
-
*
|
|
791
|
-
*
|
|
814
|
+
* replaced. The adapter verifies this ID against the current live version
|
|
815
|
+
* while holding the document-row lock and throws `ERR_CONFLICT` when it is
|
|
816
|
+
* stale. A locale-specific write to an existing versioned document must
|
|
817
|
+
* provide this value so field rows for every other locale can be copied
|
|
818
|
+
* forward safely. `locale: 'all'` writes replace the complete locale tree
|
|
819
|
+
* and may omit it; when supplied, it must still be current.
|
|
792
820
|
*/
|
|
793
821
|
previousVersionId?: string;
|
|
794
822
|
/**
|
|
@@ -877,6 +905,7 @@ export interface IDocumentCommands {
|
|
|
877
905
|
*/
|
|
878
906
|
restoreSoftDeletedDocument(params: {
|
|
879
907
|
document_id: string;
|
|
908
|
+
expectedRevision: number;
|
|
880
909
|
}): Promise<number>;
|
|
881
910
|
/**
|
|
882
911
|
* Remove one content locale's data from a document by writing a new
|
|
@@ -975,6 +1004,11 @@ export interface ICollectionQueries {
|
|
|
975
1004
|
getCollectionById(id: string): Promise<any>;
|
|
976
1005
|
}
|
|
977
1006
|
export interface IDocumentQueries {
|
|
1007
|
+
/** Internal observation; expose only alongside source data from the same snapshot. */
|
|
1008
|
+
getDocumentRevision(params: {
|
|
1009
|
+
collection_id: string;
|
|
1010
|
+
document_id: string;
|
|
1011
|
+
}): Promise<number | null>;
|
|
978
1012
|
/** Scheduled-publication reads; callers resolve collection abilities above storage. */
|
|
979
1013
|
publishSchedules: IDocumentPublishScheduleQueries;
|
|
980
1014
|
/**
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
/** The revision observed before preparing an existing-document mutation. */
|
|
9
|
+
export interface DocumentWritePrecondition {
|
|
10
|
+
expectedRevision: number;
|
|
11
|
+
}
|
|
12
|
+
/** An empty singleton slot must be asserted explicitly, never by omission. */
|
|
13
|
+
export type SingletonSavePrecondition = {
|
|
14
|
+
expectedState: 'empty';
|
|
15
|
+
expectedRevision?: never;
|
|
16
|
+
} | {
|
|
17
|
+
expectedRevision: number;
|
|
18
|
+
expectedState?: never;
|
|
19
|
+
};
|
|
20
|
+
/** Returned only for the document state committed by a mutation. */
|
|
21
|
+
export interface DocumentRevisionReceipt {
|
|
22
|
+
documentId: string;
|
|
23
|
+
revision: number;
|
|
24
|
+
}
|
|
25
|
+
/** Preserve operation-specific results, including committed side-effect failures. */
|
|
26
|
+
export type DocumentMutationResult<T extends object> = T & DocumentRevisionReceipt;
|
|
27
|
+
export interface StructuralMutationReceipt extends DocumentRevisionReceipt {
|
|
28
|
+
/** Hosts must authorize/filter these identities before returning them to clients. */
|
|
29
|
+
affectedDocuments: DocumentRevisionReceipt[];
|
|
30
|
+
scheduledPublicationsNeedReconfirmation: boolean;
|
|
31
|
+
}
|
|
32
|
+
export type DocumentStaleDetails = {
|
|
33
|
+
reason: 'revision_mismatch';
|
|
34
|
+
documentId: string;
|
|
35
|
+
expectedRevision: number;
|
|
36
|
+
currentRevision: number;
|
|
37
|
+
} | {
|
|
38
|
+
reason: 'version_parent_mismatch';
|
|
39
|
+
documentId: string;
|
|
40
|
+
previousVersionId: string;
|
|
41
|
+
currentVersionId: string | null;
|
|
42
|
+
} | {
|
|
43
|
+
reason: 'singleton_slot_changed';
|
|
44
|
+
singletonPath: string;
|
|
45
|
+
expectedState: 'empty';
|
|
46
|
+
currentState: 'document';
|
|
47
|
+
};
|
|
48
|
+
export type DocumentRevisionValidationDetails = {
|
|
49
|
+
reason: 'missing_document_revision';
|
|
50
|
+
} | {
|
|
51
|
+
reason: 'invalid_document_revision';
|
|
52
|
+
};
|
|
53
|
+
/** Adapter-internal inputs. Lifecycle services authorize these targets before locking. */
|
|
54
|
+
export interface DocumentRevisionTarget extends DocumentWritePrecondition {
|
|
55
|
+
documentId: string;
|
|
56
|
+
collectionId: string;
|
|
57
|
+
previousVersionId?: string;
|
|
58
|
+
locale?: string;
|
|
59
|
+
}
|
|
60
|
+
/** An adapter-issued observation valid only inside its originating transaction. */
|
|
61
|
+
export interface LockedDocumentRevision extends DocumentRevisionReceipt {
|
|
62
|
+
readonly collectionId: string;
|
|
63
|
+
readonly currentVersionId: string | null;
|
|
64
|
+
readonly status: string | null;
|
|
65
|
+
readonly sourceLocale: string;
|
|
66
|
+
readonly path: string | null;
|
|
67
|
+
readonly availableLocales: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
/** Transaction-scoped structural state; used under an exclusive collection lock. */
|
|
70
|
+
export interface DocumentStructuralState {
|
|
71
|
+
documentId: string;
|
|
72
|
+
revision: number;
|
|
73
|
+
orderKey: string | null;
|
|
74
|
+
placed: boolean;
|
|
75
|
+
parentDocumentId: string | null;
|
|
76
|
+
treeOrderKey: string | null;
|
|
77
|
+
live: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** Storage primitives; public SDK mutations compose them inside the service boundary. */
|
|
80
|
+
export interface DocumentRevisionStore {
|
|
81
|
+
readStructure(params: {
|
|
82
|
+
collectionId: string;
|
|
83
|
+
documentIds?: readonly string[];
|
|
84
|
+
parentDocumentId?: string;
|
|
85
|
+
}): Promise<DocumentStructuralState[]>;
|
|
86
|
+
assertCompatibleSchema(): Promise<void>;
|
|
87
|
+
isInTransaction(): boolean;
|
|
88
|
+
/** Lock in stable document identity order, then validate every observation before returning. */
|
|
89
|
+
lock(targets: readonly DocumentRevisionTarget[]): Promise<readonly LockedDocumentRevision[]>;
|
|
90
|
+
/** Advance an authentic locked observation exactly once; participates in rollback/savepoints. */
|
|
91
|
+
advance(locked: LockedDocumentRevision): Promise<DocumentRevisionReceipt>;
|
|
92
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/dist/@types/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
export * from './admin-types.js';
|
|
9
9
|
export * from './collection-types.js';
|
|
10
10
|
export * from './db-types.js';
|
|
11
|
+
export * from './document-revision.js';
|
|
11
12
|
export * from './field-data-types.js';
|
|
12
13
|
export * from './field-types.js';
|
|
13
14
|
export * from './populate-types.js';
|
package/dist/@types/index.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
export * from './admin-types.js';
|
|
9
9
|
export * from './collection-types.js';
|
|
10
10
|
export * from './db-types.js';
|
|
11
|
+
export * from './document-revision.js';
|
|
11
12
|
export * from './field-data-types.js';
|
|
12
13
|
export * from './field-types.js';
|
|
13
14
|
export * from './populate-types.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { resolveHooks, resolveUploadHooks } from '../@types/collection-types.js';
|
|
3
3
|
import { initBylineCore } from '../core.js';
|
|
4
|
+
import { testAdapter } from '../storage/db-adapter.test-helper.js';
|
|
4
5
|
import { commitHookAttachment, prepareHookAttachment } from './attach-hooks.js';
|
|
5
6
|
function collection(path = 'documents') {
|
|
6
7
|
return {
|
|
@@ -197,7 +198,7 @@ describe('server hook attachment', () => {
|
|
|
197
198
|
}));
|
|
198
199
|
const invalid = {
|
|
199
200
|
collections: [definition],
|
|
200
|
-
db: {},
|
|
201
|
+
db: testAdapter({}),
|
|
201
202
|
hooks: { collections: { documents: replacement } },
|
|
202
203
|
i18n: {
|
|
203
204
|
admin: { defaultLocale: 'en', locales: ['en'] },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
2
|
import { defineAdmin, defineSingletonAdmin } from '../@types/admin-types.js';
|
|
3
3
|
import { defineCollection, defineSingleton } from '../@types/collection-types.js';
|
|
4
|
+
import { testAdapter } from '../storage/db-adapter.test-helper.js';
|
|
4
5
|
import { defineAdminConfig, defineServerConfig, getCollectionAdminConfig, getCollectionDefinition, getSingletonAdminConfig, } from './config.js';
|
|
5
6
|
const SERVER_CONFIG = Symbol.for('__byline_server_config__');
|
|
6
7
|
const ADMIN_CONFIG = Symbol.for('__byline_admin_config__');
|
|
@@ -53,7 +54,7 @@ describe('collection definition config preference', () => {
|
|
|
53
54
|
defineAdminConfig({ collections: [client], i18n });
|
|
54
55
|
defineServerConfig({
|
|
55
56
|
collections: [server],
|
|
56
|
-
db: {},
|
|
57
|
+
db: testAdapter({}),
|
|
57
58
|
hooks: { collections: { docs: hooks } },
|
|
58
59
|
i18n,
|
|
59
60
|
});
|
package/dist/core.js
CHANGED
|
@@ -18,6 +18,7 @@ import { validateTreeAuditCapability } from './services/document-lifecycle/audit
|
|
|
18
18
|
import { validateTranslations } from './services/i18n-validator.js';
|
|
19
19
|
import { validateRichTextFieldFlags } from './services/richtext-populate.js';
|
|
20
20
|
import { validateSearchConfig } from './services/validate-search-config.js';
|
|
21
|
+
import { assertDocumentRevisionCapability } from './storage/document-revision-capability.js';
|
|
21
22
|
/**
|
|
22
23
|
* Initialize Byline CMS core services via the typed registry.
|
|
23
24
|
*
|
|
@@ -118,6 +119,7 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
118
119
|
// Reconcile collection definitions with the database: insert new rows,
|
|
119
120
|
// bump schema versions when the fingerprint has drifted, and build the
|
|
120
121
|
// in-memory record cache used by the lifecycle/upload/client paths.
|
|
122
|
+
await assertDocumentRevisionCapability(composed.db);
|
|
121
123
|
const collectionRecords = await ensureCollections({
|
|
122
124
|
definitions: composed.collections,
|
|
123
125
|
db: composed.db,
|
|
@@ -161,6 +163,12 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
161
163
|
// abilities. Plugins and subsystems add their own via
|
|
162
164
|
// `core.registerAbility()` or `core.abilities.register()`.
|
|
163
165
|
const abilities = new AbilityRegistry();
|
|
166
|
+
abilities.register({
|
|
167
|
+
key: 'system.documentMaintenance',
|
|
168
|
+
label: 'Maintain document fields while preserving status',
|
|
169
|
+
group: 'system',
|
|
170
|
+
source: 'core',
|
|
171
|
+
});
|
|
164
172
|
for (const definition of composed.collections) {
|
|
165
173
|
registerDocumentAbilities(abilities, definition);
|
|
166
174
|
}
|
package/dist/core.test.node.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { defineServerConfig, getServerConfig } from './config/config.js';
|
|
3
3
|
import { initBylineCore } from './core.js';
|
|
4
|
+
import { testAdapter } from './storage/db-adapter.test-helper.js';
|
|
4
5
|
function serverConfig(admin) {
|
|
5
6
|
return {
|
|
6
7
|
routes: { admin },
|
|
7
8
|
collections: [],
|
|
8
|
-
db: {},
|
|
9
|
+
db: testAdapter({}),
|
|
9
10
|
i18n: {
|
|
10
11
|
admin: { defaultLocale: 'en', locales: [] },
|
|
11
12
|
content: { defaultLocale: 'en', locales: [] },
|
|
@@ -25,3 +26,27 @@ describe('initBylineCore configuration registration', () => {
|
|
|
25
26
|
expect(getServerConfig().routes.admin).toBe('/stable/admin');
|
|
26
27
|
});
|
|
27
28
|
});
|
|
29
|
+
it('rejects revision-incompatible storage before any boot database writes', async () => {
|
|
30
|
+
const config = serverConfig('/revision-check/admin');
|
|
31
|
+
const write = vi.fn();
|
|
32
|
+
const schemaError = new Error('fence and upgrade the revision schema');
|
|
33
|
+
config.db = testAdapter({
|
|
34
|
+
withTransaction: vi.fn(),
|
|
35
|
+
withReadSnapshot: vi.fn(),
|
|
36
|
+
revisions: {
|
|
37
|
+
assertCompatibleSchema: vi.fn().mockRejectedValue(schemaError),
|
|
38
|
+
isInTransaction: () => false,
|
|
39
|
+
lock: vi.fn(),
|
|
40
|
+
readStructure: vi.fn(),
|
|
41
|
+
advance: vi.fn(),
|
|
42
|
+
},
|
|
43
|
+
commands: {
|
|
44
|
+
collections: { create: write, update: write, lockCollectionRegistration: write },
|
|
45
|
+
documents: { publishSchedules: { lockDocuments: write } },
|
|
46
|
+
counters: { ensureCounterGroup: write },
|
|
47
|
+
},
|
|
48
|
+
backfillSourceLocales: write,
|
|
49
|
+
});
|
|
50
|
+
await expect(initBylineCore(config, {})).rejects.toBe(schemaError);
|
|
51
|
+
expect(write).not.toHaveBeenCalled();
|
|
52
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
|
|
|
8
8
|
export { type BylineCore, getBylineCore, initBylineCore } from './core.js';
|
|
9
9
|
export * from './defaults/default-values.js';
|
|
10
10
|
export { getHostRequestBridge, type HostCookieSetOptions, type HostRequestBridge, registerHostRequestBridge, tryGetHostRequestBridge, } from './host/host-request-bridge.js';
|
|
11
|
-
export { BylineError, type DbErrorClassification, type DbErrorCode, DbErrorCodes, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, type ErrorReport, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
|
|
11
|
+
export { BylineError, type DbErrorClassification, type DbErrorCode, DbErrorCodes, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_DOCUMENT_HOOK_COMMITTED, ERR_DOCUMENT_STALE, ERR_LOCK_CONFLICT, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, type ErrorReport, getLockConflictDetails, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
|
|
12
12
|
export { generateKeyBetween, generateNKeysBetween, validateOrderKey, } from './lib/fractional-index.js';
|
|
13
13
|
export { type BylineLogger, getLogger } from './lib/logger.js';
|
|
14
14
|
export { AsyncRegistry, type RegisteredServices, Registry } from './lib/registry.js';
|
|
@@ -18,7 +18,9 @@ export { mergePredicates, type ParseContext, type ParsedSort, type ParsedWhere,
|
|
|
18
18
|
export { defineRecurringTask, MIN_INTERVAL_MS, MIN_LEASE_MS, } from './scheduler/define-recurring-task.js';
|
|
19
19
|
export { getCollectionSchemasForPath } from './schemas/zod/cache.js';
|
|
20
20
|
export * from './services/index.js';
|
|
21
|
+
export { documentRevisionFromDatabase, isDocumentRevision, parseDocumentRevision, } from './storage/document-revision.js';
|
|
21
22
|
export * from './storage/index.js';
|
|
23
|
+
export { runReadSnapshot } from './storage/read-snapshot.js';
|
|
22
24
|
export { normalizeRootRelativeRedirect } from './utils/root-relative-redirect.js';
|
|
23
25
|
export { formatTextValue, looksLikeISODate, type SlugifierFn, type SlugifyContext, slugify, } from './utils/slugify.js';
|
|
24
26
|
export { type FilenameSlugifierFn, type FilenameSlugifyContext, resolveUploadFilename, slugifyFilename, } from './utils/slugify-filename.js';
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
|
|
|
20
20
|
export { getBylineCore, initBylineCore } from './core.js';
|
|
21
21
|
export * from './defaults/default-values.js';
|
|
22
22
|
export { getHostRequestBridge, registerHostRequestBridge, tryGetHostRequestBridge, } from './host/host-request-bridge.js';
|
|
23
|
-
export { BylineError, DbErrorCodes, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
|
|
23
|
+
export { BylineError, DbErrorCodes, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_DOCUMENT_HOOK_COMMITTED, ERR_DOCUMENT_STALE, ERR_LOCK_CONFLICT, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, getLockConflictDetails, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
|
|
24
24
|
export { generateKeyBetween, generateNKeysBetween, validateOrderKey, } from './lib/fractional-index.js';
|
|
25
25
|
export { getLogger } from './lib/logger.js';
|
|
26
26
|
export { AsyncRegistry, Registry } from './lib/registry.js';
|
|
@@ -30,7 +30,9 @@ export { mergePredicates, parsePredicateFilters, parseSort, parseWhere, } from '
|
|
|
30
30
|
export { defineRecurringTask, MIN_INTERVAL_MS, MIN_LEASE_MS, } from './scheduler/define-recurring-task.js';
|
|
31
31
|
export { getCollectionSchemasForPath } from './schemas/zod/cache.js';
|
|
32
32
|
export * from './services/index.js';
|
|
33
|
+
export { documentRevisionFromDatabase, isDocumentRevision, parseDocumentRevision, } from './storage/document-revision.js';
|
|
33
34
|
export * from './storage/index.js';
|
|
35
|
+
export { runReadSnapshot } from './storage/read-snapshot.js';
|
|
34
36
|
export { normalizeRootRelativeRedirect } from './utils/root-relative-redirect.js';
|
|
35
37
|
export { formatTextValue, looksLikeISODate, slugify, } from './utils/slugify.js';
|
|
36
38
|
export { resolveUploadFilename, slugifyFilename, } from './utils/slugify-filename.js';
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* details: { documentId },
|
|
20
20
|
* }).log(logger)
|
|
21
21
|
*/
|
|
22
|
+
import type { DocumentStaleDetails } from '../@types/document-revision.js';
|
|
22
23
|
import type { BylineLogger, LogLevel, LogLevelWithSilent } from './logger.js';
|
|
23
24
|
export type ErrorReport = {
|
|
24
25
|
code: string;
|
|
@@ -83,24 +84,40 @@ export declare const ErrorCodes: {
|
|
|
83
84
|
readonly INVALID_TRANSITION: 'ERR_INVALID_TRANSITION';
|
|
84
85
|
readonly PATCH_FAILED: 'ERR_PATCH_FAILED';
|
|
85
86
|
readonly DATABASE: 'ERR_DATABASE';
|
|
87
|
+
readonly LOCK_CONFLICT: 'ERR_LOCK_CONFLICT';
|
|
86
88
|
readonly STORAGE: 'ERR_STORAGE';
|
|
87
89
|
readonly READ_BUDGET_EXCEEDED: 'ERR_READ_BUDGET_EXCEEDED';
|
|
88
90
|
readonly READ_RECURSION: 'ERR_READ_RECURSION';
|
|
89
91
|
readonly PATH_CONFLICT: 'ERR_PATH_CONFLICT';
|
|
90
92
|
readonly AUDIT_UNSUPPORTED: 'ERR_AUDIT_UNSUPPORTED';
|
|
93
|
+
readonly DOCUMENT_HOOK_COMMITTED: 'ERR_DOCUMENT_HOOK_COMMITTED';
|
|
94
|
+
readonly DOCUMENT_STALE: 'ERR_DOCUMENT_STALE';
|
|
91
95
|
readonly TREE_HOOK_COMMITTED: 'ERR_TREE_HOOK_COMMITTED';
|
|
92
96
|
};
|
|
93
97
|
/** Stable message fallback for stale tree-placement `ERR_CONFLICT` errors. */
|
|
94
98
|
export declare const TREE_PLACEMENT_STALE_MARKER = "[ERR_CONFLICT:TREE_PLACEMENT_STALE]";
|
|
95
99
|
/** Stable message fallback for `ERR_TREE_HOOK_COMMITTED` errors. */
|
|
96
100
|
export declare const TREE_HOOK_COMMITTED_MARKER = "[ERR_TREE_HOOK_COMMITTED]";
|
|
101
|
+
/** Stable message fallback for `ERR_DOCUMENT_HOOK_COMMITTED` errors. */
|
|
102
|
+
export declare const DOCUMENT_HOOK_COMMITTED_MARKER = "[ERR_DOCUMENT_HOOK_COMMITTED]";
|
|
97
103
|
export declare const ERR_UNHANDLED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
98
104
|
export declare const ERR_NOT_FOUND: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
99
105
|
export declare const ERR_CONFLICT: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
106
|
+
/** Editorial stale state has its own code; other conflicts must not trigger reload. */
|
|
107
|
+
export declare const ERR_DOCUMENT_STALE: (options: Omit<BylineErrorOptions, 'details'> & {
|
|
108
|
+
details: DocumentStaleDetails;
|
|
109
|
+
}) => BylineError;
|
|
100
110
|
export declare const ERR_VALIDATION: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
101
111
|
export declare const ERR_INVALID_TRANSITION: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
102
112
|
export declare const ERR_PATCH_FAILED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
103
113
|
export declare const ERR_DATABASE: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
114
|
+
export declare const ERR_LOCK_CONFLICT: (options: BylineErrorOptions) => BylineError;
|
|
115
|
+
/** Safe, small contract for live errors, reports, and message-only Error transports. */
|
|
116
|
+
export declare function getLockConflictDetails(error: unknown): {
|
|
117
|
+
reason: 'lock_conflict';
|
|
118
|
+
rolledBack: true;
|
|
119
|
+
retryable: true;
|
|
120
|
+
} | null;
|
|
104
121
|
export declare const ERR_STORAGE: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
105
122
|
export declare const ERR_READ_BUDGET_EXCEEDED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
106
123
|
export declare const ERR_READ_RECURSION: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
@@ -121,6 +138,13 @@ export declare const ERR_PATH_CONFLICT: (opts: BylineErrorOptions, errorConstruc
|
|
|
121
138
|
* docs/03-architecture/03-transactions.md and docs/07-auth-and-security/02-auditability.md.
|
|
122
139
|
*/
|
|
123
140
|
export declare const ERR_AUDIT_UNSUPPORTED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
141
|
+
/**
|
|
142
|
+
* A document version committed, but its post-commit `afterCreate` or
|
|
143
|
+
* `afterUpdate` hook failed. Callers still receive a rejection so existing
|
|
144
|
+
* SDK error handling keeps working, while hosts can distinguish this from a
|
|
145
|
+
* rolled-back write and reconcile their UI without replaying the mutation.
|
|
146
|
+
*/
|
|
147
|
+
export declare const ERR_DOCUMENT_HOOK_COMMITTED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
124
148
|
/**
|
|
125
149
|
* A tree mutation and its audit row committed, but its post-commit
|
|
126
150
|
* `afterTreeChange` work failed. Callers still receive a rejection so they can
|
|
@@ -142,6 +166,7 @@ export declare const ERR_TREE_HOOK_COMMITTED: (opts: BylineErrorOptions, errorCo
|
|
|
142
166
|
export declare const DbErrorCodes: {
|
|
143
167
|
readonly UNIQUE_VIOLATION: 'DB_UNIQUE_VIOLATION';
|
|
144
168
|
readonly FOREIGN_KEY_VIOLATION: 'DB_FOREIGN_KEY_VIOLATION';
|
|
169
|
+
readonly LOCK_CONFLICT: 'DB_LOCK_CONFLICT';
|
|
145
170
|
readonly UNKNOWN: 'DB_UNKNOWN';
|
|
146
171
|
};
|
|
147
172
|
export type DbErrorCode = (typeof DbErrorCodes)[keyof typeof DbErrorCodes];
|
package/dist/lib/errors.js
CHANGED
|
@@ -117,27 +117,61 @@ export const ErrorCodes = {
|
|
|
117
117
|
INVALID_TRANSITION: 'ERR_INVALID_TRANSITION',
|
|
118
118
|
PATCH_FAILED: 'ERR_PATCH_FAILED',
|
|
119
119
|
DATABASE: 'ERR_DATABASE',
|
|
120
|
+
LOCK_CONFLICT: 'ERR_LOCK_CONFLICT',
|
|
120
121
|
STORAGE: 'ERR_STORAGE',
|
|
121
122
|
READ_BUDGET_EXCEEDED: 'ERR_READ_BUDGET_EXCEEDED',
|
|
122
123
|
READ_RECURSION: 'ERR_READ_RECURSION',
|
|
123
124
|
PATH_CONFLICT: 'ERR_PATH_CONFLICT',
|
|
124
125
|
AUDIT_UNSUPPORTED: 'ERR_AUDIT_UNSUPPORTED',
|
|
126
|
+
DOCUMENT_HOOK_COMMITTED: 'ERR_DOCUMENT_HOOK_COMMITTED',
|
|
127
|
+
DOCUMENT_STALE: 'ERR_DOCUMENT_STALE',
|
|
125
128
|
TREE_HOOK_COMMITTED: 'ERR_TREE_HOOK_COMMITTED',
|
|
126
129
|
};
|
|
127
130
|
/** Stable message fallback for stale tree-placement `ERR_CONFLICT` errors. */
|
|
128
131
|
export const TREE_PLACEMENT_STALE_MARKER = '[ERR_CONFLICT:TREE_PLACEMENT_STALE]';
|
|
129
132
|
/** Stable message fallback for `ERR_TREE_HOOK_COMMITTED` errors. */
|
|
130
133
|
export const TREE_HOOK_COMMITTED_MARKER = '[ERR_TREE_HOOK_COMMITTED]';
|
|
134
|
+
/** Stable message fallback for `ERR_DOCUMENT_HOOK_COMMITTED` errors. */
|
|
135
|
+
export const DOCUMENT_HOOK_COMMITTED_MARKER = '[ERR_DOCUMENT_HOOK_COMMITTED]';
|
|
131
136
|
// ---------------------------------------------------------------------------
|
|
132
137
|
// Pre-instantiated factories
|
|
133
138
|
// ---------------------------------------------------------------------------
|
|
134
139
|
export const ERR_UNHANDLED = createErrorType(ErrorCodes.UNHANDLED);
|
|
135
140
|
export const ERR_NOT_FOUND = createErrorType(ErrorCodes.NOT_FOUND, 'warn');
|
|
136
141
|
export const ERR_CONFLICT = createErrorType(ErrorCodes.CONFLICT, 'warn');
|
|
142
|
+
/** Editorial stale state has its own code; other conflicts must not trigger reload. */
|
|
143
|
+
export const ERR_DOCUMENT_STALE = (options) => new BylineError(ErrorCodes.DOCUMENT_STALE, { logLevel: 'warn', ...options });
|
|
137
144
|
export const ERR_VALIDATION = createErrorType(ErrorCodes.VALIDATION, 'warn');
|
|
138
145
|
export const ERR_INVALID_TRANSITION = createErrorType(ErrorCodes.INVALID_TRANSITION, 'warn');
|
|
139
146
|
export const ERR_PATCH_FAILED = createErrorType(ErrorCodes.PATCH_FAILED);
|
|
140
147
|
export const ERR_DATABASE = createErrorType(ErrorCodes.DATABASE);
|
|
148
|
+
/** Only emitted after the owned transaction has confirmed complete rollback. */
|
|
149
|
+
const LOCK_CONFLICT_MARKER = '[ERR_LOCK_CONFLICT:ROLLED_BACK]';
|
|
150
|
+
export const ERR_LOCK_CONFLICT = (options) => new BylineError(ErrorCodes.LOCK_CONFLICT, {
|
|
151
|
+
...options,
|
|
152
|
+
logLevel: 'warn',
|
|
153
|
+
message: `${LOCK_CONFLICT_MARKER} This change could not be saved because another operation was using the document. Reload before trying again.`,
|
|
154
|
+
details: { reason: 'lock_conflict', rolledBack: true, retryable: true },
|
|
155
|
+
});
|
|
156
|
+
/** Safe, small contract for live errors, reports, and message-only Error transports. */
|
|
157
|
+
export function getLockConflictDetails(error) {
|
|
158
|
+
try {
|
|
159
|
+
if (typeof error !== 'object' || error === null)
|
|
160
|
+
return null;
|
|
161
|
+
const details = Reflect.get(error, 'details');
|
|
162
|
+
const typed = Reflect.get(error, 'code') === ErrorCodes.LOCK_CONFLICT &&
|
|
163
|
+
details?.reason === 'lock_conflict' &&
|
|
164
|
+
details?.rolledBack === true &&
|
|
165
|
+
details?.retryable === true;
|
|
166
|
+
const message = Reflect.get(error, 'message');
|
|
167
|
+
if (!typed && !(typeof message === 'string' && message.startsWith(LOCK_CONFLICT_MARKER)))
|
|
168
|
+
return null;
|
|
169
|
+
return { reason: 'lock_conflict', rolledBack: true, retryable: true };
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
141
175
|
export const ERR_STORAGE = createErrorType(ErrorCodes.STORAGE);
|
|
142
176
|
export const ERR_READ_BUDGET_EXCEEDED = createErrorType(ErrorCodes.READ_BUDGET_EXCEEDED);
|
|
143
177
|
export const ERR_READ_RECURSION = createErrorType(ErrorCodes.READ_RECURSION);
|
|
@@ -158,6 +192,13 @@ export const ERR_PATH_CONFLICT = createErrorType(ErrorCodes.PATH_CONFLICT, 'warn
|
|
|
158
192
|
* docs/03-architecture/03-transactions.md and docs/07-auth-and-security/02-auditability.md.
|
|
159
193
|
*/
|
|
160
194
|
export const ERR_AUDIT_UNSUPPORTED = createErrorType(ErrorCodes.AUDIT_UNSUPPORTED);
|
|
195
|
+
/**
|
|
196
|
+
* A document version committed, but its post-commit `afterCreate` or
|
|
197
|
+
* `afterUpdate` hook failed. Callers still receive a rejection so existing
|
|
198
|
+
* SDK error handling keeps working, while hosts can distinguish this from a
|
|
199
|
+
* rolled-back write and reconcile their UI without replaying the mutation.
|
|
200
|
+
*/
|
|
201
|
+
export const ERR_DOCUMENT_HOOK_COMMITTED = createErrorType(ErrorCodes.DOCUMENT_HOOK_COMMITTED, 'warn');
|
|
161
202
|
/**
|
|
162
203
|
* A tree mutation and its audit row committed, but its post-commit
|
|
163
204
|
* `afterTreeChange` work failed. Callers still receive a rejection so they can
|
|
@@ -182,5 +223,6 @@ export const ERR_TREE_HOOK_COMMITTED = createErrorType(ErrorCodes.TREE_HOOK_COMM
|
|
|
182
223
|
export const DbErrorCodes = {
|
|
183
224
|
UNIQUE_VIOLATION: 'DB_UNIQUE_VIOLATION',
|
|
184
225
|
FOREIGN_KEY_VIOLATION: 'DB_FOREIGN_KEY_VIOLATION',
|
|
226
|
+
LOCK_CONFLICT: 'DB_LOCK_CONFLICT',
|
|
185
227
|
UNKNOWN: 'DB_UNKNOWN',
|
|
186
228
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ERR_LOCK_CONFLICT, getLockConflictDetails } from './errors.js';
|
|
3
|
+
describe('confirmed lock conflict transport', () => {
|
|
4
|
+
it('preserves the contract through reports and message-only Error reconstruction', () => {
|
|
5
|
+
const cause = Object.assign(new Error('secret driver SQL'), { code: '40P01' });
|
|
6
|
+
const error = ERR_LOCK_CONFLICT({ cause, message: cause.message });
|
|
7
|
+
expect(error.cause).toBe(cause);
|
|
8
|
+
for (const transported of [
|
|
9
|
+
error,
|
|
10
|
+
JSON.parse(JSON.stringify(error.report())),
|
|
11
|
+
new Error(error.message),
|
|
12
|
+
]) {
|
|
13
|
+
expect(getLockConflictDetails(transported)).toEqual({
|
|
14
|
+
reason: 'lock_conflict',
|
|
15
|
+
rolledBack: true,
|
|
16
|
+
retryable: true,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
expect(JSON.stringify(error.report())).not.toContain('secret driver SQL');
|
|
20
|
+
});
|
|
21
|
+
it('rejects stale state, driver failures, and incomplete rollback claims', () => {
|
|
22
|
+
for (const error of [
|
|
23
|
+
null,
|
|
24
|
+
{ code: 'ERR_DOCUMENT_STALE' },
|
|
25
|
+
{ code: '40P01' },
|
|
26
|
+
{ code: 'ERR_LOCK_CONFLICT' },
|
|
27
|
+
{ code: 'ERR_LOCK_CONFLICT', details: { rolledBack: false } },
|
|
28
|
+
])
|
|
29
|
+
expect(getLockConflictDetails(error)).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -1,8 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) Infonomic Company Limited
|
|
7
|
-
*/
|
|
8
1
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { testAdapter } from '../storage/db-adapter.test-helper.js';
|
|
1
2
|
/**
|
|
2
3
|
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
4
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
@@ -384,13 +385,13 @@ describe('runDueTasks', () => {
|
|
|
384
385
|
const injectedRun = vi.fn(async () => { });
|
|
385
386
|
const store = fakeStore({ claim: vi.fn(async ({ name }) => claimed(name)) });
|
|
386
387
|
const core = {
|
|
387
|
-
db: { scheduler: store },
|
|
388
|
+
db: testAdapter({ scheduler: store }),
|
|
388
389
|
recurringTasks: [task('core-task', coreRun)],
|
|
389
390
|
logger: silentLogger,
|
|
390
391
|
};
|
|
391
392
|
await runDueTasks(core, { owner: 'test', tasks: [task('injected-task', injectedRun)] });
|
|
392
393
|
expect(coreRun).toHaveBeenCalledTimes(1);
|
|
393
394
|
expect(injectedRun).not.toHaveBeenCalled();
|
|
394
|
-
await expect(runDueTasks({ ...core, db:
|
|
395
|
+
await expect(runDueTasks({ ...core, db: testAdapter() }, { owner: 'test' })).rejects.toThrow(/scheduler capability/i);
|
|
395
396
|
});
|
|
396
397
|
});
|
|
@@ -7,6 +7,20 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { BylineCore } from '../core.js';
|
|
9
9
|
import type { BylineLogger } from '../lib/logger.js';
|
|
10
|
+
/** The sweep depends only on this server-side operational surface. */
|
|
11
|
+
type ScheduledPublicationCore = Pick<BylineCore, 'db' | 'collections' | 'collectionRecords' | 'storage' | 'logger'> & {
|
|
12
|
+
config: {
|
|
13
|
+
slugifier?: BylineCore['config']['slugifier'];
|
|
14
|
+
i18n: {
|
|
15
|
+
content: {
|
|
16
|
+
defaultLocale: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
scheduledPublication?: {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
10
24
|
export interface ScheduledPublicationSweepOptions {
|
|
11
25
|
/** Abort before claiming another batch. */
|
|
12
26
|
signal?: AbortSignal;
|
|
@@ -28,4 +42,5 @@ export interface ScheduledPublicationSweepResult {
|
|
|
28
42
|
* Database claims provide the execution fence, so independent external
|
|
29
43
|
* orchestrators may invoke this operation concurrently.
|
|
30
44
|
*/
|
|
31
|
-
export declare function runScheduledPublicationSweep(core:
|
|
45
|
+
export declare function runScheduledPublicationSweep(core: ScheduledPublicationCore, options?: ScheduledPublicationSweepOptions): Promise<ScheduledPublicationSweepResult>;
|
|
46
|
+
export {};
|