@byline/core 4.19.0 → 5.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/db-types.d.ts +32 -1
- 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/@types/site-config.d.ts +3 -1
- package/dist/config/attach-hooks.test.node.js +2 -1
- package/dist/config/config-hooks.test.node.js +2 -1
- package/dist/core.js +13 -1
- 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 +15 -0
- package/dist/lib/errors.js +32 -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 +54 -12
- package/dist/scheduler/ticker.test.node.d.ts +0 -7
- package/dist/scheduler/ticker.test.node.js +3 -2
- package/dist/scheduler/validate-scheduler-config.d.ts +2 -0
- package/dist/scheduler/validate-scheduler-config.js +5 -0
- package/dist/scheduler/validate-scheduler-config.test.node.js +16 -0
- 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 +2 -1
- package/dist/services/document-lifecycle/committed-hook.js +19 -2
- package/dist/services/document-lifecycle/copy-to-locale.d.ts +2 -0
- package/dist/services/document-lifecycle/copy-to-locale.js +28 -43
- package/dist/services/document-lifecycle/create.d.ts +1 -0
- package/dist/services/document-lifecycle/create.js +4 -20
- package/dist/services/document-lifecycle/delete-locale.d.ts +4 -2
- package/dist/services/document-lifecycle/delete-locale.js +35 -37
- 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 +48 -41
- package/dist/services/document-lifecycle/index.d.ts +3 -1
- package/dist/services/document-lifecycle/index.js +3 -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 +19 -25
- 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 +101 -80
- package/dist/services/document-lifecycle.test.node.js +401 -73
- 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 +74 -77
- 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 +67 -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/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/dist/validation/password-sign-in.test.node.d.ts +8 -0
- package/dist/validation/password-sign-in.test.node.js +21 -0
- package/dist/validation/shared.d.ts +5 -11
- package/dist/validation/shared.js +7 -1
- package/package.json +8 -8
|
@@ -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>;
|
|
@@ -880,6 +905,7 @@ export interface IDocumentCommands {
|
|
|
880
905
|
*/
|
|
881
906
|
restoreSoftDeletedDocument(params: {
|
|
882
907
|
document_id: string;
|
|
908
|
+
expectedRevision: number;
|
|
883
909
|
}): Promise<number>;
|
|
884
910
|
/**
|
|
885
911
|
* Remove one content locale's data from a document by writing a new
|
|
@@ -978,6 +1004,11 @@ export interface ICollectionQueries {
|
|
|
978
1004
|
getCollectionById(id: string): Promise<any>;
|
|
979
1005
|
}
|
|
980
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>;
|
|
981
1012
|
/** Scheduled-publication reads; callers resolve collection abilities above storage. */
|
|
982
1013
|
publishSchedules: IDocumentPublishScheduleQueries;
|
|
983
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';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import type { SessionProvider } from '@byline/auth';
|
|
8
|
+
import type { PasswordSignInProtection, SessionProvider } from '@byline/auth';
|
|
9
9
|
import type { RecurringTaskDefinition } from '../scheduler/types.js';
|
|
10
10
|
import type { SlugifierFn } from '../utils/slugify.js';
|
|
11
11
|
import type { FilenameSlugifierFn } from '../utils/slugify-filename.js';
|
|
@@ -351,6 +351,8 @@ export interface ServerConfig<TAdminStore = unknown> extends BaseConfig {
|
|
|
351
351
|
* ```
|
|
352
352
|
*/
|
|
353
353
|
sessionProvider?: SessionProvider;
|
|
354
|
+
/** Required by the host password sign-in endpoint; shared limiter and trusted IP resolver. */
|
|
355
|
+
passwordSignIn?: PasswordSignInProtection;
|
|
354
356
|
/**
|
|
355
357
|
* Adapter-built bundle of admin repositories (users / roles /
|
|
356
358
|
* permissions / refresh tokens). Typically constructed via the
|
|
@@ -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
|
*
|
|
@@ -82,7 +83,11 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
82
83
|
},
|
|
83
84
|
});
|
|
84
85
|
}
|
|
85
|
-
validateSchedulerConfig({
|
|
86
|
+
validateSchedulerConfig({
|
|
87
|
+
tasks: configuredTasks,
|
|
88
|
+
adapter: composed.db,
|
|
89
|
+
passwordSignIn: resolvedConfig.passwordSignIn,
|
|
90
|
+
});
|
|
86
91
|
// Freeze a snapshot of the validated task set: a new array of new, frozen
|
|
87
92
|
// objects, then freeze the array itself. Without this, `core.recurringTasks`
|
|
88
93
|
// would hold the caller's own array and definition objects, so a caller
|
|
@@ -118,6 +123,7 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
118
123
|
// Reconcile collection definitions with the database: insert new rows,
|
|
119
124
|
// bump schema versions when the fingerprint has drifted, and build the
|
|
120
125
|
// in-memory record cache used by the lifecycle/upload/client paths.
|
|
126
|
+
await assertDocumentRevisionCapability(composed.db);
|
|
121
127
|
const collectionRecords = await ensureCollections({
|
|
122
128
|
definitions: composed.collections,
|
|
123
129
|
db: composed.db,
|
|
@@ -161,6 +167,12 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
161
167
|
// abilities. Plugins and subsystems add their own via
|
|
162
168
|
// `core.registerAbility()` or `core.abilities.register()`.
|
|
163
169
|
const abilities = new AbilityRegistry();
|
|
170
|
+
abilities.register({
|
|
171
|
+
key: 'system.documentMaintenance',
|
|
172
|
+
label: 'Maintain document fields while preserving status',
|
|
173
|
+
group: 'system',
|
|
174
|
+
source: 'core',
|
|
175
|
+
});
|
|
164
176
|
for (const definition of composed.collections) {
|
|
165
177
|
registerDocumentAbilities(abilities, definition);
|
|
166
178
|
}
|
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_DOCUMENT_HOOK_COMMITTED, 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_DOCUMENT_HOOK_COMMITTED, 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,12 +84,14 @@ 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';
|
|
91
93
|
readonly DOCUMENT_HOOK_COMMITTED: 'ERR_DOCUMENT_HOOK_COMMITTED';
|
|
94
|
+
readonly DOCUMENT_STALE: 'ERR_DOCUMENT_STALE';
|
|
92
95
|
readonly TREE_HOOK_COMMITTED: 'ERR_TREE_HOOK_COMMITTED';
|
|
93
96
|
};
|
|
94
97
|
/** Stable message fallback for stale tree-placement `ERR_CONFLICT` errors. */
|
|
@@ -100,10 +103,21 @@ export declare const DOCUMENT_HOOK_COMMITTED_MARKER = "[ERR_DOCUMENT_HOOK_COMMIT
|
|
|
100
103
|
export declare const ERR_UNHANDLED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
101
104
|
export declare const ERR_NOT_FOUND: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
102
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;
|
|
103
110
|
export declare const ERR_VALIDATION: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
104
111
|
export declare const ERR_INVALID_TRANSITION: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
105
112
|
export declare const ERR_PATCH_FAILED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
106
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;
|
|
107
121
|
export declare const ERR_STORAGE: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
108
122
|
export declare const ERR_READ_BUDGET_EXCEEDED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
109
123
|
export declare const ERR_READ_RECURSION: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
@@ -152,6 +166,7 @@ export declare const ERR_TREE_HOOK_COMMITTED: (opts: BylineErrorOptions, errorCo
|
|
|
152
166
|
export declare const DbErrorCodes: {
|
|
153
167
|
readonly UNIQUE_VIOLATION: 'DB_UNIQUE_VIOLATION';
|
|
154
168
|
readonly FOREIGN_KEY_VIOLATION: 'DB_FOREIGN_KEY_VIOLATION';
|
|
169
|
+
readonly LOCK_CONFLICT: 'DB_LOCK_CONFLICT';
|
|
155
170
|
readonly UNKNOWN: 'DB_UNKNOWN';
|
|
156
171
|
};
|
|
157
172
|
export type DbErrorCode = (typeof DbErrorCodes)[keyof typeof DbErrorCodes];
|
package/dist/lib/errors.js
CHANGED
|
@@ -117,12 +117,14 @@ 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',
|
|
125
126
|
DOCUMENT_HOOK_COMMITTED: 'ERR_DOCUMENT_HOOK_COMMITTED',
|
|
127
|
+
DOCUMENT_STALE: 'ERR_DOCUMENT_STALE',
|
|
126
128
|
TREE_HOOK_COMMITTED: 'ERR_TREE_HOOK_COMMITTED',
|
|
127
129
|
};
|
|
128
130
|
/** Stable message fallback for stale tree-placement `ERR_CONFLICT` errors. */
|
|
@@ -137,10 +139,39 @@ export const DOCUMENT_HOOK_COMMITTED_MARKER = '[ERR_DOCUMENT_HOOK_COMMITTED]';
|
|
|
137
139
|
export const ERR_UNHANDLED = createErrorType(ErrorCodes.UNHANDLED);
|
|
138
140
|
export const ERR_NOT_FOUND = createErrorType(ErrorCodes.NOT_FOUND, 'warn');
|
|
139
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 });
|
|
140
144
|
export const ERR_VALIDATION = createErrorType(ErrorCodes.VALIDATION, 'warn');
|
|
141
145
|
export const ERR_INVALID_TRANSITION = createErrorType(ErrorCodes.INVALID_TRANSITION, 'warn');
|
|
142
146
|
export const ERR_PATCH_FAILED = createErrorType(ErrorCodes.PATCH_FAILED);
|
|
143
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
|
+
}
|
|
144
175
|
export const ERR_STORAGE = createErrorType(ErrorCodes.STORAGE);
|
|
145
176
|
export const ERR_READ_BUDGET_EXCEEDED = createErrorType(ErrorCodes.READ_BUDGET_EXCEEDED);
|
|
146
177
|
export const ERR_READ_RECURSION = createErrorType(ErrorCodes.READ_RECURSION);
|
|
@@ -192,5 +223,6 @@ export const ERR_TREE_HOOK_COMMITTED = createErrorType(ErrorCodes.TREE_HOOK_COMM
|
|
|
192
223
|
export const DbErrorCodes = {
|
|
193
224
|
UNIQUE_VIOLATION: 'DB_UNIQUE_VIOLATION',
|
|
194
225
|
FOREIGN_KEY_VIOLATION: 'DB_FOREIGN_KEY_VIOLATION',
|
|
226
|
+
LOCK_CONFLICT: 'DB_LOCK_CONFLICT',
|
|
195
227
|
UNKNOWN: 'DB_UNKNOWN',
|
|
196
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 {};
|