@byline/core 4.19.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/@types/db-types.d.ts +32 -1
- package/dist/@types/document-revision.d.ts +92 -0
- package/dist/@types/document-revision.js +8 -0
- package/dist/@types/index.d.ts +1 -0
- package/dist/@types/index.js +1 -0
- package/dist/@types/site-config.d.ts +3 -1
- package/dist/config/attach-hooks.test.node.js +2 -1
- package/dist/config/config-hooks.test.node.js +2 -1
- package/dist/core.js +13 -1
- package/dist/core.test.node.js +27 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lib/errors.d.ts +15 -0
- package/dist/lib/errors.js +32 -0
- package/dist/lib/lock-conflict.test.node.d.ts +1 -0
- package/dist/lib/lock-conflict.test.node.js +31 -0
- package/dist/scheduler/run-due-tasks.test.node.d.ts +0 -7
- package/dist/scheduler/run-due-tasks.test.node.js +3 -2
- package/dist/scheduler/scheduled-publication.d.ts +16 -1
- package/dist/scheduler/scheduled-publication.js +27 -1
- package/dist/scheduler/scheduled-publication.test.node.d.ts +0 -7
- package/dist/scheduler/scheduled-publication.test.node.js +18 -3
- package/dist/scheduler/scheduler-boot.test.node.d.ts +0 -7
- package/dist/scheduler/scheduler-boot.test.node.js +54 -12
- package/dist/scheduler/ticker.test.node.d.ts +0 -7
- package/dist/scheduler/ticker.test.node.js +3 -2
- package/dist/scheduler/validate-scheduler-config.d.ts +2 -0
- package/dist/scheduler/validate-scheduler-config.js +5 -0
- package/dist/scheduler/validate-scheduler-config.test.node.js +16 -0
- package/dist/services/collection-bootstrap.test.node.d.ts +0 -7
- package/dist/services/collection-bootstrap.test.node.js +12 -3
- package/dist/services/discover-counter-groups.test.node.d.ts +0 -7
- package/dist/services/discover-counter-groups.test.node.js +19 -3
- package/dist/services/document-lifecycle/audit.d.ts +1 -0
- package/dist/services/document-lifecycle/audit.js +1 -0
- package/dist/services/document-lifecycle/committed-hook.d.ts +2 -1
- package/dist/services/document-lifecycle/committed-hook.js +19 -2
- package/dist/services/document-lifecycle/copy-to-locale.d.ts +2 -0
- package/dist/services/document-lifecycle/copy-to-locale.js +28 -43
- package/dist/services/document-lifecycle/create.d.ts +1 -0
- package/dist/services/document-lifecycle/create.js +4 -20
- package/dist/services/document-lifecycle/delete-locale.d.ts +4 -2
- package/dist/services/document-lifecycle/delete-locale.js +35 -37
- package/dist/services/document-lifecycle/delete.d.ts +8 -2
- package/dist/services/document-lifecycle/delete.js +24 -15
- package/dist/services/document-lifecycle/duplicate.d.ts +3 -0
- package/dist/services/document-lifecycle/duplicate.js +48 -41
- package/dist/services/document-lifecycle/index.d.ts +3 -1
- package/dist/services/document-lifecycle/index.js +3 -1
- package/dist/services/document-lifecycle/persistence.js +18 -7
- package/dist/services/document-lifecycle/publish-schedule-consistency.d.ts +2 -0
- package/dist/services/document-lifecycle/publish-schedule-consistency.js +28 -24
- package/dist/services/document-lifecycle/reorder.d.ts +11 -0
- package/dist/services/document-lifecycle/reorder.js +83 -0
- package/dist/services/document-lifecycle/restore.d.ts +2 -0
- package/dist/services/document-lifecycle/restore.js +19 -25
- package/dist/services/document-lifecycle/rethrow-path-conflict.test.node.d.ts +0 -7
- package/dist/services/document-lifecycle/rethrow-path-conflict.test.node.js +2 -1
- package/dist/services/document-lifecycle/revision-guard.d.ts +34 -0
- package/dist/services/document-lifecycle/revision-guard.js +154 -0
- package/dist/services/document-lifecycle/same-document-data.d.ts +2 -0
- package/dist/services/document-lifecycle/same-document-data.js +17 -0
- package/dist/services/document-lifecycle/scheduled-publish.d.ts +20 -4
- package/dist/services/document-lifecycle/scheduled-publish.js +146 -84
- package/dist/services/document-lifecycle/stale-document.d.ts +13 -0
- package/dist/services/document-lifecycle/stale-document.js +95 -0
- package/dist/services/document-lifecycle/stale-document.test.node.d.ts +1 -0
- package/dist/services/document-lifecycle/stale-document.test.node.js +121 -0
- package/dist/services/document-lifecycle/status-transition.test.node.d.ts +0 -7
- package/dist/services/document-lifecycle/status-transition.test.node.js +3 -2
- package/dist/services/document-lifecycle/status.d.ts +5 -21
- package/dist/services/document-lifecycle/status.js +90 -133
- package/dist/services/document-lifecycle/system-fields.d.ts +21 -0
- package/dist/services/document-lifecycle/system-fields.js +118 -116
- package/dist/services/document-lifecycle/tree.d.ts +9 -6
- package/dist/services/document-lifecycle/tree.js +65 -39
- package/dist/services/document-lifecycle/tree.test.node.d.ts +0 -7
- package/dist/services/document-lifecycle/tree.test.node.js +152 -35
- package/dist/services/document-lifecycle/update.d.ts +14 -6
- package/dist/services/document-lifecycle/update.js +101 -80
- package/dist/services/document-lifecycle.test.node.js +401 -73
- package/dist/services/field-upload.d.ts +1 -0
- package/dist/services/field-upload.js +5 -1
- package/dist/services/field-upload.test.node.d.ts +0 -7
- package/dist/services/field-upload.test.node.js +38 -2
- package/dist/services/populate.test.node.d.ts +0 -7
- package/dist/services/populate.test.node.js +19 -3
- package/dist/services/richtext-populate.test.node.d.ts +0 -7
- package/dist/services/richtext-populate.test.node.js +3 -2
- package/dist/services/singleton-lifecycle/copy-to-locale.d.ts +1 -0
- package/dist/services/singleton-lifecycle/copy-to-locale.js +9 -2
- package/dist/services/singleton-lifecycle/index.d.ts +1 -1
- package/dist/services/singleton-lifecycle/internals.d.ts +4 -6
- package/dist/services/singleton-lifecycle/internals.js +74 -77
- package/dist/services/singleton-lifecycle/restore.d.ts +1 -0
- package/dist/services/singleton-lifecycle/restore.js +3 -1
- package/dist/services/singleton-lifecycle/update.d.ts +6 -5
- package/dist/services/singleton-lifecycle/update.js +158 -91
- package/dist/services/singleton-lifecycle.test.node.d.ts +0 -7
- package/dist/services/singleton-lifecycle.test.node.js +67 -23
- package/dist/storage/db-adapter.test-helper.d.ts +26 -0
- package/dist/storage/db-adapter.test-helper.js +117 -0
- package/dist/storage/document-revision-capability.d.ts +3 -0
- package/dist/storage/document-revision-capability.js +15 -0
- package/dist/storage/document-revision-capability.test.node.d.ts +1 -0
- package/dist/storage/document-revision-capability.test.node.js +53 -0
- package/dist/storage/document-revision.d.ts +12 -0
- package/dist/storage/document-revision.js +37 -0
- package/dist/storage/document-revision.test.node.d.ts +1 -0
- package/dist/storage/document-revision.test.node.js +48 -0
- package/dist/storage/read-snapshot.d.ts +3 -0
- package/dist/storage/read-snapshot.js +55 -0
- package/dist/storage/revision-store.test-helper.d.ts +3 -0
- package/dist/storage/revision-store.test-helper.js +16 -0
- package/dist/validation/password-sign-in.test.node.d.ts +8 -0
- package/dist/validation/password-sign-in.test.node.js +21 -0
- package/dist/validation/shared.d.ts +5 -11
- package/dist/validation/shared.js +7 -1
- package/package.json +8 -8
|
@@ -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,44 +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
17
|
import { runCommittedDocumentHook } from './committed-hook.js';
|
|
18
18
|
import { applyRichTextEmbed, extractDocumentId, extractVersionId, invokeHook, resolvePathForUpdate, rethrowPathConflict, } from './internals.js';
|
|
19
19
|
import { persistExistingDocumentVersion } from './persistence.js';
|
|
20
|
+
import { commitGuardedDocumentMutation, readDocumentForMutation } from './revision-guard.js';
|
|
21
|
+
import { finishSystemFieldMutation, updateDocumentSystemFields, writeSystemFieldsInTransaction, } from './system-fields.js';
|
|
20
22
|
import { selfHealTreePlacement } from './tree.js';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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() };
|
|
35
44
|
return withLogContext({ domain: 'services', module: 'lifecycle', function: 'updateDocument' }, async () => {
|
|
36
|
-
const { db, definition,
|
|
45
|
+
const { db, definition, collectionPath, defaultLocale } = ctx;
|
|
37
46
|
assertActorCanPerform(ctx.requestContext, definition, 'update');
|
|
38
|
-
const hooks = await resolveHooks(definition);
|
|
39
47
|
const data = params.data;
|
|
40
48
|
// Fetch the real original so hooks get accurate originalData (fixes the
|
|
41
49
|
// PUT handler bug where originalData === data).
|
|
42
|
-
const latest = await
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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;
|
|
48
57
|
const originalData = latest ?? {};
|
|
49
58
|
normaliseDateFields(data);
|
|
50
59
|
normalizeNumericFields(definition.fields, data);
|
|
@@ -68,32 +77,47 @@ export async function updateDocument(ctx, params) {
|
|
|
68
77
|
// The document's own content-locale anchor governs which save writes the
|
|
69
78
|
// path row — not the mutable global default. Falls back to the global
|
|
70
79
|
// default for rows predating source_locale (not yet backfilled).
|
|
71
|
-
const sourceLocale =
|
|
80
|
+
const sourceLocale = observedSourceLocale;
|
|
72
81
|
const pathForCommand = resolvePathForUpdate({
|
|
73
82
|
explicitPath,
|
|
74
|
-
currentPath:
|
|
83
|
+
currentPath: observedPath,
|
|
75
84
|
requestLocale,
|
|
76
85
|
sourceLocale,
|
|
77
86
|
documentId: params.documentId,
|
|
78
87
|
logger: ctx.logger,
|
|
79
88
|
});
|
|
80
89
|
await applyRichTextEmbed(ctx, data);
|
|
81
|
-
const
|
|
90
|
+
const committed = await commitGuardedDocumentMutation(ctx, {
|
|
82
91
|
documentId: params.documentId,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
path: pathForCommand,
|
|
86
|
-
availableLocales: params.availableLocales,
|
|
87
|
-
status: defaultStatus,
|
|
92
|
+
expectedRevision: params.expectedRevision,
|
|
93
|
+
previousVersionId,
|
|
88
94
|
locale: requestLocale,
|
|
89
|
-
|
|
90
|
-
|
|
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;
|
|
91
117
|
const documentId = extractDocumentId(result.document) || params.documentId;
|
|
92
118
|
const documentVersionId = extractVersionId(result.document);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
await selfHealTreePlacement(ctx, documentId);
|
|
96
|
-
await runCommittedDocumentHook(ctx, { phase: 'afterUpdate', documentId, documentVersionId }, () => invokeHook(hooks?.afterUpdate, {
|
|
119
|
+
await finishSystemFieldMutation(ctx, params, { ...systemFields, documentVersionId }, revision);
|
|
120
|
+
await runCommittedDocumentHook(ctx, { phase: 'afterUpdate', documentId, documentVersionId, revision }, () => invokeHook(hooks?.afterUpdate, {
|
|
97
121
|
data,
|
|
98
122
|
originalData,
|
|
99
123
|
collectionPath,
|
|
@@ -101,7 +125,7 @@ export async function updateDocument(ctx, params) {
|
|
|
101
125
|
documentVersionId,
|
|
102
126
|
path: pathForCommand ?? originalData.path,
|
|
103
127
|
}));
|
|
104
|
-
return { documentId, documentVersionId };
|
|
128
|
+
return { documentId, documentVersionId, revision };
|
|
105
129
|
});
|
|
106
130
|
}
|
|
107
131
|
/**
|
|
@@ -109,46 +133,28 @@ export async function updateDocument(ctx, params) {
|
|
|
109
133
|
*
|
|
110
134
|
* Flow:
|
|
111
135
|
* 1. Fetch current document via `getDocumentById({ reconstruct: true })`
|
|
112
|
-
* 2.
|
|
136
|
+
* 2. Validate the observed document revision before preparation
|
|
113
137
|
* 3. `applyPatches(definition, originalData, patches)` → `nextData`
|
|
114
138
|
* 4. Normalize date and numeric fields
|
|
115
139
|
* 5. `hooks.beforeUpdate({ data: nextData, originalData, collectionPath })`, then normalize numerics again
|
|
116
140
|
* 6. `db.commands.documents.createDocumentVersion(...)` (action = 'update')
|
|
117
141
|
* 7. `hooks.afterUpdate({ data: nextData, originalData, collectionPath, documentId, documentVersionId })`
|
|
118
142
|
*
|
|
119
|
-
* @throws {BylineError}
|
|
143
|
+
* @throws {BylineError} ERR_DOCUMENT_STALE if the observed revision is stale.
|
|
120
144
|
* @throws {BylineError} ERR_PATCH_FAILED if `applyPatches` fails.
|
|
121
145
|
*/
|
|
122
146
|
export async function updateDocumentWithPatches(ctx, params) {
|
|
147
|
+
params = { ...params, availableLocales: params.availableLocales?.slice() };
|
|
123
148
|
return withLogContext({ domain: 'services', module: 'lifecycle', function: 'updateDocumentWithPatches' }, async () => {
|
|
124
|
-
const { db, definition,
|
|
149
|
+
const { db, definition, collectionPath, defaultLocale } = ctx;
|
|
125
150
|
assertActorCanPerform(ctx.requestContext, definition, 'update');
|
|
126
|
-
const hooks = await resolveHooks(definition);
|
|
127
151
|
// 1. Fetch current document.
|
|
128
|
-
const latest = await
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
});
|
|
134
|
-
if (latest == null) {
|
|
135
|
-
throw ERR_NOT_FOUND({
|
|
136
|
-
message: 'document not found',
|
|
137
|
-
details: { documentId: params.documentId },
|
|
138
|
-
}).log(ctx.logger);
|
|
139
|
-
}
|
|
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;
|
|
140
157
|
const originalData = latest;
|
|
141
|
-
// 2. Optimistic concurrency check.
|
|
142
|
-
if (params.documentVersionId &&
|
|
143
|
-
params.documentVersionId !== originalData.document_version_id) {
|
|
144
|
-
throw ERR_CONFLICT({
|
|
145
|
-
message: 'document has been modified since you loaded it',
|
|
146
|
-
details: {
|
|
147
|
-
currentVersionId: originalData.document_version_id,
|
|
148
|
-
yourVersionId: params.documentVersionId,
|
|
149
|
-
},
|
|
150
|
-
}).log(ctx.logger);
|
|
151
|
-
}
|
|
152
158
|
// 3. Apply patches (patches operate on flat field data, not the full envelope).
|
|
153
159
|
const { doc: patchedDocument, errors } = applyPatches(definition, originalData.fields ?? {}, params.patches);
|
|
154
160
|
if (errors.length > 0) {
|
|
@@ -181,33 +187,42 @@ export async function updateDocumentWithPatches(ctx, params) {
|
|
|
181
187
|
// The document's own content-locale anchor governs which save writes the
|
|
182
188
|
// path row — not the mutable global default. Falls back to the global
|
|
183
189
|
// default for rows predating source_locale (not yet backfilled).
|
|
184
|
-
const sourceLocale =
|
|
190
|
+
const sourceLocale = observedSourceLocale;
|
|
185
191
|
const pathForCommand = resolvePathForUpdate({
|
|
186
192
|
explicitPath,
|
|
187
|
-
currentPath:
|
|
193
|
+
currentPath: observedPath,
|
|
188
194
|
requestLocale,
|
|
189
195
|
sourceLocale,
|
|
190
196
|
documentId: params.documentId,
|
|
191
197
|
logger: ctx.logger,
|
|
192
198
|
});
|
|
193
199
|
await applyRichTextEmbed(ctx, nextData);
|
|
194
|
-
const
|
|
200
|
+
const committed = await commitGuardedDocumentMutation(ctx, {
|
|
195
201
|
documentId: params.documentId,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
path: pathForCommand,
|
|
199
|
-
availableLocales: params.availableLocales,
|
|
200
|
-
status: defaultStatus,
|
|
202
|
+
expectedRevision: params.expectedRevision,
|
|
203
|
+
previousVersionId,
|
|
201
204
|
locale: requestLocale,
|
|
202
|
-
|
|
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 };
|
|
203
218
|
}).catch((err) => rethrowPathConflict(db, err, pathForCommand ?? '', sourceLocale, 'update'));
|
|
219
|
+
const { result, systemFields } = committed.value;
|
|
220
|
+
const revision = committed.revision;
|
|
204
221
|
const documentId = extractDocumentId(result.document) || params.documentId;
|
|
205
222
|
const documentVersionId = extractVersionId(result.document);
|
|
206
|
-
// Self-heal: re-root a genuinely-unplaced doc in a tree collection so any
|
|
207
|
-
// save re-trees a stray (system step, best-effort, no-op when placed).
|
|
208
|
-
await selfHealTreePlacement(ctx, documentId);
|
|
209
223
|
// 7. afterUpdate hook.
|
|
210
|
-
await
|
|
224
|
+
await finishSystemFieldMutation(ctx, params, { ...systemFields, documentVersionId }, revision);
|
|
225
|
+
await runCommittedDocumentHook(ctx, { phase: 'afterUpdate', documentId, documentVersionId, revision }, () => invokeHook(hooks?.afterUpdate, {
|
|
211
226
|
data: nextData,
|
|
212
227
|
originalData,
|
|
213
228
|
collectionPath,
|
|
@@ -215,6 +230,12 @@ export async function updateDocumentWithPatches(ctx, params) {
|
|
|
215
230
|
documentVersionId,
|
|
216
231
|
path: pathForCommand ?? originalData.path,
|
|
217
232
|
}));
|
|
218
|
-
return { documentId, documentVersionId };
|
|
233
|
+
return { documentId, documentVersionId, revision };
|
|
219
234
|
});
|
|
220
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
|
+
}
|