@byline/core 3.11.2 → 3.12.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/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/core",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.12.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"sharp": "^0.34.5",
|
|
80
80
|
"uuid": "^14.0.0",
|
|
81
81
|
"zod": "^4.4.3",
|
|
82
|
-
"@byline/auth": "3.
|
|
82
|
+
"@byline/auth": "3.12.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@biomejs/biome": "2.4.15",
|
|
@@ -1,513 +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
|
-
/**
|
|
9
|
-
* Document lifecycle service.
|
|
10
|
-
*
|
|
11
|
-
* Orchestrates CRUD operations and workflow transitions, invoking collection
|
|
12
|
-
* hooks at the appropriate points. Sits between the API route layer and the
|
|
13
|
-
* storage adapter (`IDbAdapter`) so that every operation path — POST, PUT,
|
|
14
|
-
* PATCH, status change, unpublish — goes through a single set of hooks.
|
|
15
|
-
*
|
|
16
|
-
* Hook invocations run **outside** the storage transaction. They are suitable
|
|
17
|
-
* for logging, cache invalidation, webhooks, and similar side-effects.
|
|
18
|
-
*
|
|
19
|
-
* This module depends only on `@byline/core` types and utilities — it has no
|
|
20
|
-
* dependency on any specific database adapter.
|
|
21
|
-
*/
|
|
22
|
-
import type { RequestContext } from '@byline/auth';
|
|
23
|
-
import { type CollectionDefinition, type IDbAdapter, type IStorageProvider } from '../@types/index.js';
|
|
24
|
-
import { type SlugifierFn } from '../utils/slugify.js';
|
|
25
|
-
import type { BylineLogger } from '../lib/logger.js';
|
|
26
|
-
import type { DocumentPatch } from '../patches/index.js';
|
|
27
|
-
/**
|
|
28
|
-
* The shared context every lifecycle function requires. Built once per
|
|
29
|
-
* request by the API route layer and passed through.
|
|
30
|
-
*/
|
|
31
|
-
export interface DocumentLifecycleContext {
|
|
32
|
-
/** The database adapter returned by `getServerConfig().db`. */
|
|
33
|
-
db: IDbAdapter;
|
|
34
|
-
/** The resolved `CollectionDefinition` (includes `hooks`). */
|
|
35
|
-
definition: CollectionDefinition;
|
|
36
|
-
/** The database-level collection row ID. */
|
|
37
|
-
collectionId: string;
|
|
38
|
-
/**
|
|
39
|
-
* The collection's current schema version. Stamped onto every
|
|
40
|
-
* `documentVersions` row written during the lifecycle call so that
|
|
41
|
-
* Phase-2 in-memory migration can later resolve each document against
|
|
42
|
-
* the shape it was authored under. Callers resolve this from the core
|
|
43
|
-
* registry (`core.getCollectionRecord(path).version`).
|
|
44
|
-
*/
|
|
45
|
-
collectionVersion: number;
|
|
46
|
-
/** The collection `path` string (e.g. `'docs'`, `'news'`). */
|
|
47
|
-
collectionPath: string;
|
|
48
|
-
/**
|
|
49
|
-
* Storage provider for this collection. Required when the collection
|
|
50
|
-
* has any upload-capable image/file field, so that the original files
|
|
51
|
-
* and their persisted variants can be cleaned up on document deletion.
|
|
52
|
-
*
|
|
53
|
-
* Resolved by the route layer as:
|
|
54
|
-
* `field.upload?.storage ?? serverConfig.storage`
|
|
55
|
-
*
|
|
56
|
-
* Optional — callers whose collections have no upload-capable fields
|
|
57
|
-
* are unaffected.
|
|
58
|
-
*/
|
|
59
|
-
storage?: IStorageProvider;
|
|
60
|
-
/** Structured logger instance. Provided via the DI registry. */
|
|
61
|
-
logger: BylineLogger;
|
|
62
|
-
/**
|
|
63
|
-
* The default content locale (e.g. `'en'`). Used to anchor `path`
|
|
64
|
-
* derivation: the slugifier always runs against the default-locale
|
|
65
|
-
* source value, and creating a brand-new document in any other locale
|
|
66
|
-
* is rejected.
|
|
67
|
-
*
|
|
68
|
-
* Sourced by callers from `ServerConfig.i18n.content.defaultLocale`.
|
|
69
|
-
*/
|
|
70
|
-
defaultLocale: string;
|
|
71
|
-
/**
|
|
72
|
-
* Installation slugifier. When omitted, the lifecycle falls back to
|
|
73
|
-
* the default `slugify` exported from `@byline/core`.
|
|
74
|
-
*/
|
|
75
|
-
slugifier?: SlugifierFn;
|
|
76
|
-
/**
|
|
77
|
-
* Request-scoped context carrying the authenticated actor, request id,
|
|
78
|
-
* and related per-request metadata.
|
|
79
|
-
*
|
|
80
|
-
* Plumbing only in Phase 0 of the auth roadmap — present on the context
|
|
81
|
-
* so every lifecycle service can accept and forward it, but no ability
|
|
82
|
-
* assertions are performed yet. Phase 4 turns enforcement on: lifecycle
|
|
83
|
-
* entry points will call `context.requestContext?.actor?.assertAbility(...)`
|
|
84
|
-
* before any storage mutation.
|
|
85
|
-
*
|
|
86
|
-
* Optional so that internal-tooling callers (seed scripts, migration
|
|
87
|
-
* tools) continue to compile. Production write paths always supply it
|
|
88
|
-
* — `assertActorCanPerform` runs at every lifecycle entry and rejects
|
|
89
|
-
* a missing context.
|
|
90
|
-
*
|
|
91
|
-
* See docs/AUTHN-AUTHZ.md.
|
|
92
|
-
*/
|
|
93
|
-
requestContext?: RequestContext;
|
|
94
|
-
}
|
|
95
|
-
export interface CreateDocumentResult {
|
|
96
|
-
documentId: string;
|
|
97
|
-
documentVersionId: string;
|
|
98
|
-
}
|
|
99
|
-
export interface UpdateDocumentResult {
|
|
100
|
-
documentId: string;
|
|
101
|
-
documentVersionId: string;
|
|
102
|
-
}
|
|
103
|
-
export interface UpdateDocumentWithPatchesResult {
|
|
104
|
-
documentId: string;
|
|
105
|
-
documentVersionId: string;
|
|
106
|
-
}
|
|
107
|
-
export interface UpdateDocumentSystemFieldsResult {
|
|
108
|
-
documentId: string;
|
|
109
|
-
/** The path actually written, or `undefined` when no path write occurred. */
|
|
110
|
-
path?: string;
|
|
111
|
-
/** Whether the advertised-locale set was rewritten this call. */
|
|
112
|
-
availableLocalesWritten: boolean;
|
|
113
|
-
}
|
|
114
|
-
export interface ChangeStatusResult {
|
|
115
|
-
previousStatus: string;
|
|
116
|
-
newStatus: string;
|
|
117
|
-
}
|
|
118
|
-
export interface UnpublishResult {
|
|
119
|
-
archivedCount: number;
|
|
120
|
-
}
|
|
121
|
-
export interface DeleteDocumentResult {
|
|
122
|
-
deletedVersionCount: number;
|
|
123
|
-
}
|
|
124
|
-
export interface RestoreVersionResult {
|
|
125
|
-
documentId: string;
|
|
126
|
-
documentVersionId: string;
|
|
127
|
-
sourceVersionId: string;
|
|
128
|
-
}
|
|
129
|
-
export interface CopyToLocaleResult {
|
|
130
|
-
documentId: string;
|
|
131
|
-
documentVersionId: string;
|
|
132
|
-
/** Source locale read for the copy. */
|
|
133
|
-
sourceLocale: string;
|
|
134
|
-
/** Target locale into which the source's localized leaves were written. */
|
|
135
|
-
targetLocale: string;
|
|
136
|
-
/**
|
|
137
|
-
* Number of localized field values copied from source to target. Useful
|
|
138
|
-
* for UI toasts ("Copied 4 fields from EN to FR"). A zero result means
|
|
139
|
-
* the source had no localized content to copy into the target under the
|
|
140
|
-
* chosen merge rule (e.g. `overwrite: false` and target was already
|
|
141
|
-
* fully populated).
|
|
142
|
-
*/
|
|
143
|
-
fieldsUpdated: number;
|
|
144
|
-
}
|
|
145
|
-
export interface DeleteLocaleResult {
|
|
146
|
-
documentId: string;
|
|
147
|
-
/** The newly-created version that omits the deleted locale's content. */
|
|
148
|
-
documentVersionId: string;
|
|
149
|
-
/** The content locale that was removed. */
|
|
150
|
-
locale: string;
|
|
151
|
-
}
|
|
152
|
-
export interface DuplicateDocumentResult {
|
|
153
|
-
/** The newly-created document's id. */
|
|
154
|
-
documentId: string;
|
|
155
|
-
/** The newly-created version id (every duplicate starts at version 1). */
|
|
156
|
-
documentVersionId: string;
|
|
157
|
-
/** The id of the document this duplicate was cloned from. */
|
|
158
|
-
sourceDocumentId: string;
|
|
159
|
-
/**
|
|
160
|
-
* Final `path` written into `byline_document_paths` for the new document.
|
|
161
|
-
* Surfaced in the result so the UI can include it in success toasts /
|
|
162
|
-
* navigate to it directly.
|
|
163
|
-
*/
|
|
164
|
-
newPath: string;
|
|
165
|
-
/**
|
|
166
|
-
* `true` when the candidate path collided with an existing row and the
|
|
167
|
-
* lifecycle retried with a short-UUID suffix. UIs can surface a hint that
|
|
168
|
-
* the auto-generated path is uglier than usual so the editor knows to
|
|
169
|
-
* adjust it via the path widget.
|
|
170
|
-
*/
|
|
171
|
-
pathRetried: boolean;
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Create a new document.
|
|
175
|
-
*
|
|
176
|
-
* Flow:
|
|
177
|
-
* 1. Default-locale enforcement: reject if `params.locale` is anything
|
|
178
|
-
* other than the configured default content locale (a brand-new
|
|
179
|
-
* document's canonical `path` lives in the default locale).
|
|
180
|
-
* 2. `normaliseDateFields(data)`
|
|
181
|
-
* 3. `hooks.beforeCreate({ data, collectionPath })`
|
|
182
|
-
* 4. Resolve `path` — explicit `params.path` → derive via `useAsPath`
|
|
183
|
-
* → UUID fallback.
|
|
184
|
-
* 5. `db.commands.documents.createDocumentVersion(...)` (action = 'create')
|
|
185
|
-
* 6. `hooks.afterCreate({ data, collectionPath, documentId, documentVersionId })`
|
|
186
|
-
*/
|
|
187
|
-
export declare function createDocument(ctx: DocumentLifecycleContext, params: {
|
|
188
|
-
data: Record<string, any>;
|
|
189
|
-
locale?: string;
|
|
190
|
-
status?: string;
|
|
191
|
-
/**
|
|
192
|
-
* Explicit, user-supplied path (e.g. from the admin sidebar widget
|
|
193
|
-
* or an SDK caller importing legacy content). When omitted, the
|
|
194
|
-
* lifecycle derives the value from `definition.useAsPath`.
|
|
195
|
-
*/
|
|
196
|
-
path?: string;
|
|
197
|
-
/**
|
|
198
|
-
* The editorial advertised-locale set (from the admin available-locales
|
|
199
|
-
* sidebar widget). Document-grain and sticky like `path`: passed straight
|
|
200
|
-
* to the storage primitive, which replaces the document's rows wholesale.
|
|
201
|
-
* `undefined` writes nothing (a new document starts with an empty set —
|
|
202
|
-
* the safe opt-in default); `[]` clears it. See docs/I18N.md.
|
|
203
|
-
*/
|
|
204
|
-
availableLocales?: string[];
|
|
205
|
-
}): Promise<CreateDocumentResult>;
|
|
206
|
-
/**
|
|
207
|
-
* Update a document via full replacement (PUT semantics).
|
|
208
|
-
*
|
|
209
|
-
* Unlike the previous implementation, this now fetches the current version
|
|
210
|
-
* from storage to provide a real `originalData` to hooks.
|
|
211
|
-
*
|
|
212
|
-
* Flow:
|
|
213
|
-
* 1. Fetch current document via `getDocumentById({ reconstruct: true })`
|
|
214
|
-
* 2. `normaliseDateFields(data)`
|
|
215
|
-
* 3. `hooks.beforeUpdate({ data, originalData, collectionPath })`
|
|
216
|
-
* 4. `db.commands.documents.createDocumentVersion(...)` (action = 'update')
|
|
217
|
-
* 5. `hooks.afterUpdate({ data, originalData, collectionPath, documentId, documentVersionId })`
|
|
218
|
-
*/
|
|
219
|
-
export declare function updateDocument(ctx: DocumentLifecycleContext, params: {
|
|
220
|
-
documentId: string;
|
|
221
|
-
data: Record<string, any>;
|
|
222
|
-
locale?: string;
|
|
223
|
-
/**
|
|
224
|
-
* Explicit path override. When omitted, the previous version's path
|
|
225
|
-
* carries forward unchanged (sticky). The lifecycle never re-derives
|
|
226
|
-
* `path` from the source field on update — that is an explicit user
|
|
227
|
-
* action driven by the admin path widget.
|
|
228
|
-
*/
|
|
229
|
-
path?: string;
|
|
230
|
-
/**
|
|
231
|
-
* The editorial advertised-locale set. `undefined` leaves the existing
|
|
232
|
-
* set untouched (sticky — document-grain, like `path`); an explicit array
|
|
233
|
-
* (empty included) replaces it wholesale. Driven by the admin
|
|
234
|
-
* available-locales sidebar widget. See docs/I18N.md.
|
|
235
|
-
*/
|
|
236
|
-
availableLocales?: string[];
|
|
237
|
-
}): Promise<UpdateDocumentResult>;
|
|
238
|
-
/**
|
|
239
|
-
* Update a document via patch application.
|
|
240
|
-
*
|
|
241
|
-
* Flow:
|
|
242
|
-
* 1. Fetch current document via `getDocumentById({ reconstruct: true })`
|
|
243
|
-
* 2. Optimistic concurrency check on `documentVersionId`
|
|
244
|
-
* 3. `applyPatches(definition, originalData, patches)` → `nextData`
|
|
245
|
-
* 4. `normaliseDateFields(nextData)`
|
|
246
|
-
* 5. `hooks.beforeUpdate({ data: nextData, originalData, collectionPath })`
|
|
247
|
-
* 6. `db.commands.documents.createDocumentVersion(...)` (action = 'update')
|
|
248
|
-
* 7. `hooks.afterUpdate({ data: nextData, originalData, collectionPath, documentId, documentVersionId })`
|
|
249
|
-
*
|
|
250
|
-
* @throws {BylineError} ERR_CONFLICT if the supplied `documentVersionId` does not match the current version.
|
|
251
|
-
* @throws {BylineError} ERR_PATCH_FAILED if `applyPatches` fails.
|
|
252
|
-
*/
|
|
253
|
-
export declare function updateDocumentWithPatches(ctx: DocumentLifecycleContext, params: {
|
|
254
|
-
documentId: string;
|
|
255
|
-
patches: DocumentPatch[];
|
|
256
|
-
/** Client-supplied version ID for optimistic concurrency. */
|
|
257
|
-
documentVersionId?: string;
|
|
258
|
-
locale?: string;
|
|
259
|
-
/**
|
|
260
|
-
* Explicit path override (typically supplied alongside patches when
|
|
261
|
-
* the admin path widget has been edited). When omitted, sticky from
|
|
262
|
-
* the previous version.
|
|
263
|
-
*/
|
|
264
|
-
path?: string;
|
|
265
|
-
/**
|
|
266
|
-
* The editorial advertised-locale set (typically supplied alongside
|
|
267
|
-
* patches when the admin available-locales widget has been edited).
|
|
268
|
-
* `undefined` leaves the existing set untouched (sticky); an explicit
|
|
269
|
-
* array replaces it wholesale. See docs/I18N.md.
|
|
270
|
-
*/
|
|
271
|
-
availableLocales?: string[];
|
|
272
|
-
}): Promise<UpdateDocumentWithPatchesResult>;
|
|
273
|
-
/**
|
|
274
|
-
* Write a document's system-managed, document-grain fields — `path` and the
|
|
275
|
-
* editorial `availableLocales` set — **without** minting a new version or
|
|
276
|
-
* touching workflow status.
|
|
277
|
-
*
|
|
278
|
-
* These fields are document-grain (they live in `byline_document_paths` and
|
|
279
|
-
* `byline_document_available_locales`, keyed by logical document, sticky across
|
|
280
|
-
* versions), so a workflow status change would falsely imply the edit is gated
|
|
281
|
-
* behind publish. It is not: the write is immediate and applies across every
|
|
282
|
-
* version. This service backs the admin path / available-locales widgets'
|
|
283
|
-
* direct-write Save (the `direct-write` and `both` dirty-reason cases). The
|
|
284
|
-
* public *advertised* set remains the intersection of `availableLocales` with
|
|
285
|
-
* the resolved version's completeness ledger. See docs/I18N.md.
|
|
286
|
-
*
|
|
287
|
-
* Flow:
|
|
288
|
-
* 1. `assertActorCanPerform('update')` — same auth gate as content writes.
|
|
289
|
-
* 2. Fetch the document to resolve its `source_locale` anchor + current path.
|
|
290
|
-
* 3. Path (when supplied): `resolvePathForUpdate` enforces the source-locale
|
|
291
|
-
* rule (translation-locale path edits are dropped with a warn); a real
|
|
292
|
-
* change is written via `updateDocumentPath`, mapping the unique-constraint
|
|
293
|
-
* violation to `ERR_PATH_CONFLICT`.
|
|
294
|
-
* 4. `availableLocales` (when supplied): rewritten wholesale via
|
|
295
|
-
* `setDocumentAvailableLocales`.
|
|
296
|
-
*
|
|
297
|
-
* No content hooks fire — these are not content writes. Accountability for
|
|
298
|
-
* these mutations is the job of the (planned) document-grain audit log.
|
|
299
|
-
*
|
|
300
|
-
* @throws {BylineError} ERR_NOT_FOUND if the document does not exist.
|
|
301
|
-
* @throws {BylineError} ERR_PATH_CONFLICT if the path is already in use.
|
|
302
|
-
*/
|
|
303
|
-
export declare function updateDocumentSystemFields(ctx: DocumentLifecycleContext, params: {
|
|
304
|
-
documentId: string;
|
|
305
|
-
locale?: string;
|
|
306
|
-
/**
|
|
307
|
-
* Explicit path override from the path widget. `null` / empty / omitted
|
|
308
|
-
* means "no path write" (the existing row stays sticky). A non-empty
|
|
309
|
-
* string is written when the request locale is the document's source
|
|
310
|
-
* locale; on a translation locale it is dropped with a warn.
|
|
311
|
-
*/
|
|
312
|
-
path?: string | null;
|
|
313
|
-
/**
|
|
314
|
-
* The editorial advertised-locale set from the available-locales widget.
|
|
315
|
-
* `undefined` means "no advertised-locale write"; an explicit array — `[]`
|
|
316
|
-
* included — replaces the set wholesale.
|
|
317
|
-
*/
|
|
318
|
-
availableLocales?: string[];
|
|
319
|
-
}): Promise<UpdateDocumentSystemFieldsResult>;
|
|
320
|
-
/**
|
|
321
|
-
* Change a document's workflow status.
|
|
322
|
-
*
|
|
323
|
-
* Flow:
|
|
324
|
-
* 1. Fetch current document metadata
|
|
325
|
-
* 2. Validate transition via `validateStatusTransition()`
|
|
326
|
-
* 3. `hooks.beforeStatusChange({ documentId, documentVersionId, collectionPath, previousStatus, nextStatus })`
|
|
327
|
-
* 4. `db.commands.documents.setDocumentStatus(...)` — in-place mutation
|
|
328
|
-
* 5. Auto-archive: if transitioning to `'published'`, archive other published versions
|
|
329
|
-
* 6. `hooks.afterStatusChange({ documentId, documentVersionId, collectionPath, previousStatus, nextStatus })`
|
|
330
|
-
*/
|
|
331
|
-
export declare function changeDocumentStatus(ctx: DocumentLifecycleContext, params: {
|
|
332
|
-
documentId: string;
|
|
333
|
-
nextStatus: string;
|
|
334
|
-
}): Promise<ChangeStatusResult>;
|
|
335
|
-
/**
|
|
336
|
-
* Unpublish a document by archiving its published version(s).
|
|
337
|
-
*
|
|
338
|
-
* Flow:
|
|
339
|
-
* 1. `hooks.beforeUnpublish({ documentId, collectionPath })`
|
|
340
|
-
* 2. `db.commands.documents.archivePublishedVersions(...)`
|
|
341
|
-
* 3. `hooks.afterUnpublish({ documentId, collectionPath, archivedCount })`
|
|
342
|
-
*/
|
|
343
|
-
export declare function unpublishDocument(ctx: DocumentLifecycleContext, params: {
|
|
344
|
-
documentId: string;
|
|
345
|
-
}): Promise<UnpublishResult>;
|
|
346
|
-
/**
|
|
347
|
-
* Restore a historical document version as the new current version.
|
|
348
|
-
*
|
|
349
|
-
* Reads the source version with `locale: 'all'` so the entire multi-locale
|
|
350
|
-
* field tree (with `_id` / `_type` meta inlined onto blocks and array items
|
|
351
|
-
* by `reconstructFromUnifiedRows`) is captured. That tree is then re-emitted
|
|
352
|
-
* through `createDocumentVersion` with `locale: 'all'`, which produces a
|
|
353
|
-
* fresh version row with a new UUIDv7 id and the latest `created_at`. The
|
|
354
|
-
* `current_documents` view (`ROW_NUMBER() OVER PARTITION BY document_id
|
|
355
|
-
* ORDER BY created_at DESC`) automatically promotes the new row to current.
|
|
356
|
-
*
|
|
357
|
-
* Status is hard-defaulted to the workflow's first status — restoring an
|
|
358
|
-
* old `published` version must never silently re-publish content. The user
|
|
359
|
-
* runs the restored draft through the normal workflow.
|
|
360
|
-
*
|
|
361
|
-
* `path` is sticky from the previous current version (not from the source),
|
|
362
|
-
* matching the semantics of `updateDocument`. A path change made between
|
|
363
|
-
* the source and now should not be undone by the restore.
|
|
364
|
-
*
|
|
365
|
-
* Auth reuses the `update` ability — restore is conceptually an edit
|
|
366
|
-
* against an existing document.
|
|
367
|
-
*
|
|
368
|
-
* Hooks: fires `beforeUpdate` / `afterUpdate` with a `restore: { sourceVersionId }`
|
|
369
|
-
* field on the context. Userland hooks that need to react differently
|
|
370
|
-
* (e.g. tag the audit entry, skip search re-index) can branch on its
|
|
371
|
-
* presence.
|
|
372
|
-
*
|
|
373
|
-
* Flow:
|
|
374
|
-
* 1. Auth: `update` ability.
|
|
375
|
-
* 2. Read source version with `locale: 'all'`.
|
|
376
|
-
* 3. Validate source belongs to `documentId` (defence against forged
|
|
377
|
-
* cross-document version ids).
|
|
378
|
-
* 4. Read current version metadata; reject if the source IS already the
|
|
379
|
-
* current version (nothing to restore).
|
|
380
|
-
* 5. Read current document with reconstruction for hook `originalData`.
|
|
381
|
-
* 6. `hooks.beforeUpdate({ data, originalData, collectionPath, restore })`
|
|
382
|
-
* 7. `db.commands.documents.createDocumentVersion(...)` with
|
|
383
|
-
* `action: 'restore'`, `locale: 'all'`, sticky path, default status.
|
|
384
|
-
* 8. `hooks.afterUpdate({ ..., restore })`
|
|
385
|
-
*/
|
|
386
|
-
export declare function restoreDocumentVersion(ctx: DocumentLifecycleContext, params: {
|
|
387
|
-
documentId: string;
|
|
388
|
-
sourceVersionId: string;
|
|
389
|
-
}): Promise<RestoreVersionResult>;
|
|
390
|
-
/**
|
|
391
|
-
* Soft-delete a document.
|
|
392
|
-
*
|
|
393
|
-
* Marks all versions of the document as deleted (`is_deleted = true`). The
|
|
394
|
-
* `current_documents` view automatically filters deleted rows, so the
|
|
395
|
-
* document disappears from all list / page queries without physically
|
|
396
|
-
* removing data.
|
|
397
|
-
*
|
|
398
|
-
* When the collection has any upload-capable image/file field and
|
|
399
|
-
* `ctx.storage` is provided, every original file and persisted variant
|
|
400
|
-
* across those fields is also removed from storage after the DB
|
|
401
|
-
* soft-delete succeeds. Variant paths are read from the field value's
|
|
402
|
-
* `variants` array (no re-derivation from `upload.sizes`), so cleanup
|
|
403
|
-
* stays correct even if the size set changed between upload and delete.
|
|
404
|
-
* File cleanup failures are logged but are non-fatal.
|
|
405
|
-
*
|
|
406
|
-
* Flow:
|
|
407
|
-
* 1. Fetch current document (reconstruct when upload-capable fields exist)
|
|
408
|
-
* 2. `hooks.beforeDelete({ documentId, collectionPath })`
|
|
409
|
-
* 3. `db.commands.documents.softDeleteDocument({ document_id })`
|
|
410
|
-
* 4. Storage file + variant cleanup (skipped when no upload fields, non-fatal)
|
|
411
|
-
* 5. `hooks.afterDelete({ documentId, collectionPath })`
|
|
412
|
-
*/
|
|
413
|
-
export declare function deleteDocument(ctx: DocumentLifecycleContext, params: {
|
|
414
|
-
documentId: string;
|
|
415
|
-
}): Promise<DeleteDocumentResult>;
|
|
416
|
-
/**
|
|
417
|
-
* Duplicate a document, cloning all of its locales into a brand-new
|
|
418
|
-
* document atomically.
|
|
419
|
-
*
|
|
420
|
-
* Flow:
|
|
421
|
-
* 1. `assertActorCanPerform('create')` — duplicating is a create at the
|
|
422
|
-
* ability level. The source must be readable (any RBAC scoping the
|
|
423
|
-
* caller has applies via the storage read).
|
|
424
|
-
* 2. Fetch the source with `locale: 'all'` so a single read carries the
|
|
425
|
-
* full multi-locale tree forward.
|
|
426
|
-
* 3. Deep-clone the source fields; strip block / array-item `_id` meta
|
|
427
|
-
* so the new doc gets fresh identities.
|
|
428
|
-
* 4. Append `" (copy)"` to the `useAsTitle` field's value(s).
|
|
429
|
-
* 5. Derive a candidate path from the default-locale suffixed title.
|
|
430
|
-
* 6. `hooks.beforeCreate({ data, collectionPath, duplicate })`.
|
|
431
|
-
* 7. `db.commands.documents.createDocumentVersion(...)` with `locale:
|
|
432
|
-
* 'all'`, `action: 'create'`, no `documentId` → fresh document_id.
|
|
433
|
-
* On `ERR_PATH_CONFLICT` retry once with the candidate path plus a
|
|
434
|
-
* 4-char UUID suffix; bounded to two attempts, no existence
|
|
435
|
-
* pre-check, no TOCTOU race.
|
|
436
|
-
* 8. `hooks.afterCreate({ data, collectionPath, documentId,
|
|
437
|
-
* documentVersionId, duplicate })`.
|
|
438
|
-
*
|
|
439
|
-
* The write is atomic at the storage layer — a partial duplicate is
|
|
440
|
-
* structurally impossible. Editors are expected to rename both the
|
|
441
|
-
* title and the system path after the operation; the UI surfaces a
|
|
442
|
-
* confirmation modal that calls this out.
|
|
443
|
-
*/
|
|
444
|
-
export declare function duplicateDocument(ctx: DocumentLifecycleContext, params: {
|
|
445
|
-
sourceDocumentId: string;
|
|
446
|
-
}): Promise<DuplicateDocumentResult>;
|
|
447
|
-
/**
|
|
448
|
-
* Copy a document's content from one locale into another, in place on
|
|
449
|
-
* the same document.
|
|
450
|
-
*
|
|
451
|
-
* Reads the source and target locales separately (the storage layer
|
|
452
|
-
* resolves localized fields to flat single-locale shapes when given a
|
|
453
|
-
* specific `resolveLocale`). A schema-aware merge walker decides, leaf
|
|
454
|
-
* by leaf, whether to take the source's value or keep the target's,
|
|
455
|
-
* driven by the `overwrite` flag. The merged tree is written via
|
|
456
|
-
* `createDocumentVersion({ action: 'copy_to_locale', locale: target })`
|
|
457
|
-
* — the existing cross-locale carry-forward in the storage primitive
|
|
458
|
-
* preserves every *other* locale's rows untouched.
|
|
459
|
-
*
|
|
460
|
-
* Non-localized fields are never altered by this operation: they live
|
|
461
|
-
* on `locale: 'all'` rows and the merge walker passes the target's
|
|
462
|
-
* value through so the write does not blank them.
|
|
463
|
-
*
|
|
464
|
-
* Path is sticky and lives on default-locale only; this operation never
|
|
465
|
-
* touches `byline_document_paths`. Status resets to the workflow
|
|
466
|
-
* default — translations land as drafts.
|
|
467
|
-
*
|
|
468
|
-
* Flow:
|
|
469
|
-
* 1. `assertActorCanPerform('update')` — same gate as a translation save.
|
|
470
|
-
* 2. Reject if `sourceLocale === targetLocale`.
|
|
471
|
-
* 3. Fetch source via `getDocumentById({ locale: sourceLocale })`.
|
|
472
|
-
* 4. Fetch target via `getDocumentById({ locale: targetLocale })`.
|
|
473
|
-
* 5. `mergeLocaleData(definition.fields, source.fields, target.fields, overwrite)`.
|
|
474
|
-
* 6. `hooks.beforeUpdate({ data, originalData, collectionPath, copyToLocale })`.
|
|
475
|
-
* 7. `createDocumentVersion({ documentId, action: 'copy_to_locale',
|
|
476
|
-
* locale: targetLocale, documentData, previousVersionId, status })`.
|
|
477
|
-
* 8. `hooks.afterUpdate({ ..., copyToLocale })`.
|
|
478
|
-
*/
|
|
479
|
-
export declare function copyToLocale(ctx: DocumentLifecycleContext, params: {
|
|
480
|
-
documentId: string;
|
|
481
|
-
sourceLocale: string;
|
|
482
|
-
targetLocale: string;
|
|
483
|
-
overwrite: boolean;
|
|
484
|
-
}): Promise<CopyToLocaleResult>;
|
|
485
|
-
/**
|
|
486
|
-
* Remove one content locale's data from a document, in place on the same
|
|
487
|
-
* document, by writing a new immutable version that omits that locale's
|
|
488
|
-
* store rows (every other locale and all non-localized `'all'` rows are
|
|
489
|
-
* carried forward by the storage primitive).
|
|
490
|
-
*
|
|
491
|
-
* The default content locale is the document's anchor (path + source_locale)
|
|
492
|
-
* and can never be removed — rejected up front. The new version lands as the
|
|
493
|
-
* workflow's default status (a fresh draft), exactly like `copyToLocale`: the
|
|
494
|
-
* previously-published version keeps serving — including the locale being
|
|
495
|
-
* removed — until the new version is reviewed and published. The deletion is
|
|
496
|
-
* recoverable: the prior version still holds the locale, so restoring it
|
|
497
|
-
* brings the content back.
|
|
498
|
-
*
|
|
499
|
-
* Flow:
|
|
500
|
-
* 1. `assertActorCanPerform('update')` — removing a translation is an edit.
|
|
501
|
-
* 2. Reject `locale === defaultLocale`.
|
|
502
|
-
* 3. Read the document in the target locale (validates existence; supplies
|
|
503
|
-
* `originalData` for hooks and the availability set for the presence
|
|
504
|
-
* check).
|
|
505
|
-
* 4. Reject when the locale has no content to delete.
|
|
506
|
-
* 5. `hooks.beforeUpdate({ …, deleteLocale: { locale } })`.
|
|
507
|
-
* 6. `db.commands.documents.deleteDocumentLocale({ …, status: default })`.
|
|
508
|
-
* 7. `hooks.afterUpdate({ …, deleteLocale: { locale } })`.
|
|
509
|
-
*/
|
|
510
|
-
export declare function deleteLocale(ctx: DocumentLifecycleContext, params: {
|
|
511
|
-
documentId: string;
|
|
512
|
-
locale: string;
|
|
513
|
-
}): Promise<DeleteLocaleResult>;
|