@byline/cli 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/lib/copied-script-drift.test.d.ts +2 -0
- package/dist/lib/copied-script-drift.test.d.ts.map +1 -0
- package/dist/lib/copied-script-drift.test.js +37 -0
- package/dist/lib/copied-script-drift.test.js.map +1 -0
- package/dist/templates/byline-examples/scripts/import-docs.ts +13 -19
- package/dist/templates/byline-examples/scripts/lib/walk-document-status.ts +21 -0
- package/dist/templates/byline-examples/scripts/regenerate-media-operation.ts +17 -69
- package/dist/templates/byline-examples/scripts/regenerate-media.ts +20 -23
- package/dist/templates/byline-examples/seeds/site-settings.ts +10 -7
- package/dist/templates/dialects/postgres/byline-examples/scripts/re-anchor.ts +15 -2
- package/dist/templates/migrations/mysql/{0000_clean_magdalene.sql → 0000_pale_bastion.sql} +6 -2
- package/dist/templates/migrations/mysql/meta/_journal.json +2 -2
- package/dist/templates/migrations/postgres/{0000_quiet_johnny_blaze.sql → 0000_yielding_vulture.sql} +6 -2
- package/dist/templates/migrations/postgres/meta/_journal.json +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copied-script-drift.test.d.ts","sourceRoot":"","sources":["../../src/lib/copied-script-drift.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const cliTemplates = resolve(here, '../templates');
|
|
7
|
+
const webappByline = resolve(here, '../../../../apps/webapp/byline');
|
|
8
|
+
const exactCopies = [
|
|
9
|
+
{
|
|
10
|
+
name: 'media regeneration operation',
|
|
11
|
+
template: 'byline-examples/scripts/regenerate-media-operation.ts',
|
|
12
|
+
application: 'scripts/regenerate-media-operation.ts',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'media regeneration caller',
|
|
16
|
+
template: 'byline-examples/scripts/regenerate-media.ts',
|
|
17
|
+
application: 'scripts/regenerate-media.ts',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'PostgreSQL source-locale re-anchor script',
|
|
21
|
+
template: 'dialects/postgres/byline-examples/scripts/re-anchor.ts',
|
|
22
|
+
application: 'scripts/re-anchor.ts',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'singleton seed',
|
|
26
|
+
template: 'byline-examples/seeds/site-settings.ts',
|
|
27
|
+
application: 'seeds/site-settings.ts',
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
describe('R01 copied maintenance scripts', () => {
|
|
31
|
+
for (const copy of exactCopies) {
|
|
32
|
+
it(`keeps the ${copy.name} byte-for-byte equal to its application copy`, () => {
|
|
33
|
+
expect(readFileSync(resolve(webappByline, copy.application), 'utf8')).toBe(readFileSync(resolve(cliTemplates, copy.template), 'utf8'));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
//# sourceMappingURL=copied-script-drift.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copied-script-drift.test.js","sourceRoot":"","sources":["../../src/lib/copied-script-drift.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE7C,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AACpD,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,EAAE,cAAc,CAAC,CAAA;AAClD,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,EAAE,gCAAgC,CAAC,CAAA;AAEpE,MAAM,WAAW,GAAG;IAClB;QACE,IAAI,EAAE,8BAA8B;QACpC,QAAQ,EAAE,uDAAuD;QACjE,WAAW,EAAE,uCAAuC;KACrD;IACD;QACE,IAAI,EAAE,2BAA2B;QACjC,QAAQ,EAAE,6CAA6C;QACvD,WAAW,EAAE,6BAA6B;KAC3C;IACD;QACE,IAAI,EAAE,2CAA2C;QACjD,QAAQ,EAAE,wDAAwD;QAClE,WAAW,EAAE,sBAAsB;KACpC;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,wCAAwC;QAClD,WAAW,EAAE,wBAAwB;KACtC;CACO,CAAA;AAEV,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,EAAE,CAAC,aAAa,IAAI,CAAC,IAAI,8CAA8C,EAAE,GAAG,EAAE;YAC5E,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CACxE,YAAY,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAC3D,CAAA;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;AACH,CAAC,CAAC,CAAA"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { walkToStatus } from './lib/walk-document-status.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
|
|
@@ -185,25 +186,13 @@ function derivePath(frontmatter: DocFrontmatter, locale: string): string {
|
|
|
185
186
|
* through any intermediate statuses (e.g. needs_review). No-op when the
|
|
186
187
|
* workflow doesn't include the target or when already at/past it.
|
|
187
188
|
*/
|
|
188
|
-
|
|
189
|
-
handle: DocsHandle,
|
|
190
|
-
documentId: string,
|
|
191
|
-
workflowStatuses: readonly { name: string }[],
|
|
192
|
-
currentStatus: string,
|
|
193
|
-
targetStatus: string
|
|
194
|
-
): Promise<void> {
|
|
195
|
-
const currentIdx = workflowStatuses.findIndex((s) => s.name === currentStatus)
|
|
196
|
-
const targetIdx = workflowStatuses.findIndex((s) => s.name === targetStatus)
|
|
197
|
-
if (currentIdx === -1 || targetIdx === -1 || targetIdx <= currentIdx) return
|
|
198
|
-
for (let i = currentIdx + 1; i <= targetIdx; i++) {
|
|
199
|
-
await handle.changeStatus(documentId, workflowStatuses[i].name)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
189
|
+
|
|
202
190
|
|
|
203
191
|
interface ProcessResult {
|
|
204
192
|
filePath: string
|
|
205
193
|
action: 'created' | 'updated' | 'skipped'
|
|
206
194
|
documentId?: string
|
|
195
|
+
revision?: number
|
|
207
196
|
path: string
|
|
208
197
|
}
|
|
209
198
|
|
|
@@ -301,18 +290,22 @@ async function processFile(
|
|
|
301
290
|
const workflowStatuses = definition?.workflow?.statuses ?? []
|
|
302
291
|
const defaultStatus = workflowStatuses[0]?.name ?? 'draft'
|
|
303
292
|
|
|
304
|
-
const
|
|
293
|
+
const selected = await handle.findByPath(docPath, {
|
|
305
294
|
locale,
|
|
306
295
|
status: 'any',
|
|
307
296
|
_bypassBeforeRead: true,
|
|
308
297
|
})
|
|
309
298
|
|
|
299
|
+
const existing = selected == null ? null : await handle.findByIdForEdit(selected.id, { locale, _bypassBeforeRead: true })
|
|
300
|
+
if (selected != null && existing == null) throw new Error('Import target is no longer available; rerun the import.')
|
|
301
|
+
|
|
310
302
|
if (existing) {
|
|
311
303
|
// Don't clobber publishedOn if Byline already has one.
|
|
312
304
|
if (existing.fields?.publishedOn) {
|
|
313
305
|
delete payload.publishedOn
|
|
314
306
|
}
|
|
315
307
|
const result = await handle.update(existing.id, payload, {
|
|
308
|
+
expectedRevision: existing.revision,
|
|
316
309
|
locale,
|
|
317
310
|
// Advertise the imported locale (editorial available-locales set), merged
|
|
318
311
|
// with whatever is already advertised so a later-locale re-import doesn't
|
|
@@ -320,8 +313,8 @@ async function processFile(
|
|
|
320
313
|
// completeness ledger (intersection). See docs/08-internationalization/index.md.
|
|
321
314
|
availableLocales: [...new Set([...(existing.availableLocales ?? []), locale])],
|
|
322
315
|
})
|
|
323
|
-
await walkToStatus(handle, result.documentId, workflowStatuses, defaultStatus, desiredStatus)
|
|
324
|
-
return { filePath, action: 'updated', documentId: result.documentId, path: docPath }
|
|
316
|
+
const revision = await walkToStatus(handle, result.documentId, workflowStatuses, defaultStatus, desiredStatus, result.revision)
|
|
317
|
+
return { filePath, action: 'updated', documentId: result.documentId, revision, path: docPath }
|
|
325
318
|
}
|
|
326
319
|
|
|
327
320
|
const result = await handle.create(payload, {
|
|
@@ -332,7 +325,7 @@ async function processFile(
|
|
|
332
325
|
// intersection of this editorial set with the completeness ledger.
|
|
333
326
|
availableLocales: [locale],
|
|
334
327
|
})
|
|
335
|
-
return { filePath, action: 'created', documentId: result.documentId, path: docPath }
|
|
328
|
+
return { filePath, action: 'created', documentId: result.documentId, revision: result.revision, path: docPath }
|
|
336
329
|
}
|
|
337
330
|
|
|
338
331
|
/**
|
|
@@ -386,7 +379,8 @@ async function placeTreeFromDirectories(
|
|
|
386
379
|
const groupKey = parentDocumentId ?? ROOT_GROUP
|
|
387
380
|
const beforeDocumentId = lastSiblingByGroup.get(groupKey) ?? null
|
|
388
381
|
try {
|
|
389
|
-
|
|
382
|
+
if (r.revision == null) throw new Error('Imported document has no observed revision')
|
|
383
|
+
await handle.placeTreeNode(r.documentId, { expectedRevision: r.revision, parentDocumentId, beforeDocumentId })
|
|
390
384
|
lastSiblingByGroup.set(groupKey, r.documentId)
|
|
391
385
|
if (parentDocumentId != null) {
|
|
392
386
|
placed += 1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CollectionHandle } from '@byline/client'
|
|
2
|
+
|
|
3
|
+
/** Preserve the revision returned by each successful workflow transition.
|
|
4
|
+
* A conflict terminates the walk; it never fetches a newer observation. */
|
|
5
|
+
export async function walkToStatus(
|
|
6
|
+
handle: Pick<CollectionHandle, 'changeStatus'>,
|
|
7
|
+
documentId: string,
|
|
8
|
+
workflowStatuses: readonly { name: string }[],
|
|
9
|
+
currentStatus: string,
|
|
10
|
+
targetStatus: string,
|
|
11
|
+
expectedRevision: number
|
|
12
|
+
): Promise<number> {
|
|
13
|
+
const currentIdx = workflowStatuses.findIndex((s) => s.name === currentStatus)
|
|
14
|
+
const targetIdx = workflowStatuses.findIndex((s) => s.name === targetStatus)
|
|
15
|
+
if (currentIdx === -1 || targetIdx === -1 || targetIdx <= currentIdx) return expectedRevision
|
|
16
|
+
for (let i = currentIdx + 1; i <= targetIdx; i++) {
|
|
17
|
+
const result = await handle.changeStatus(documentId, workflowStatuses[i].name, { expectedRevision })
|
|
18
|
+
expectedRevision = result.revision
|
|
19
|
+
}
|
|
20
|
+
return expectedRevision
|
|
21
|
+
}
|
|
@@ -6,14 +6,10 @@
|
|
|
6
6
|
* storage.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
type CollectionDefinition,
|
|
14
|
-
type IDbAdapter,
|
|
15
|
-
type StoredFileValue,
|
|
16
|
-
} from '@byline/core'
|
|
9
|
+
import type { StoredFileValue } from '@byline/core'
|
|
10
|
+
import { getDocumentHookCommittedDetails } from '@byline/core/services'
|
|
11
|
+
|
|
12
|
+
export { replaceDocumentFieldsPreservingStatus as replaceMediaVersionPreservingStatus } from '@byline/core/services'
|
|
17
13
|
|
|
18
14
|
export function storedFilePaths(value: StoredFileValue): Set<string> {
|
|
19
15
|
return new Set([
|
|
@@ -47,66 +43,18 @@ export function assertCompleteVariantSet(
|
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* ordinary application writes. Scripts are the supported escape hatch for
|
|
64
|
-
* migrations that must preserve metadata while still using the lifecycle for
|
|
65
|
-
* the content update itself.
|
|
66
|
-
*/
|
|
67
|
-
export async function replaceMediaVersionPreservingStatus(params: {
|
|
68
|
-
db: IDbAdapter
|
|
69
|
-
definition: CollectionDefinition
|
|
70
|
-
collectionId: string
|
|
71
|
-
handle: Pick<CollectionHandle, 'update'>
|
|
72
|
-
documentId: string
|
|
73
|
-
fields: Record<string, any>
|
|
74
|
-
targetStatus: string
|
|
75
|
-
}): Promise<{ documentVersionId: string }> {
|
|
76
|
-
const statuses = getWorkflowStatuses(params.definition).map((status) => status.name)
|
|
77
|
-
if (!statuses.includes(params.targetStatus)) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
`regenerate-media: status '${params.targetStatus}' is not declared on collection ` +
|
|
80
|
-
`'${params.definition.path}' (declared: ${statuses.join(', ')}).`
|
|
81
|
-
)
|
|
46
|
+
/** Only rejected document writes may discard freshly prepared storage objects. */
|
|
47
|
+
export function mediaFailureOutcome(
|
|
48
|
+
error: unknown,
|
|
49
|
+
freshPaths: ReadonlySet<string>,
|
|
50
|
+
oldPaths: ReadonlySet<string>
|
|
51
|
+
):
|
|
52
|
+
| { committed: true; revision: number; cleanupPaths: Set<string> }
|
|
53
|
+
| { committed: false; cleanupPaths: Set<string> } {
|
|
54
|
+
const committed = getDocumentHookCommittedDetails(error)
|
|
55
|
+
if (committed) return { committed: true, revision: committed.revision, cleanupPaths: new Set() }
|
|
56
|
+
return {
|
|
57
|
+
committed: false,
|
|
58
|
+
cleanupPaths: new Set([...freshPaths].filter((path) => !oldPaths.has(path))),
|
|
82
59
|
}
|
|
83
|
-
|
|
84
|
-
const defaultStatus = getDefaultStatus(params.definition)
|
|
85
|
-
return params.db.withTransaction(async () => {
|
|
86
|
-
const result = await params.handle.update(params.documentId, params.fields)
|
|
87
|
-
if (params.targetStatus === defaultStatus) return result
|
|
88
|
-
|
|
89
|
-
await params.db.commands.documents.setDocumentStatus({
|
|
90
|
-
document_version_id: result.documentVersionId,
|
|
91
|
-
status: params.targetStatus,
|
|
92
|
-
})
|
|
93
|
-
if (params.targetStatus === 'published') {
|
|
94
|
-
await params.db.commands.documents.archivePublishedVersions({
|
|
95
|
-
document_id: params.documentId,
|
|
96
|
-
excludeVersionId: result.documentVersionId,
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
await params.db.commands.audit.append({
|
|
100
|
-
documentId: params.documentId,
|
|
101
|
-
collectionId: params.collectionId,
|
|
102
|
-
actorId: null,
|
|
103
|
-
actorRealm: 'system',
|
|
104
|
-
action: 'document.status.changed',
|
|
105
|
-
field: 'status',
|
|
106
|
-
before: defaultStatus,
|
|
107
|
-
after: params.targetStatus,
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
return result
|
|
111
|
-
})
|
|
112
60
|
}
|
|
@@ -61,17 +61,14 @@ import path from 'node:path'
|
|
|
61
61
|
|
|
62
62
|
import { createSuperAdminContext } from '@byline/auth'
|
|
63
63
|
import { createBylineClient } from '@byline/client'
|
|
64
|
-
import {
|
|
65
|
-
type FieldUploadContext,
|
|
66
|
-
getCollectionDefinition,
|
|
67
|
-
getServerConfig,
|
|
68
|
-
} from '@byline/core'
|
|
64
|
+
import { type FieldUploadContext, getCollectionDefinition, getServerConfig } from '@byline/core'
|
|
69
65
|
import { extractImageMeta, generateImageVariants, isBypassMimeType } from '@byline/core/image'
|
|
70
66
|
import { uploadField as coreUploadField } from '@byline/core/services'
|
|
71
|
-
|
|
72
67
|
import type { MediaFields } from '@byline/generated-types'
|
|
68
|
+
|
|
73
69
|
import {
|
|
74
70
|
assertCompleteVariantSet,
|
|
71
|
+
mediaFailureOutcome,
|
|
75
72
|
replaceMediaVersionPreservingStatus,
|
|
76
73
|
storedFilePaths,
|
|
77
74
|
} from './regenerate-media-operation.js'
|
|
@@ -120,14 +117,15 @@ async function run(): Promise<void> {
|
|
|
120
117
|
id: string
|
|
121
118
|
path: string
|
|
122
119
|
status: string
|
|
120
|
+
revision: number
|
|
123
121
|
fields: MediaFields
|
|
124
122
|
}[] = []
|
|
125
123
|
const pageSize = 100
|
|
126
124
|
for (let page = 1; ; page++) {
|
|
127
|
-
const result = await handle.
|
|
125
|
+
const result = await handle.findForEdit({
|
|
126
|
+
locale: 'all',
|
|
128
127
|
page,
|
|
129
128
|
pageSize,
|
|
130
|
-
status: 'any',
|
|
131
129
|
_bypassBeforeRead: true,
|
|
132
130
|
})
|
|
133
131
|
for (const d of result.docs) {
|
|
@@ -135,6 +133,7 @@ async function run(): Promise<void> {
|
|
|
135
133
|
id: d.id,
|
|
136
134
|
path: d.path,
|
|
137
135
|
status: d.status,
|
|
136
|
+
revision: d.revision,
|
|
138
137
|
fields: d.fields,
|
|
139
138
|
})
|
|
140
139
|
}
|
|
@@ -228,15 +227,10 @@ async function run(): Promise<void> {
|
|
|
228
227
|
}
|
|
229
228
|
|
|
230
229
|
const nextFields = { ...doc.fields, [FIELD_NAME]: uploadResult.storedFile }
|
|
231
|
-
await replaceMediaVersionPreservingStatus(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
handle,
|
|
236
|
-
documentId: doc.id,
|
|
237
|
-
fields: nextFields,
|
|
238
|
-
targetStatus: doc.status,
|
|
239
|
-
})
|
|
230
|
+
await replaceMediaVersionPreservingStatus(
|
|
231
|
+
{ ...baseUploadCtx, requestContext },
|
|
232
|
+
{ documentId: doc.id, data: nextFields, expectedRevision: doc.revision }
|
|
233
|
+
)
|
|
240
234
|
|
|
241
235
|
const variantSummary =
|
|
242
236
|
uploadResult.storedFile.variants?.map((v) => `${v.name}:${v.format ?? '?'}`).join(', ') ??
|
|
@@ -246,11 +240,11 @@ async function run(): Promise<void> {
|
|
|
246
240
|
)
|
|
247
241
|
processed += 1
|
|
248
242
|
} catch (err) {
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const cleanupErrors = await deletePathsBestEffort(storage,
|
|
243
|
+
// A post-commit hook failure must retain the files referenced by the saved version.
|
|
244
|
+
// Rejected writes discard only fresh paths, preserving any reused original path.
|
|
245
|
+
const outcome = mediaFailureOutcome(err, freshPaths, oldPaths)
|
|
246
|
+
if (outcome.committed) processed += 1
|
|
247
|
+
const cleanupErrors = await deletePathsBestEffort(storage, outcome.cleanupPaths)
|
|
254
248
|
const failure = asDocumentError(doc.id, doc.path, err)
|
|
255
249
|
failures.push(
|
|
256
250
|
cleanupErrors.length === 0
|
|
@@ -260,7 +254,10 @@ async function run(): Promise<void> {
|
|
|
260
254
|
`regenerate-media: operation and rollback cleanup failed for '${doc.path}'`
|
|
261
255
|
)
|
|
262
256
|
)
|
|
263
|
-
console.error(
|
|
257
|
+
console.error(
|
|
258
|
+
` ! ${outcome.committed ? `saved revision ${outcome.revision}; hook failed` : 'fail'} ${doc.id} (${doc.path}):`,
|
|
259
|
+
err
|
|
260
|
+
)
|
|
264
261
|
}
|
|
265
262
|
}
|
|
266
263
|
|
|
@@ -11,14 +11,17 @@ import { getSystemBylineClient } from '@byline/client/server'
|
|
|
11
11
|
/** Seed the singleton slot once, without overwriting later editorial changes. */
|
|
12
12
|
export async function seedSiteSettings(): Promise<'seeded' | 'unchanged'> {
|
|
13
13
|
const settings = getSystemBylineClient().singleton('site-settings')
|
|
14
|
-
const existing = await settings.
|
|
15
|
-
if (existing
|
|
14
|
+
const existing = await settings.getForEdit()
|
|
15
|
+
if (existing?.state !== 'empty') return 'unchanged'
|
|
16
16
|
|
|
17
|
-
await settings.update(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
await settings.update(
|
|
18
|
+
{
|
|
19
|
+
siteName: 'Example site',
|
|
20
|
+
siteDescription:
|
|
21
|
+
'A concise description of this site for search results and social media previews.',
|
|
22
|
+
},
|
|
23
|
+
{ expectedState: 'empty' }
|
|
24
|
+
)
|
|
22
25
|
console.log('Seeded site settings')
|
|
23
26
|
return 'seeded'
|
|
24
27
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* listed — that list is the outstanding-translation backlog. Re-run as
|
|
17
17
|
* translation progresses; it is idempotent (each doc is its own transaction).
|
|
18
18
|
*
|
|
19
|
+
* From the application directory containing byline/:
|
|
19
20
|
* pnpm tsx byline/scripts/re-anchor.ts --to fr
|
|
20
21
|
* pnpm tsx byline/scripts/re-anchor.ts --to fr --collection pages
|
|
21
22
|
* pnpm tsx byline/scripts/re-anchor.ts --to fr --dry-run
|
|
@@ -28,7 +29,7 @@ import '../server.config.js'
|
|
|
28
29
|
|
|
29
30
|
import { parseArgs } from 'node:util'
|
|
30
31
|
|
|
31
|
-
import { getBylineCore, getServerConfig } from '@byline/core'
|
|
32
|
+
import { documentRevisionFromDatabase, getBylineCore, getServerConfig } from '@byline/core'
|
|
32
33
|
import type { PgAdapter } from '@byline/db-postgres'
|
|
33
34
|
|
|
34
35
|
const USAGE =
|
|
@@ -74,7 +75,19 @@ async function run() {
|
|
|
74
75
|
`${dryRun ? '[dry-run] ' : ''}re-anchoring ${scope} → content source locale '${targetLocale}'…`
|
|
75
76
|
)
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
// Capture each precondition before starting the batch; never refresh and retry a stale target.
|
|
79
|
+
const observed = await db.pool.query<{ document_id: string; revision: string }>(
|
|
80
|
+
`SELECT DISTINCT d.id AS document_id, d.revision
|
|
81
|
+
FROM byline_documents d JOIN byline_document_versions v ON v.document_id = d.id
|
|
82
|
+
WHERE v.is_deleted = false AND ($1::uuid IS NULL OR v.collection_id = $1::uuid)
|
|
83
|
+
ORDER BY d.id`,
|
|
84
|
+
[collectionId ?? null]
|
|
85
|
+
)
|
|
86
|
+
const targets = observed.rows.map((row) => ({
|
|
87
|
+
documentId: row.document_id,
|
|
88
|
+
expectedRevision: documentRevisionFromDatabase(row.revision),
|
|
89
|
+
}))
|
|
90
|
+
const report = await db.reAnchorDocuments({ targets, targetLocale, collectionId, dryRun })
|
|
78
91
|
|
|
79
92
|
console.log(
|
|
80
93
|
`${dryRun ? '[dry-run] would re-anchor' : '✓ re-anchored'} ${report.reanchored}/${report.total} document(s) → '${targetLocale}'`
|
|
@@ -173,6 +173,7 @@ CREATE TABLE `byline_document_publish_schedules` (
|
|
|
173
173
|
`document_id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
|
174
174
|
`collection_id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
|
175
175
|
`target_version_id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
|
176
|
+
`authorized_revision` bigint,
|
|
176
177
|
`publish_at` datetime(6) NOT NULL,
|
|
177
178
|
`state` varchar(32) NOT NULL DEFAULT 'armed',
|
|
178
179
|
`suspended_at` datetime(6),
|
|
@@ -189,8 +190,9 @@ CREATE TABLE `byline_document_publish_schedules` (
|
|
|
189
190
|
`attempt_count` int NOT NULL DEFAULT 0,
|
|
190
191
|
`last_error` text,
|
|
191
192
|
CONSTRAINT `byline_document_publish_schedules_document_id` PRIMARY KEY(`document_id`),
|
|
193
|
+
CONSTRAINT `check_publish_schedules_authorized_revision` CHECK(`byline_document_publish_schedules`.`authorized_revision` IS NULL OR `byline_document_publish_schedules`.`authorized_revision` BETWEEN 1 AND 9007199254740991),
|
|
192
194
|
CONSTRAINT `check_publish_schedules_state` CHECK(`byline_document_publish_schedules`.`state` IN ('armed', 'needs_reconfirm')),
|
|
193
|
-
CONSTRAINT `check_publish_schedules_suspended_reason` CHECK(`byline_document_publish_schedules`.`suspended_reason` IS NULL OR `byline_document_publish_schedules`.`suspended_reason`
|
|
195
|
+
CONSTRAINT `check_publish_schedules_suspended_reason` CHECK(`byline_document_publish_schedules`.`suspended_reason` IS NULL OR `byline_document_publish_schedules`.`suspended_reason` IN ('content_edited', 'document_metadata_changed', 'upgrade_invalidated'))
|
|
194
196
|
);
|
|
195
197
|
--> statement-breakpoint
|
|
196
198
|
CREATE TABLE `byline_document_relationships` (
|
|
@@ -229,10 +231,12 @@ CREATE TABLE `byline_documents` (
|
|
|
229
231
|
`collection_id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
|
230
232
|
`order_key` varchar(128) CHARACTER SET ascii COLLATE ascii_bin,
|
|
231
233
|
`source_locale` varchar(10) NOT NULL,
|
|
234
|
+
`revision` bigint NOT NULL,
|
|
232
235
|
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
233
236
|
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
234
237
|
CONSTRAINT `byline_documents_id` PRIMARY KEY(`id`),
|
|
235
|
-
CONSTRAINT `uq_documents_collection_id_id` UNIQUE(`collection_id`,`id`)
|
|
238
|
+
CONSTRAINT `uq_documents_collection_id_id` UNIQUE(`collection_id`,`id`),
|
|
239
|
+
CONSTRAINT `check_documents_revision` CHECK(`byline_documents`.`revision` BETWEEN 1 AND 9007199254740991)
|
|
236
240
|
);
|
|
237
241
|
--> statement-breakpoint
|
|
238
242
|
CREATE TABLE `byline_store_file` (
|
package/dist/templates/migrations/postgres/{0000_quiet_johnny_blaze.sql → 0000_yielding_vulture.sql}
RENAMED
|
@@ -163,6 +163,7 @@ CREATE TABLE "byline_document_publish_schedules" (
|
|
|
163
163
|
"document_id" uuid PRIMARY KEY NOT NULL,
|
|
164
164
|
"collection_id" uuid NOT NULL,
|
|
165
165
|
"target_version_id" uuid NOT NULL,
|
|
166
|
+
"authorized_revision" bigint,
|
|
166
167
|
"publish_at" timestamp (6) with time zone NOT NULL,
|
|
167
168
|
"state" varchar(32) DEFAULT 'armed' NOT NULL,
|
|
168
169
|
"suspended_at" timestamp (6) with time zone,
|
|
@@ -178,8 +179,9 @@ CREATE TABLE "byline_document_publish_schedules" (
|
|
|
178
179
|
"next_attempt_at" timestamp (6) with time zone NOT NULL,
|
|
179
180
|
"attempt_count" integer DEFAULT 0 NOT NULL,
|
|
180
181
|
"last_error" text,
|
|
182
|
+
CONSTRAINT "check_publish_schedules_authorized_revision" CHECK ("byline_document_publish_schedules"."authorized_revision" IS NULL OR "byline_document_publish_schedules"."authorized_revision" BETWEEN 1 AND 9007199254740991),
|
|
181
183
|
CONSTRAINT "check_document_publish_schedules_state" CHECK ("byline_document_publish_schedules"."state" IN ('armed', 'needs_reconfirm')),
|
|
182
|
-
CONSTRAINT "check_document_publish_schedules_suspended_reason" CHECK ("byline_document_publish_schedules"."suspended_reason" IS NULL OR "byline_document_publish_schedules"."suspended_reason"
|
|
184
|
+
CONSTRAINT "check_document_publish_schedules_suspended_reason" CHECK ("byline_document_publish_schedules"."suspended_reason" IS NULL OR "byline_document_publish_schedules"."suspended_reason" IN ('content_edited', 'document_metadata_changed', 'upgrade_invalidated'))
|
|
183
185
|
);
|
|
184
186
|
--> statement-breakpoint
|
|
185
187
|
CREATE TABLE "byline_document_relationships" (
|
|
@@ -217,9 +219,11 @@ CREATE TABLE "byline_documents" (
|
|
|
217
219
|
"collection_id" uuid NOT NULL,
|
|
218
220
|
"order_key" varchar(128) COLLATE "C",
|
|
219
221
|
"source_locale" varchar(10) NOT NULL,
|
|
222
|
+
"revision" bigint NOT NULL,
|
|
220
223
|
"created_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
|
|
221
224
|
"updated_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
|
|
222
|
-
CONSTRAINT "uq_documents_collection_id_id" UNIQUE("collection_id","id")
|
|
225
|
+
CONSTRAINT "uq_documents_collection_id_id" UNIQUE("collection_id","id"),
|
|
226
|
+
CONSTRAINT "check_documents_revision" CHECK ("byline_documents"."revision" BETWEEN 1 AND 9007199254740991)
|
|
223
227
|
);
|
|
224
228
|
--> statement-breakpoint
|
|
225
229
|
CREATE TABLE "byline_store_file" (
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/cli",
|
|
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
|
},
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"typescript": "^7.0.2",
|
|
62
62
|
"unified": "^11.0.5",
|
|
63
63
|
"vitest": "^4.1.11",
|
|
64
|
-
"@byline/core": "
|
|
64
|
+
"@byline/core": "5.0.0"
|
|
65
65
|
},
|
|
66
66
|
"publishConfig": {
|
|
67
67
|
"access": "public",
|