@byline/core 4.19.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 +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/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 +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 +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 +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/package.json +2 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ERR_DATABASE } from '../lib/errors.js';
|
|
2
|
+
/** Must run before collection reconciliation, counters, or boot maintenance writes. */
|
|
3
|
+
export async function assertDocumentRevisionCapability(db) {
|
|
4
|
+
if (typeof db.withTransaction !== 'function' ||
|
|
5
|
+
typeof db.withReadSnapshot !== 'function' ||
|
|
6
|
+
typeof db.commands?.collections?.lockCollectionRegistration !== 'function' ||
|
|
7
|
+
typeof db.commands?.documents?.publishSchedules?.lockDocuments !== 'function' ||
|
|
8
|
+
db.revisions == null ||
|
|
9
|
+
['assertCompatibleSchema', 'isInTransaction', 'lock', 'advance', 'readStructure'].some((name) => typeof db.revisions[name] !== 'function')) {
|
|
10
|
+
throw ERR_DATABASE({
|
|
11
|
+
message: 'Byline requires a revision-capable database adapter. Upgrade the adapter and document revision schema while all writers/workers are fenced.',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
await db.revisions.assertCompatibleSchema();
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { testAdapter } from './db-adapter.test-helper.js';
|
|
3
|
+
import { assertDocumentRevisionCapability } from './document-revision-capability.js';
|
|
4
|
+
describe('document revision adapter capability', () => {
|
|
5
|
+
const adapter = () => testAdapter({
|
|
6
|
+
commands: {
|
|
7
|
+
collections: { lockCollectionRegistration: vi.fn() },
|
|
8
|
+
documents: { publishSchedules: { lockDocuments: vi.fn() } },
|
|
9
|
+
},
|
|
10
|
+
withTransaction: vi.fn(),
|
|
11
|
+
withReadSnapshot: vi.fn(),
|
|
12
|
+
revisions: {
|
|
13
|
+
assertCompatibleSchema: vi.fn(async () => { }),
|
|
14
|
+
isInTransaction: () => false,
|
|
15
|
+
lock: vi.fn(),
|
|
16
|
+
readStructure: vi.fn(),
|
|
17
|
+
advance: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
for (const missing of [
|
|
21
|
+
'assertCompatibleSchema',
|
|
22
|
+
'isInTransaction',
|
|
23
|
+
'lock',
|
|
24
|
+
'advance',
|
|
25
|
+
'readStructure',
|
|
26
|
+
]) {
|
|
27
|
+
it(`rejects JavaScript adapters missing ${missing}`, async () => {
|
|
28
|
+
const db = adapter();
|
|
29
|
+
Reflect.deleteProperty(db.revisions, missing);
|
|
30
|
+
await expect(assertDocumentRevisionCapability(db)).rejects.toMatchObject({
|
|
31
|
+
code: 'ERR_DATABASE',
|
|
32
|
+
message: expect.stringContaining('Upgrade the adapter'),
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
it('rejects JavaScript adapters without coherent read snapshots before schema work', async () => {
|
|
37
|
+
const db = adapter();
|
|
38
|
+
Reflect.deleteProperty(db, 'withReadSnapshot');
|
|
39
|
+
await expect(assertDocumentRevisionCapability(db)).rejects.toMatchObject({
|
|
40
|
+
code: 'ERR_DATABASE',
|
|
41
|
+
});
|
|
42
|
+
expect(db.revisions.assertCompatibleSchema).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
it('rejects old adapters with no revision surface', async () => {
|
|
45
|
+
await expect(Reflect.apply(assertDocumentRevisionCapability, undefined, [{ withTransaction: vi.fn() }])).rejects.toMatchObject({ code: 'ERR_DATABASE' });
|
|
46
|
+
});
|
|
47
|
+
it('awaits schema validation and preserves the actionable upgrade error', async () => {
|
|
48
|
+
const db = adapter();
|
|
49
|
+
const error = new Error('fence and upgrade schema');
|
|
50
|
+
vi.mocked(db.revisions.assertCompatibleSchema).mockRejectedValue(error);
|
|
51
|
+
await expect(assertDocumentRevisionCapability(db)).rejects.toBe(error);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
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 declare function isDocumentRevision(value: unknown): value is number;
|
|
9
|
+
/** Validate caller input without coercing strings, nulls, or missing values. */
|
|
10
|
+
export declare function parseDocumentRevision(value: unknown): number;
|
|
11
|
+
/** Convert driver BIGINT values without accepting unsafe or noncanonical values. */
|
|
12
|
+
export declare function documentRevisionFromDatabase(value: unknown): number;
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
import { ERR_DATABASE, ERR_VALIDATION } from '../lib/errors.js';
|
|
9
|
+
export function isDocumentRevision(value) {
|
|
10
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
11
|
+
}
|
|
12
|
+
/** Validate caller input without coercing strings, nulls, or missing values. */
|
|
13
|
+
export function parseDocumentRevision(value) {
|
|
14
|
+
if (isDocumentRevision(value))
|
|
15
|
+
return value;
|
|
16
|
+
const details = {
|
|
17
|
+
reason: value === undefined ? 'missing_document_revision' : 'invalid_document_revision',
|
|
18
|
+
};
|
|
19
|
+
throw ERR_VALIDATION({
|
|
20
|
+
message: 'an observed positive safe-integer document revision is required',
|
|
21
|
+
details,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Convert driver BIGINT values without accepting unsafe or noncanonical values. */
|
|
25
|
+
export function documentRevisionFromDatabase(value) {
|
|
26
|
+
if (isDocumentRevision(value))
|
|
27
|
+
return value;
|
|
28
|
+
if (typeof value === 'bigint' && value > 0n && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
29
|
+
return Number(value);
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === 'string' && /^[1-9]\d{0,15}$/.test(value)) {
|
|
32
|
+
const revision = Number(value);
|
|
33
|
+
if (isDocumentRevision(revision) && String(revision) === value)
|
|
34
|
+
return revision;
|
|
35
|
+
}
|
|
36
|
+
throw ERR_DATABASE({ message: 'database returned an invalid document revision' });
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ErrorCodes } from '../lib/errors.js';
|
|
3
|
+
import { documentRevisionFromDatabase, isDocumentRevision, parseDocumentRevision, } from './document-revision.js';
|
|
4
|
+
describe('document revisions', () => {
|
|
5
|
+
it.each([1, 2, Number.MAX_SAFE_INTEGER])('accepts observed revision %s unchanged', (value) => {
|
|
6
|
+
expect(isDocumentRevision(value)).toBe(true);
|
|
7
|
+
expect(parseDocumentRevision(value)).toBe(value);
|
|
8
|
+
});
|
|
9
|
+
it.each([null, '1', 1n, 0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, {}, []])('rejects invalid caller input %s without coercion', (value) => {
|
|
10
|
+
expect(isDocumentRevision(value)).toBe(false);
|
|
11
|
+
expect(() => parseDocumentRevision(value)).toThrowError(expect.objectContaining({
|
|
12
|
+
code: ErrorCodes.VALIDATION,
|
|
13
|
+
details: { reason: 'invalid_document_revision' },
|
|
14
|
+
}));
|
|
15
|
+
});
|
|
16
|
+
it('distinguishes an omitted revision from malformed input', () => {
|
|
17
|
+
expect(() => parseDocumentRevision(undefined)).toThrowError(expect.objectContaining({
|
|
18
|
+
code: ErrorCodes.VALIDATION,
|
|
19
|
+
details: { reason: 'missing_document_revision' },
|
|
20
|
+
}));
|
|
21
|
+
});
|
|
22
|
+
it.each([1, '1', 1n, Number.MAX_SAFE_INTEGER, '9007199254740991', 9007199254740991n])('converts the database representation %s exactly', (value) => expect(documentRevisionFromDatabase(value)).toBe(Number(value)));
|
|
23
|
+
it.each([
|
|
24
|
+
undefined,
|
|
25
|
+
null,
|
|
26
|
+
0,
|
|
27
|
+
-1,
|
|
28
|
+
1.2,
|
|
29
|
+
NaN,
|
|
30
|
+
Infinity,
|
|
31
|
+
'01',
|
|
32
|
+
'+1',
|
|
33
|
+
'1.0',
|
|
34
|
+
'1e1',
|
|
35
|
+
' 1',
|
|
36
|
+
'1 ',
|
|
37
|
+
'1\n',
|
|
38
|
+
'',
|
|
39
|
+
'9007199254740992',
|
|
40
|
+
'9007199254740993',
|
|
41
|
+
9007199254740992n,
|
|
42
|
+
-1n,
|
|
43
|
+
Number.MAX_SAFE_INTEGER + 1,
|
|
44
|
+
{ valueOf: () => 1 },
|
|
45
|
+
])('rejects malformed/overflowing storage value %s', (value) => {
|
|
46
|
+
expect(() => documentRevisionFromDatabase(value)).toThrowError(expect.objectContaining({ code: ErrorCodes.DATABASE }));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { IDbAdapter, ReadSnapshotQueries } from '../@types/db-types.js';
|
|
2
|
+
/** Construct an allowlisted facade without leaking query classes or their executors. */
|
|
3
|
+
export declare function runReadSnapshot<T>(queries: IDbAdapter['queries'], fn: (queries: ReadSnapshotQueries) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ERR_DATABASE } from '../lib/errors.js';
|
|
2
|
+
/** Construct an allowlisted facade without leaking query classes or their executors. */
|
|
3
|
+
export async function runReadSnapshot(queries, fn) {
|
|
4
|
+
let active = true;
|
|
5
|
+
function bind(source, names) {
|
|
6
|
+
return Object.freeze(Object.fromEntries(names.map((name) => [
|
|
7
|
+
name,
|
|
8
|
+
(...args) => {
|
|
9
|
+
if (!active)
|
|
10
|
+
throw ERR_DATABASE({ message: 'Read snapshot has expired' });
|
|
11
|
+
return source[name].apply(source, args);
|
|
12
|
+
},
|
|
13
|
+
])));
|
|
14
|
+
}
|
|
15
|
+
const facade = Object.freeze({
|
|
16
|
+
collections: bind(queries.collections, [
|
|
17
|
+
'getAllCollections',
|
|
18
|
+
'getCollectionByPath',
|
|
19
|
+
'getCollectionById',
|
|
20
|
+
]),
|
|
21
|
+
audit: bind(queries.audit, ['getDocumentAuditLog', 'findAuditLog']),
|
|
22
|
+
singletons: bind(queries.singletons, ['getMappedDocumentId']),
|
|
23
|
+
documents: Object.freeze({
|
|
24
|
+
...bind(queries.documents, [
|
|
25
|
+
'getDocumentRevision',
|
|
26
|
+
'getDocumentById',
|
|
27
|
+
'getCurrentVersionMetadata',
|
|
28
|
+
'getCurrentPath',
|
|
29
|
+
'getDocumentByPath',
|
|
30
|
+
'getDocumentByVersion',
|
|
31
|
+
'getDocumentsByVersionIds',
|
|
32
|
+
'getDocumentsByDocumentIds',
|
|
33
|
+
'getDocumentHistory',
|
|
34
|
+
'getPublishedVersion',
|
|
35
|
+
'getPublishedDocumentIds',
|
|
36
|
+
'getDocumentCountsByStatus',
|
|
37
|
+
'findDocuments',
|
|
38
|
+
'getLastOrderKey',
|
|
39
|
+
'getNeighborOrderKeys',
|
|
40
|
+
'getCanonicalDocumentOrder',
|
|
41
|
+
'getTreeAncestors',
|
|
42
|
+
'getTreeChildren',
|
|
43
|
+
'getTreeParent',
|
|
44
|
+
'getTreeSubtree',
|
|
45
|
+
]),
|
|
46
|
+
publishSchedules: bind(queries.documents.publishSchedules, ['get', 'list']),
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
try {
|
|
50
|
+
return await fn(facade);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
active = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Unrelated unit fixtures must fail if they accidentally exercise revision storage. */
|
|
2
|
+
export const unusedRevisionStore = {
|
|
3
|
+
readStructure: async () => {
|
|
4
|
+
throw new Error('Structural storage is outside this unit fixture.');
|
|
5
|
+
},
|
|
6
|
+
assertCompatibleSchema: async () => {
|
|
7
|
+
throw new Error('Revision storage is outside this unit fixture.');
|
|
8
|
+
},
|
|
9
|
+
isInTransaction: () => false,
|
|
10
|
+
lock: async () => {
|
|
11
|
+
throw new Error('Revision storage is outside this unit fixture.');
|
|
12
|
+
},
|
|
13
|
+
advance: async () => {
|
|
14
|
+
throw new Error('Revision storage is outside this unit fixture.');
|
|
15
|
+
},
|
|
16
|
+
};
|
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": "
|
|
5
|
+
"version": "5.0.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"sharp": "^0.35.4",
|
|
88
88
|
"uuid": "^14.0.2",
|
|
89
89
|
"zod": "^4.4.3",
|
|
90
|
-
"@byline/auth": "
|
|
90
|
+
"@byline/auth": "5.0.0"
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
93
|
"@biomejs/biome": "2.5.10",
|