@byline/core 6.6.0 → 6.7.1
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 +60 -0
- package/dist/auth/assert-locale-visibility.d.ts +27 -0
- package/dist/auth/assert-locale-visibility.js +43 -0
- package/dist/auth/assert-locale-visibility.test.node.js +46 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/config/resolve-item-view-columns.test.node.js +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/services/document-lifecycle/internals.js +3 -0
- package/dist/services/document-lifecycle.test.node.js +135 -0
- package/dist/services/populate.d.ts +9 -1
- package/dist/services/populate.js +4 -0
- package/dist/services/richtext-populate.d.ts +7 -1
- package/dist/services/richtext-populate.js +7 -4
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/locale-resolution.d.ts +76 -0
- package/dist/storage/locale-resolution.js +122 -0
- package/dist/storage/locale-resolution.test.node.d.ts +8 -0
- package/dist/storage/locale-resolution.test.node.js +245 -0
- package/package.json +2 -2
- package/dist/schemas/zod/review-comparison-probe.test.node.d.ts +0 -1
- package/dist/schemas/zod/review-comparison-probe.test.node.js +0 -26
- package/dist/services/restore-review-probe.test.node.js +0 -3397
- package/dist/services/review-probes.test.node.js +0 -349
- /package/dist/{services/restore-review-probe.test.node.d.ts → auth/assert-locale-visibility.test.node.d.ts} +0 -0
- /package/dist/{services/review-probes.test.node.d.ts → config/resolve-item-view-columns.test.node.d.ts} +0 -0
|
@@ -1,349 +0,0 @@
|
|
|
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 { AdminAuth, AuthError, AuthErrorCodes, createRequestContext, createSuperAdminContext, } from '@byline/auth';
|
|
9
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
-
import { BylineError, DbErrorCodes, ERR_DOCUMENT_STALE, ERR_NOT_FOUND, ErrorCodes, } from '../lib/errors.js';
|
|
11
|
-
import { runReadSnapshot } from '../storage/read-snapshot.js';
|
|
12
|
-
import { unusedRevisionStore } from '../storage/revision-store.test-helper.js';
|
|
13
|
-
import { cancelDocumentScheduledPublish, changeDocumentStatus, confirmDocumentScheduledPublish, copyToLocale, createDocument, deleteDocument, deleteLocale, duplicateDocument, listDocumentPublishSchedules, restoreDocumentVersion, scheduleDocumentPublish, unpublishDocument, updateDocument, updateDocumentSystemFields, updateDocumentWithPatches, } from './document-lifecycle/index.js';
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Fixtures / Helpers
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
const minimalCollection = {
|
|
18
|
-
path: 'articles',
|
|
19
|
-
labels: { singular: 'Article', plural: 'Articles' },
|
|
20
|
-
fields: [{ name: 'title', type: 'text', label: 'Title' }],
|
|
21
|
-
workflow: {
|
|
22
|
-
statuses: [
|
|
23
|
-
{ name: 'draft', label: 'Draft' },
|
|
24
|
-
{ name: 'published', label: 'Published' },
|
|
25
|
-
{ name: 'archived', label: 'Archived' },
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
const numericCollection = {
|
|
30
|
-
...minimalCollection,
|
|
31
|
-
fields: [
|
|
32
|
-
{ name: 'quantity', type: 'integer' },
|
|
33
|
-
{ name: 'score', type: 'float' },
|
|
34
|
-
{ name: 'price', type: 'decimal' },
|
|
35
|
-
],
|
|
36
|
-
};
|
|
37
|
-
function publishScheduleRow(overrides = {}) {
|
|
38
|
-
const now = new Date('2026-08-22T12:00:00.000Z');
|
|
39
|
-
return {
|
|
40
|
-
documentId: 'doc-1',
|
|
41
|
-
collectionId: 'col-1',
|
|
42
|
-
targetVersionId: 'ver-1',
|
|
43
|
-
authorizedRevision: 1,
|
|
44
|
-
publishAt: new Date('2026-08-23T12:00:00.000Z'),
|
|
45
|
-
state: 'armed',
|
|
46
|
-
suspendedAt: null,
|
|
47
|
-
suspendedReason: null,
|
|
48
|
-
scheduledBy: TEST_ACTOR_ID,
|
|
49
|
-
lastAuthorizedBy: TEST_ACTOR_ID,
|
|
50
|
-
lastAuthorizedAt: now,
|
|
51
|
-
scheduledAt: now,
|
|
52
|
-
updatedAt: now,
|
|
53
|
-
executionToken: null,
|
|
54
|
-
executionExpiresAt: null,
|
|
55
|
-
lastAttemptAt: null,
|
|
56
|
-
nextAttemptAt: new Date('2026-08-23T12:00:00.000Z'),
|
|
57
|
-
attemptCount: 0,
|
|
58
|
-
lastError: null,
|
|
59
|
-
...overrides,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
/** Build a mock IDbAdapter. Returns the adapter plus individual mock fns. */
|
|
63
|
-
function createMockDb() {
|
|
64
|
-
const createDocumentVersion = vi.fn().mockResolvedValue({
|
|
65
|
-
document: { id: 'ver-1', document_id: 'doc-1' },
|
|
66
|
-
fieldCount: 3,
|
|
67
|
-
});
|
|
68
|
-
const setDocumentStatus = vi.fn().mockResolvedValue(undefined);
|
|
69
|
-
const archivePublishedVersions = vi.fn().mockResolvedValue(0);
|
|
70
|
-
const softDeleteDocument = vi.fn().mockResolvedValue(1);
|
|
71
|
-
const deleteDocumentLocale = vi.fn().mockResolvedValue({ newVersionId: 'ver-2' });
|
|
72
|
-
const getDocumentById = vi.fn().mockImplementation(async () => {
|
|
73
|
-
const metadata = await getCurrentVersionMetadata();
|
|
74
|
-
return metadata == null ? null : { ...metadata, fields: {}, path: await getCurrentPath() };
|
|
75
|
-
});
|
|
76
|
-
const getDocumentSystemFieldsForUpdate = vi.fn().mockResolvedValue(null);
|
|
77
|
-
const getCurrentVersionMetadata = vi.fn().mockResolvedValue(null);
|
|
78
|
-
const getCurrentPath = vi.fn().mockResolvedValue('current-path');
|
|
79
|
-
const publishSchedule = vi.fn().mockResolvedValue({ status: 'document_not_found' });
|
|
80
|
-
const confirmPublishSchedule = vi.fn().mockResolvedValue({ status: 'schedule_not_found' });
|
|
81
|
-
const cancelPublishSchedule = vi.fn().mockResolvedValue(null);
|
|
82
|
-
const suspendPublishSchedule = vi.fn().mockResolvedValue({ status: 'schedule_not_found' });
|
|
83
|
-
const claimDuePublishSchedules = vi.fn().mockResolvedValue([]);
|
|
84
|
-
const lockPublishScheduleClaim = vi.fn().mockResolvedValue(null);
|
|
85
|
-
const deletePublishScheduleClaim = vi.fn().mockResolvedValue(false);
|
|
86
|
-
const suspendPublishScheduleClaim = vi.fn().mockResolvedValue(false);
|
|
87
|
-
const releasePublishScheduleClaim = vi.fn().mockResolvedValue(false);
|
|
88
|
-
const getPublishSchedule = vi.fn().mockResolvedValue(null);
|
|
89
|
-
const listPublishSchedules = vi.fn().mockResolvedValue({ schedules: [], total: 0 });
|
|
90
|
-
// Audit capability (docs/07-auth-and-security/02-auditability.md — W2). `withTransaction` is a passthrough
|
|
91
|
-
// in unit tests (runs the unit of work immediately, no real tx); `append`
|
|
92
|
-
// records the calls so write-point tests can assert the audit rows emitted.
|
|
93
|
-
const auditAppend = vi.fn().mockResolvedValue({ id: 'audit-1' });
|
|
94
|
-
let transactionDepth = 0;
|
|
95
|
-
const revisions = new Map();
|
|
96
|
-
const transaction = {
|
|
97
|
-
run: async (fn) => {
|
|
98
|
-
transactionDepth++;
|
|
99
|
-
try {
|
|
100
|
-
return await fn();
|
|
101
|
-
}
|
|
102
|
-
finally {
|
|
103
|
-
transactionDepth--;
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
const withTransaction = vi.spyOn(transaction, 'run');
|
|
108
|
-
const fail = () => {
|
|
109
|
-
throw new Error('unexpected singleton mapping call');
|
|
110
|
-
};
|
|
111
|
-
// Fake classifier mirroring db-postgres's real one closely enough for
|
|
112
|
-
// rethrowPathConflict's consumption of the seam: a pg-shaped 23505 with a
|
|
113
|
-
// constraint name classifies as a unique violation; anything else is
|
|
114
|
-
// unknown. Individual tests inject pg-shaped errors on `createDocumentVersion`
|
|
115
|
-
// (see `translates a Postgres unique-constraint violation...` below).
|
|
116
|
-
const classifyError = vi.fn((err) => {
|
|
117
|
-
const e = err;
|
|
118
|
-
if (e?.code === '23505') {
|
|
119
|
-
return { code: DbErrorCodes.UNIQUE_VIOLATION, constraint: e.constraint };
|
|
120
|
-
}
|
|
121
|
-
return { code: DbErrorCodes.UNKNOWN };
|
|
122
|
-
});
|
|
123
|
-
const db = {
|
|
124
|
-
classifyError,
|
|
125
|
-
revisions: {
|
|
126
|
-
...unusedRevisionStore,
|
|
127
|
-
isInTransaction: () => transactionDepth > 0,
|
|
128
|
-
lock: vi.fn(async (targets) => {
|
|
129
|
-
const observations = [];
|
|
130
|
-
for (const target of targets) {
|
|
131
|
-
const revision = revisions.get(target.documentId) ?? 1;
|
|
132
|
-
if (target.expectedRevision !== revision)
|
|
133
|
-
throw ERR_DOCUMENT_STALE({
|
|
134
|
-
message: 'stale',
|
|
135
|
-
details: {
|
|
136
|
-
reason: 'revision_mismatch',
|
|
137
|
-
documentId: target.documentId,
|
|
138
|
-
expectedRevision: target.expectedRevision,
|
|
139
|
-
currentRevision: revision,
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
const system = await getDocumentSystemFieldsForUpdate({
|
|
143
|
-
collection_id: target.collectionId,
|
|
144
|
-
document_id: target.documentId,
|
|
145
|
-
});
|
|
146
|
-
const source = await getDocumentById.mock.results.at(-1)?.value;
|
|
147
|
-
if (!system && !source)
|
|
148
|
-
throw ERR_NOT_FOUND({ message: 'document not found' });
|
|
149
|
-
observations.push({
|
|
150
|
-
documentId: target.documentId,
|
|
151
|
-
collectionId: target.collectionId,
|
|
152
|
-
revision,
|
|
153
|
-
currentVersionId: source?.document_version_id ?? 'ver-1',
|
|
154
|
-
status: source?.status ?? 'draft',
|
|
155
|
-
sourceLocale: system?.source_locale ?? source?.source_locale ?? 'en',
|
|
156
|
-
path: system?.path ?? source?.path ?? null,
|
|
157
|
-
availableLocales: system?.availableLocales ?? source?.availableLocales ?? [],
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
return observations;
|
|
161
|
-
}),
|
|
162
|
-
advance: vi.fn(async (locked) => {
|
|
163
|
-
const revision = locked.revision + 1;
|
|
164
|
-
revisions.set(locked.documentId, revision);
|
|
165
|
-
return { documentId: locked.documentId, revision };
|
|
166
|
-
}),
|
|
167
|
-
},
|
|
168
|
-
withReadSnapshot: (fn) => runReadSnapshot(db.queries, fn),
|
|
169
|
-
commands: {
|
|
170
|
-
collections: {
|
|
171
|
-
lockCollectionRegistration: vi.fn(async () => { }),
|
|
172
|
-
create: vi.fn(),
|
|
173
|
-
update: vi.fn(),
|
|
174
|
-
delete: vi.fn(),
|
|
175
|
-
},
|
|
176
|
-
documents: {
|
|
177
|
-
publishSchedules: {
|
|
178
|
-
lockDocuments: vi.fn(async () => { }),
|
|
179
|
-
schedule: publishSchedule,
|
|
180
|
-
confirm: confirmPublishSchedule,
|
|
181
|
-
cancel: cancelPublishSchedule,
|
|
182
|
-
suspendForContentEdit: suspendPublishSchedule,
|
|
183
|
-
claimDue: claimDuePublishSchedules,
|
|
184
|
-
lockClaim: lockPublishScheduleClaim,
|
|
185
|
-
deleteClaim: deletePublishScheduleClaim,
|
|
186
|
-
suspendClaimForContentEdit: suspendPublishScheduleClaim,
|
|
187
|
-
releaseClaim: releasePublishScheduleClaim,
|
|
188
|
-
},
|
|
189
|
-
createDocumentVersion,
|
|
190
|
-
updateDocumentPath: vi.fn().mockResolvedValue(undefined),
|
|
191
|
-
setDocumentAvailableLocales: vi.fn().mockResolvedValue(undefined),
|
|
192
|
-
setDocumentStatus,
|
|
193
|
-
archivePublishedVersions,
|
|
194
|
-
softDeleteDocument,
|
|
195
|
-
restoreSoftDeletedDocument: vi.fn(),
|
|
196
|
-
deleteDocumentLocale: deleteDocumentLocale,
|
|
197
|
-
setOrderKey: vi.fn(),
|
|
198
|
-
placeTreeNode: vi.fn(),
|
|
199
|
-
removeFromTree: vi.fn(),
|
|
200
|
-
promoteChildrenAndRemoveFromTree: vi.fn(async () => ({
|
|
201
|
-
removed: {
|
|
202
|
-
changed: false,
|
|
203
|
-
before: { placed: false, parentDocumentId: null, orderKey: null, index: null },
|
|
204
|
-
after: { placed: false, parentDocumentId: null, orderKey: null, index: null },
|
|
205
|
-
beforeSiblingDocumentIds: [],
|
|
206
|
-
beforeSubtreeDocumentIds: [],
|
|
207
|
-
},
|
|
208
|
-
promoted: [],
|
|
209
|
-
})),
|
|
210
|
-
},
|
|
211
|
-
counters: {
|
|
212
|
-
ensureCounterGroup: vi.fn(),
|
|
213
|
-
nextCounterValue: vi.fn(),
|
|
214
|
-
nextScopedCounterValue: vi.fn(),
|
|
215
|
-
},
|
|
216
|
-
audit: { append: auditAppend },
|
|
217
|
-
singletons: {
|
|
218
|
-
lockSlot: vi.fn(async () => { }),
|
|
219
|
-
setMapping: vi.fn(fail),
|
|
220
|
-
clearMapping: vi.fn(fail),
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
withTransaction: transaction.run,
|
|
224
|
-
queries: {
|
|
225
|
-
collections: {
|
|
226
|
-
getAllCollections: vi.fn(),
|
|
227
|
-
getCollectionByPath: vi.fn(),
|
|
228
|
-
getCollectionById: vi.fn(),
|
|
229
|
-
},
|
|
230
|
-
documents: {
|
|
231
|
-
publishSchedules: {
|
|
232
|
-
get: getPublishSchedule,
|
|
233
|
-
list: listPublishSchedules,
|
|
234
|
-
},
|
|
235
|
-
getDocumentRevision: async ({ document_id }) => revisions.get(document_id) ?? 1,
|
|
236
|
-
getDocumentSystemFieldsForUpdate,
|
|
237
|
-
getDocumentById,
|
|
238
|
-
getCurrentVersionMetadata,
|
|
239
|
-
getCurrentPath,
|
|
240
|
-
getDocumentByPath: vi.fn(),
|
|
241
|
-
getDocumentByVersion: vi.fn(),
|
|
242
|
-
getDocumentsByVersionIds: vi.fn(),
|
|
243
|
-
getDocumentsByDocumentIds: vi.fn(),
|
|
244
|
-
getDocumentHistory: vi.fn(),
|
|
245
|
-
getPublishedVersion: vi.fn(),
|
|
246
|
-
getPublishedDocumentIds: vi.fn(),
|
|
247
|
-
getDocumentCountsByStatus: vi.fn(),
|
|
248
|
-
findDocuments: vi.fn(),
|
|
249
|
-
getLastOrderKey: vi.fn(),
|
|
250
|
-
getNeighborOrderKeys: vi.fn(),
|
|
251
|
-
getCanonicalDocumentOrder: vi.fn(),
|
|
252
|
-
getTreeAncestors: vi.fn(),
|
|
253
|
-
getTreeChildren: vi.fn(),
|
|
254
|
-
getTreeParent: vi.fn(),
|
|
255
|
-
getTreeSubtree: vi.fn(),
|
|
256
|
-
},
|
|
257
|
-
audit: {
|
|
258
|
-
getDocumentAuditLog: vi.fn(async () => ({
|
|
259
|
-
entries: [],
|
|
260
|
-
meta: { total: 0, page: 1, pageSize: 20, totalPages: 0 },
|
|
261
|
-
})),
|
|
262
|
-
findAuditLog: vi.fn(async () => ({
|
|
263
|
-
entries: [],
|
|
264
|
-
meta: { total: 0, page: 1, pageSize: 20, totalPages: 0 },
|
|
265
|
-
})),
|
|
266
|
-
},
|
|
267
|
-
singletons: { getMappedDocumentId: vi.fn(fail) },
|
|
268
|
-
},
|
|
269
|
-
};
|
|
270
|
-
return {
|
|
271
|
-
db,
|
|
272
|
-
createDocumentVersion,
|
|
273
|
-
setDocumentStatus,
|
|
274
|
-
archivePublishedVersions,
|
|
275
|
-
softDeleteDocument,
|
|
276
|
-
deleteDocumentLocale,
|
|
277
|
-
getDocumentById,
|
|
278
|
-
getDocumentSystemFieldsForUpdate,
|
|
279
|
-
getCurrentVersionMetadata,
|
|
280
|
-
getCurrentPath,
|
|
281
|
-
publishSchedule,
|
|
282
|
-
confirmPublishSchedule,
|
|
283
|
-
cancelPublishSchedule,
|
|
284
|
-
suspendPublishSchedule,
|
|
285
|
-
claimDuePublishSchedules,
|
|
286
|
-
lockPublishScheduleClaim,
|
|
287
|
-
deletePublishScheduleClaim,
|
|
288
|
-
suspendPublishScheduleClaim,
|
|
289
|
-
releasePublishScheduleClaim,
|
|
290
|
-
getPublishSchedule,
|
|
291
|
-
listPublishSchedules,
|
|
292
|
-
auditAppend,
|
|
293
|
-
withTransaction,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
function pathUniqueViolation() {
|
|
297
|
-
return Object.assign(new Error('duplicate key value violates unique constraint'), {
|
|
298
|
-
code: '23505',
|
|
299
|
-
constraint: 'idx_document_paths_collection_locale_path',
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
const noopLogger = {
|
|
303
|
-
log: vi.fn(),
|
|
304
|
-
fatal: vi.fn(),
|
|
305
|
-
error: vi.fn(),
|
|
306
|
-
warn: vi.fn(),
|
|
307
|
-
info: vi.fn(),
|
|
308
|
-
debug: vi.fn(),
|
|
309
|
-
trace: vi.fn(),
|
|
310
|
-
silent: vi.fn(),
|
|
311
|
-
};
|
|
312
|
-
// A real persisted-user id is a UUID; `actorId()` only attributes UUIDs (see
|
|
313
|
-
// the regression note below). Use a valid UUID so the default context's
|
|
314
|
-
// writes are attributed.
|
|
315
|
-
const TEST_ACTOR_ID = '01901234-0000-7000-8000-000000000001';
|
|
316
|
-
function buildCtx(db, definition = minimalCollection) {
|
|
317
|
-
return {
|
|
318
|
-
db,
|
|
319
|
-
definition,
|
|
320
|
-
collectionId: 'col-1',
|
|
321
|
-
collectionVersion: 1,
|
|
322
|
-
collectionPath: definition.path,
|
|
323
|
-
logger: noopLogger,
|
|
324
|
-
defaultLocale: 'en',
|
|
325
|
-
// Inject a super-admin context by default so the bulk of existing
|
|
326
|
-
// tests do not have to care about ability enforcement. The dedicated
|
|
327
|
-
// "enforcement" block below covers the missing-context / missing-ability
|
|
328
|
-
// negative cases.
|
|
329
|
-
requestContext: createSuperAdminContext({ id: TEST_ACTOR_ID }),
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
// ---------------------------------------------------------------------------
|
|
333
|
-
// Tests
|
|
334
|
-
// ---------------------------------------------------------------------------
|
|
335
|
-
describe('review probes', () => {
|
|
336
|
-
it('rejects caller-selected published status for a create-only actor', async () => {
|
|
337
|
-
const { db } = createMockDb();
|
|
338
|
-
const ctx = buildCtx(db);
|
|
339
|
-
ctx.requestContext = createRequestContext({ actor: new AdminAuth({
|
|
340
|
-
id: 'editor', abilities: ['collections.articles.create'],
|
|
341
|
-
}) });
|
|
342
|
-
await expect(createDocument(ctx, { data: { title: 'Direct publish' }, status: 'published' })).rejects.toMatchObject({ code: AuthErrorCodes.FORBIDDEN });
|
|
343
|
-
});
|
|
344
|
-
it('rejects an undeclared initial workflow status', async () => {
|
|
345
|
-
const { db } = createMockDb();
|
|
346
|
-
const ctx = buildCtx(db);
|
|
347
|
-
await expect(createDocument(ctx, { data: { title: 'Invalid status' }, status: 'not-a-status' })).rejects.toBeDefined();
|
|
348
|
-
});
|
|
349
|
-
});
|
|
File without changes
|
|
File without changes
|