@byline/core 4.18.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 +38 -4
- 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 +25 -0
- package/dist/lib/errors.js +42 -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 +25 -0
- package/dist/services/document-lifecycle/committed-hook.js +96 -0
- package/dist/services/document-lifecycle/copy-to-locale.d.ts +2 -0
- package/dist/services/document-lifecycle/copy-to-locale.js +30 -44
- package/dist/services/document-lifecycle/create.d.ts +1 -0
- package/dist/services/document-lifecycle/create.js +6 -21
- package/dist/services/document-lifecycle/delete-locale.d.ts +4 -2
- package/dist/services/document-lifecycle/delete-locale.js +42 -39
- 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 +55 -43
- package/dist/services/document-lifecycle/index.d.ts +5 -1
- package/dist/services/document-lifecycle/index.js +4 -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 +21 -26
- 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 +104 -82
- package/dist/services/document-lifecycle.test.node.js +556 -70
- 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 +75 -73
- 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 +75 -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/document-version-parent.d.ts +20 -0
- package/dist/storage/document-version-parent.js +34 -0
- package/dist/storage/document-version-parent.test.node.d.ts +8 -0
- package/dist/storage/document-version-parent.test.node.js +60 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -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
|
@@ -10,10 +10,12 @@ import type { DocumentLifecycleContext } from './context.js';
|
|
|
10
10
|
export interface UpdateDocumentResult {
|
|
11
11
|
documentId: string;
|
|
12
12
|
documentVersionId: string;
|
|
13
|
+
revision: number;
|
|
13
14
|
}
|
|
14
15
|
export interface UpdateDocumentWithPatchesResult {
|
|
15
16
|
documentId: string;
|
|
16
17
|
documentVersionId: string;
|
|
18
|
+
revision: number;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* Update a document via full replacement (PUT semantics).
|
|
@@ -28,8 +30,9 @@ export interface UpdateDocumentWithPatchesResult {
|
|
|
28
30
|
* 4. `db.commands.documents.createDocumentVersion(...)` (action = 'update')
|
|
29
31
|
* 5. `hooks.afterUpdate({ data, originalData, collectionPath, documentId, documentVersionId })`
|
|
30
32
|
*/
|
|
31
|
-
|
|
33
|
+
type UpdateDocumentParams = {
|
|
32
34
|
documentId: string;
|
|
35
|
+
expectedRevision: number;
|
|
33
36
|
data: Record<string, any>;
|
|
34
37
|
locale?: string;
|
|
35
38
|
/**
|
|
@@ -46,27 +49,29 @@ export declare function updateDocument(ctx: DocumentLifecycleContext, params: {
|
|
|
46
49
|
* available-locales sidebar widget. See docs/08-internationalization/index.md.
|
|
47
50
|
*/
|
|
48
51
|
availableLocales?: string[];
|
|
49
|
-
}
|
|
52
|
+
};
|
|
53
|
+
export declare function updateDocument(ctx: DocumentLifecycleContext, params: UpdateDocumentParams): Promise<UpdateDocumentResult>;
|
|
54
|
+
/** Maintenance-only replacement; status is derived from the guarded observation. */
|
|
55
|
+
export declare function replaceDocumentFieldsPreservingStatus(ctx: DocumentLifecycleContext, params: Pick<UpdateDocumentParams, 'documentId' | 'data' | 'expectedRevision'>): Promise<UpdateDocumentResult>;
|
|
50
56
|
/**
|
|
51
57
|
* Update a document via patch application.
|
|
52
58
|
*
|
|
53
59
|
* Flow:
|
|
54
60
|
* 1. Fetch current document via `getDocumentById({ reconstruct: true })`
|
|
55
|
-
* 2.
|
|
61
|
+
* 2. Validate the observed document revision before preparation
|
|
56
62
|
* 3. `applyPatches(definition, originalData, patches)` → `nextData`
|
|
57
63
|
* 4. Normalize date and numeric fields
|
|
58
64
|
* 5. `hooks.beforeUpdate({ data: nextData, originalData, collectionPath })`, then normalize numerics again
|
|
59
65
|
* 6. `db.commands.documents.createDocumentVersion(...)` (action = 'update')
|
|
60
66
|
* 7. `hooks.afterUpdate({ data: nextData, originalData, collectionPath, documentId, documentVersionId })`
|
|
61
67
|
*
|
|
62
|
-
* @throws {BylineError}
|
|
68
|
+
* @throws {BylineError} ERR_DOCUMENT_STALE if the observed revision is stale.
|
|
63
69
|
* @throws {BylineError} ERR_PATCH_FAILED if `applyPatches` fails.
|
|
64
70
|
*/
|
|
65
71
|
export declare function updateDocumentWithPatches(ctx: DocumentLifecycleContext, params: {
|
|
66
72
|
documentId: string;
|
|
73
|
+
expectedRevision: number;
|
|
67
74
|
patches: DocumentPatch[];
|
|
68
|
-
/** Client-supplied version ID for optimistic concurrency. */
|
|
69
|
-
documentVersionId?: string;
|
|
70
75
|
locale?: string;
|
|
71
76
|
/**
|
|
72
77
|
* Explicit path override (typically supplied alongside patches when
|
|
@@ -82,3 +87,6 @@ export declare function updateDocumentWithPatches(ctx: DocumentLifecycleContext,
|
|
|
82
87
|
*/
|
|
83
88
|
availableLocales?: string[];
|
|
84
89
|
}): Promise<UpdateDocumentWithPatchesResult>;
|
|
90
|
+
/** Combined patch/content and optional metadata save, with one guarded commit. */
|
|
91
|
+
export declare function saveDocument(ctx: DocumentLifecycleContext, params: Parameters<typeof updateDocumentWithPatches>[1]): Promise<import("./system-fields.js").UpdateDocumentSystemFieldsResult | UpdateDocumentWithPatchesResult>;
|
|
92
|
+
export {};
|
|
@@ -7,43 +7,53 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { resolveHooks } from '../../@types/index.js';
|
|
9
9
|
import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
10
|
-
import {
|
|
10
|
+
import { ERR_PATCH_FAILED, ERR_VALIDATION } from '../../lib/errors.js';
|
|
11
11
|
import { withLogContext } from '../../lib/logger.js';
|
|
12
12
|
import { applyPatches } from '../../patches/index.js';
|
|
13
13
|
import { normaliseDateFields } from '../../utils/normalise-dates.js';
|
|
14
|
-
import { getDefaultStatus } from '../../workflow/workflow.js';
|
|
14
|
+
import { getDefaultStatus, getWorkflowStatuses } from '../../workflow/workflow.js';
|
|
15
15
|
import { assignCounterValues } from '../assign-counter-values.js';
|
|
16
16
|
import { normalizeNumericFields } from '../normalize-numeric-fields.js';
|
|
17
|
+
import { runCommittedDocumentHook } from './committed-hook.js';
|
|
17
18
|
import { applyRichTextEmbed, extractDocumentId, extractVersionId, invokeHook, resolvePathForUpdate, rethrowPathConflict, } from './internals.js';
|
|
18
19
|
import { persistExistingDocumentVersion } from './persistence.js';
|
|
20
|
+
import { commitGuardedDocumentMutation, readDocumentForMutation } from './revision-guard.js';
|
|
21
|
+
import { finishSystemFieldMutation, updateDocumentSystemFields, writeSystemFieldsInTransaction, } from './system-fields.js';
|
|
19
22
|
import { selfHealTreePlacement } from './tree.js';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
export function updateDocument(ctx, params) {
|
|
24
|
+
return replaceFields(ctx, params, 'editorial');
|
|
25
|
+
}
|
|
26
|
+
/** Maintenance-only replacement; status is derived from the guarded observation. */
|
|
27
|
+
export async function replaceDocumentFieldsPreservingStatus(ctx, params) {
|
|
28
|
+
assertActorCanPerform(ctx.requestContext, ctx.definition, 'update');
|
|
29
|
+
ctx.requestContext?.actor?.assertAbility('system.documentMaintenance');
|
|
30
|
+
return replaceFields(ctx, { ...params, locale: 'all' }, 'preserve');
|
|
31
|
+
}
|
|
32
|
+
function preservedStatus(ctx, status) {
|
|
33
|
+
if (status === null || !getWorkflowStatuses(ctx.definition).some((item) => item.name === status))
|
|
34
|
+
throw ERR_VALIDATION({
|
|
35
|
+
message: 'The observed status is no longer declared by this collection',
|
|
36
|
+
details: { status },
|
|
37
|
+
});
|
|
38
|
+
if (status === 'published')
|
|
39
|
+
assertActorCanPerform(ctx.requestContext, ctx.definition, 'publish');
|
|
40
|
+
return status;
|
|
41
|
+
}
|
|
42
|
+
async function replaceFields(ctx, params, policy) {
|
|
43
|
+
params = { ...params, availableLocales: params.availableLocales?.slice() };
|
|
34
44
|
return withLogContext({ domain: 'services', module: 'lifecycle', function: 'updateDocument' }, async () => {
|
|
35
|
-
const { db, definition,
|
|
45
|
+
const { db, definition, collectionPath, defaultLocale } = ctx;
|
|
36
46
|
assertActorCanPerform(ctx.requestContext, definition, 'update');
|
|
37
|
-
const hooks = await resolveHooks(definition);
|
|
38
47
|
const data = params.data;
|
|
39
48
|
// Fetch the real original so hooks get accurate originalData (fixes the
|
|
40
49
|
// PUT handler bug where originalData === data).
|
|
41
|
-
const latest = await
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
const latest = await readDocumentForMutation(ctx, params);
|
|
51
|
+
if (policy === 'preserve')
|
|
52
|
+
preservedStatus(ctx, latest.status);
|
|
53
|
+
const hooks = await resolveHooks(definition);
|
|
54
|
+
const previousVersionId = latest.document_version_id;
|
|
55
|
+
const observedPath = latest.path;
|
|
56
|
+
const observedSourceLocale = latest.source_locale ?? defaultLocale;
|
|
47
57
|
const originalData = latest ?? {};
|
|
48
58
|
normaliseDateFields(data);
|
|
49
59
|
normalizeNumericFields(definition.fields, data);
|
|
@@ -67,40 +77,55 @@ export async function updateDocument(ctx, params) {
|
|
|
67
77
|
// The document's own content-locale anchor governs which save writes the
|
|
68
78
|
// path row — not the mutable global default. Falls back to the global
|
|
69
79
|
// default for rows predating source_locale (not yet backfilled).
|
|
70
|
-
const sourceLocale =
|
|
80
|
+
const sourceLocale = observedSourceLocale;
|
|
71
81
|
const pathForCommand = resolvePathForUpdate({
|
|
72
82
|
explicitPath,
|
|
73
|
-
currentPath:
|
|
83
|
+
currentPath: observedPath,
|
|
74
84
|
requestLocale,
|
|
75
85
|
sourceLocale,
|
|
76
86
|
documentId: params.documentId,
|
|
77
87
|
logger: ctx.logger,
|
|
78
88
|
});
|
|
79
89
|
await applyRichTextEmbed(ctx, data);
|
|
80
|
-
const
|
|
90
|
+
const committed = await commitGuardedDocumentMutation(ctx, {
|
|
81
91
|
documentId: params.documentId,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
path: pathForCommand,
|
|
85
|
-
availableLocales: params.availableLocales,
|
|
86
|
-
status: defaultStatus,
|
|
92
|
+
expectedRevision: params.expectedRevision,
|
|
93
|
+
previousVersionId,
|
|
87
94
|
locale: requestLocale,
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
}, async (locked) => {
|
|
96
|
+
const systemFields = await writeSystemFieldsInTransaction(ctx, params, locked);
|
|
97
|
+
const result = await persistExistingDocumentVersion(ctx, {
|
|
98
|
+
documentId: params.documentId,
|
|
99
|
+
action: 'update',
|
|
100
|
+
documentData: data,
|
|
101
|
+
path: pathForCommand,
|
|
102
|
+
status: policy === 'preserve' ? preservedStatus(ctx, locked.status) : defaultStatus,
|
|
103
|
+
locale: requestLocale,
|
|
104
|
+
previousVersionId,
|
|
105
|
+
});
|
|
106
|
+
if (policy === 'preserve' && locked.status === 'published') {
|
|
107
|
+
await db.commands.documents.archivePublishedVersions({
|
|
108
|
+
document_id: params.documentId,
|
|
109
|
+
excludeVersionId: extractVersionId(result.document),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
await selfHealTreePlacement(ctx, params.documentId);
|
|
113
|
+
return { value: { result, systemFields }, changed: true };
|
|
114
|
+
}, policy === 'preserve' ? { collectionLock: 'exclusive' } : {}).catch((err) => rethrowPathConflict(db, err, pathForCommand ?? '', sourceLocale, 'update'));
|
|
115
|
+
const { result, systemFields } = committed.value;
|
|
116
|
+
const revision = committed.revision;
|
|
90
117
|
const documentId = extractDocumentId(result.document) || params.documentId;
|
|
91
118
|
const documentVersionId = extractVersionId(result.document);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await selfHealTreePlacement(ctx, documentId);
|
|
95
|
-
await invokeHook(hooks?.afterUpdate, {
|
|
119
|
+
await finishSystemFieldMutation(ctx, params, { ...systemFields, documentVersionId }, revision);
|
|
120
|
+
await runCommittedDocumentHook(ctx, { phase: 'afterUpdate', documentId, documentVersionId, revision }, () => invokeHook(hooks?.afterUpdate, {
|
|
96
121
|
data,
|
|
97
122
|
originalData,
|
|
98
123
|
collectionPath,
|
|
99
124
|
documentId,
|
|
100
125
|
documentVersionId,
|
|
101
126
|
path: pathForCommand ?? originalData.path,
|
|
102
|
-
});
|
|
103
|
-
return { documentId, documentVersionId };
|
|
127
|
+
}));
|
|
128
|
+
return { documentId, documentVersionId, revision };
|
|
104
129
|
});
|
|
105
130
|
}
|
|
106
131
|
/**
|
|
@@ -108,46 +133,28 @@ export async function updateDocument(ctx, params) {
|
|
|
108
133
|
*
|
|
109
134
|
* Flow:
|
|
110
135
|
* 1. Fetch current document via `getDocumentById({ reconstruct: true })`
|
|
111
|
-
* 2.
|
|
136
|
+
* 2. Validate the observed document revision before preparation
|
|
112
137
|
* 3. `applyPatches(definition, originalData, patches)` → `nextData`
|
|
113
138
|
* 4. Normalize date and numeric fields
|
|
114
139
|
* 5. `hooks.beforeUpdate({ data: nextData, originalData, collectionPath })`, then normalize numerics again
|
|
115
140
|
* 6. `db.commands.documents.createDocumentVersion(...)` (action = 'update')
|
|
116
141
|
* 7. `hooks.afterUpdate({ data: nextData, originalData, collectionPath, documentId, documentVersionId })`
|
|
117
142
|
*
|
|
118
|
-
* @throws {BylineError}
|
|
143
|
+
* @throws {BylineError} ERR_DOCUMENT_STALE if the observed revision is stale.
|
|
119
144
|
* @throws {BylineError} ERR_PATCH_FAILED if `applyPatches` fails.
|
|
120
145
|
*/
|
|
121
146
|
export async function updateDocumentWithPatches(ctx, params) {
|
|
147
|
+
params = { ...params, availableLocales: params.availableLocales?.slice() };
|
|
122
148
|
return withLogContext({ domain: 'services', module: 'lifecycle', function: 'updateDocumentWithPatches' }, async () => {
|
|
123
|
-
const { db, definition,
|
|
149
|
+
const { db, definition, collectionPath, defaultLocale } = ctx;
|
|
124
150
|
assertActorCanPerform(ctx.requestContext, definition, 'update');
|
|
125
|
-
const hooks = await resolveHooks(definition);
|
|
126
151
|
// 1. Fetch current document.
|
|
127
|
-
const latest = await
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
if (latest == null) {
|
|
134
|
-
throw ERR_NOT_FOUND({
|
|
135
|
-
message: 'document not found',
|
|
136
|
-
details: { documentId: params.documentId },
|
|
137
|
-
}).log(ctx.logger);
|
|
138
|
-
}
|
|
152
|
+
const latest = await readDocumentForMutation(ctx, params);
|
|
153
|
+
const hooks = await resolveHooks(definition);
|
|
154
|
+
const previousVersionId = latest.document_version_id;
|
|
155
|
+
const observedPath = latest.path;
|
|
156
|
+
const observedSourceLocale = latest.source_locale ?? defaultLocale;
|
|
139
157
|
const originalData = latest;
|
|
140
|
-
// 2. Optimistic concurrency check.
|
|
141
|
-
if (params.documentVersionId &&
|
|
142
|
-
params.documentVersionId !== originalData.document_version_id) {
|
|
143
|
-
throw ERR_CONFLICT({
|
|
144
|
-
message: 'document has been modified since you loaded it',
|
|
145
|
-
details: {
|
|
146
|
-
currentVersionId: originalData.document_version_id,
|
|
147
|
-
yourVersionId: params.documentVersionId,
|
|
148
|
-
},
|
|
149
|
-
}).log(ctx.logger);
|
|
150
|
-
}
|
|
151
158
|
// 3. Apply patches (patches operate on flat field data, not the full envelope).
|
|
152
159
|
const { doc: patchedDocument, errors } = applyPatches(definition, originalData.fields ?? {}, params.patches);
|
|
153
160
|
if (errors.length > 0) {
|
|
@@ -180,40 +187,55 @@ export async function updateDocumentWithPatches(ctx, params) {
|
|
|
180
187
|
// The document's own content-locale anchor governs which save writes the
|
|
181
188
|
// path row — not the mutable global default. Falls back to the global
|
|
182
189
|
// default for rows predating source_locale (not yet backfilled).
|
|
183
|
-
const sourceLocale =
|
|
190
|
+
const sourceLocale = observedSourceLocale;
|
|
184
191
|
const pathForCommand = resolvePathForUpdate({
|
|
185
192
|
explicitPath,
|
|
186
|
-
currentPath:
|
|
193
|
+
currentPath: observedPath,
|
|
187
194
|
requestLocale,
|
|
188
195
|
sourceLocale,
|
|
189
196
|
documentId: params.documentId,
|
|
190
197
|
logger: ctx.logger,
|
|
191
198
|
});
|
|
192
199
|
await applyRichTextEmbed(ctx, nextData);
|
|
193
|
-
const
|
|
200
|
+
const committed = await commitGuardedDocumentMutation(ctx, {
|
|
194
201
|
documentId: params.documentId,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
path: pathForCommand,
|
|
198
|
-
availableLocales: params.availableLocales,
|
|
199
|
-
status: defaultStatus,
|
|
202
|
+
expectedRevision: params.expectedRevision,
|
|
203
|
+
previousVersionId,
|
|
200
204
|
locale: requestLocale,
|
|
201
|
-
|
|
205
|
+
}, async (locked) => {
|
|
206
|
+
const systemFields = await writeSystemFieldsInTransaction(ctx, params, locked);
|
|
207
|
+
const result = await persistExistingDocumentVersion(ctx, {
|
|
208
|
+
documentId: params.documentId,
|
|
209
|
+
action: 'update',
|
|
210
|
+
documentData: nextData,
|
|
211
|
+
path: pathForCommand,
|
|
212
|
+
status: defaultStatus,
|
|
213
|
+
locale: requestLocale,
|
|
214
|
+
previousVersionId,
|
|
215
|
+
});
|
|
216
|
+
await selfHealTreePlacement(ctx, params.documentId);
|
|
217
|
+
return { value: { result, systemFields }, changed: true };
|
|
202
218
|
}).catch((err) => rethrowPathConflict(db, err, pathForCommand ?? '', sourceLocale, 'update'));
|
|
219
|
+
const { result, systemFields } = committed.value;
|
|
220
|
+
const revision = committed.revision;
|
|
203
221
|
const documentId = extractDocumentId(result.document) || params.documentId;
|
|
204
222
|
const documentVersionId = extractVersionId(result.document);
|
|
205
|
-
// Self-heal: re-root a genuinely-unplaced doc in a tree collection so any
|
|
206
|
-
// save re-trees a stray (system step, best-effort, no-op when placed).
|
|
207
|
-
await selfHealTreePlacement(ctx, documentId);
|
|
208
223
|
// 7. afterUpdate hook.
|
|
209
|
-
await
|
|
224
|
+
await finishSystemFieldMutation(ctx, params, { ...systemFields, documentVersionId }, revision);
|
|
225
|
+
await runCommittedDocumentHook(ctx, { phase: 'afterUpdate', documentId, documentVersionId, revision }, () => invokeHook(hooks?.afterUpdate, {
|
|
210
226
|
data: nextData,
|
|
211
227
|
originalData,
|
|
212
228
|
collectionPath,
|
|
213
229
|
documentId,
|
|
214
230
|
documentVersionId,
|
|
215
231
|
path: pathForCommand ?? originalData.path,
|
|
216
|
-
});
|
|
217
|
-
return { documentId, documentVersionId };
|
|
232
|
+
}));
|
|
233
|
+
return { documentId, documentVersionId, revision };
|
|
218
234
|
});
|
|
219
235
|
}
|
|
236
|
+
/** Combined patch/content and optional metadata save, with one guarded commit. */
|
|
237
|
+
export async function saveDocument(ctx, params) {
|
|
238
|
+
if (params.patches.length === 0)
|
|
239
|
+
return updateDocumentSystemFields(ctx, params);
|
|
240
|
+
return updateDocumentWithPatches(ctx, params);
|
|
241
|
+
}
|