@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
|
@@ -7,185 +7,142 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { resolveHooks } from '../../@types/index.js';
|
|
9
9
|
import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
10
|
-
import { ERR_INVALID_TRANSITION
|
|
10
|
+
import { ERR_INVALID_TRANSITION } from '../../lib/errors.js';
|
|
11
11
|
import { withLogContext } from '../../lib/logger.js';
|
|
12
12
|
import { getWorkflow, validateStatusTransition } from '../../workflow/workflow.js';
|
|
13
13
|
import { AUDIT_ACTIONS, auditActor, requireAuditCapability } from './audit.js';
|
|
14
|
+
import { runCommittedDocumentHook } from './committed-hook.js';
|
|
14
15
|
import { invokeHook } from './internals.js';
|
|
15
16
|
import { appendPublishScheduleCancellationAudit, cancelPublishScheduleInTransaction, } from './publish-schedule-consistency.js';
|
|
17
|
+
import { commitGuardedDocumentMutation, readDocumentForMutation } from './revision-guard.js';
|
|
16
18
|
import { commitDocumentStatusTransition } from './status-transition.js';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
function validateWorkflow(ctx) {
|
|
20
|
+
if (getWorkflow(ctx.definition).statuses.length <= 1)
|
|
21
|
+
throw ERR_INVALID_TRANSITION({
|
|
22
|
+
message: `collection '${ctx.collectionPath}' has a single-status workflow; status transitions are not supported`,
|
|
23
|
+
details: { collectionPath: ctx.collectionPath },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function validateTransition(ctx, previousStatus, nextStatus) {
|
|
27
|
+
validateWorkflow(ctx);
|
|
28
|
+
const result = validateStatusTransition(getWorkflow(ctx.definition), previousStatus, nextStatus);
|
|
29
|
+
if (!result.valid)
|
|
30
|
+
throw ERR_INVALID_TRANSITION({
|
|
31
|
+
message: result.reason ?? 'Invalid status transition',
|
|
32
|
+
details: { currentStatus: previousStatus, nextStatus },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
28
35
|
export async function changeDocumentStatus(ctx, params) {
|
|
29
36
|
return withLogContext({ domain: 'services', module: 'lifecycle', function: 'changeDocumentStatus' }, async () => {
|
|
37
|
+
params = { ...params };
|
|
30
38
|
const { db, definition, collectionId, collectionPath } = ctx;
|
|
31
|
-
// Every transition requires the general changeStatus ability.
|
|
32
|
-
// Transitions that target the `published` status additionally
|
|
33
|
-
// require the narrower `publish` ability — so installations can
|
|
34
|
-
// grant "move things through the workflow" without also granting
|
|
35
|
-
// "flip the final publish switch".
|
|
36
39
|
assertActorCanPerform(ctx.requestContext, definition, 'changeStatus');
|
|
37
|
-
if (params.nextStatus === 'published')
|
|
40
|
+
if (params.nextStatus === 'published')
|
|
38
41
|
assertActorCanPerform(ctx.requestContext, definition, 'publish');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// have no transitions to perform. Reject early with a clear message
|
|
42
|
-
// rather than relying on the generic ±1-step validator.
|
|
43
|
-
const workflow = getWorkflow(definition);
|
|
44
|
-
if (workflow.statuses.length <= 1) {
|
|
45
|
-
throw ERR_INVALID_TRANSITION({
|
|
46
|
-
message: `collection '${collectionPath}' has a single-status workflow; status transitions are not supported`,
|
|
47
|
-
details: { collectionPath, nextStatus: params.nextStatus },
|
|
48
|
-
}).log(ctx.logger);
|
|
49
|
-
}
|
|
50
|
-
const hooks = await resolveHooks(definition);
|
|
51
|
-
// 1. Fetch current version metadata. No field reconstruction needed —
|
|
52
|
-
// status transitions only touch the document_versions.status column.
|
|
53
|
-
const latest = await db.queries.documents.getCurrentVersionMetadata({
|
|
54
|
-
collection_id: collectionId,
|
|
55
|
-
document_id: params.documentId,
|
|
56
|
-
});
|
|
57
|
-
if (latest == null) {
|
|
58
|
-
throw ERR_NOT_FOUND({
|
|
59
|
-
message: 'document not found',
|
|
60
|
-
details: { documentId: params.documentId },
|
|
61
|
-
}).log(ctx.logger);
|
|
62
|
-
}
|
|
63
|
-
const currentStatus = latest.status ?? 'draft';
|
|
42
|
+
const latest = await readDocumentForMutation(ctx, { ...params, lenient: true });
|
|
43
|
+
const previousStatus = latest.status ?? 'draft';
|
|
64
44
|
const documentVersionId = latest.document_version_id;
|
|
65
|
-
|
|
66
|
-
const result = validateStatusTransition(workflow, currentStatus, params.nextStatus);
|
|
67
|
-
if (!result.valid) {
|
|
68
|
-
throw ERR_INVALID_TRANSITION({
|
|
69
|
-
message: result.reason ??
|
|
70
|
-
`invalid status transition from '${currentStatus}' to '${params.nextStatus}'`,
|
|
71
|
-
details: { currentStatus, nextStatus: params.nextStatus },
|
|
72
|
-
}).log(ctx.logger);
|
|
73
|
-
}
|
|
74
|
-
// Resolve the document's canonical path so the hooks can act on the
|
|
75
|
-
// specific document/URL (CDN purge, cache-key drop). Narrow lookup —
|
|
76
|
-
// getCurrentVersionMetadata deliberately omits the path subquery.
|
|
77
|
-
const path = (await ctx.db.queries.documents.getCurrentPath({
|
|
78
|
-
collection_id: collectionId,
|
|
79
|
-
document_id: params.documentId,
|
|
80
|
-
})) ?? '';
|
|
45
|
+
validateTransition(ctx, previousStatus, params.nextStatus);
|
|
81
46
|
const hookCtx = {
|
|
82
47
|
documentId: params.documentId,
|
|
83
48
|
documentVersionId,
|
|
84
49
|
collectionPath,
|
|
85
|
-
path,
|
|
86
|
-
previousStatus
|
|
50
|
+
path: latest.path ?? '',
|
|
51
|
+
previousStatus,
|
|
87
52
|
nextStatus: params.nextStatus,
|
|
88
53
|
};
|
|
89
|
-
|
|
54
|
+
const hooks = await resolveHooks(definition);
|
|
90
55
|
await invokeHook(hooks?.beforeStatusChange, hookCtx);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
56
|
+
const audit = requireAuditCapability(db);
|
|
57
|
+
const committed = await commitGuardedDocumentMutation(ctx, { ...params, previousVersionId: documentVersionId }, async (locked) => {
|
|
58
|
+
const status = locked.status ?? 'draft';
|
|
59
|
+
validateTransition(ctx, status, params.nextStatus);
|
|
60
|
+
const schedule = await cancelPublishScheduleInTransaction(ctx, params.documentId);
|
|
61
|
+
if (status !== params.nextStatus)
|
|
62
|
+
await commitDocumentStatusTransition({
|
|
63
|
+
db,
|
|
64
|
+
documentId: params.documentId,
|
|
65
|
+
documentVersionId: locked.currentVersionId,
|
|
66
|
+
collectionId,
|
|
67
|
+
previousStatus: status,
|
|
68
|
+
nextStatus: params.nextStatus,
|
|
69
|
+
actor: auditActor(ctx),
|
|
70
|
+
contributions: {
|
|
71
|
+
afterAuditAppend: () => appendPublishScheduleCancellationAudit({
|
|
72
|
+
ctx,
|
|
73
|
+
audit,
|
|
74
|
+
schedule,
|
|
75
|
+
reason: 'status_changed',
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
else
|
|
80
|
+
await appendPublishScheduleCancellationAudit({
|
|
110
81
|
ctx,
|
|
111
|
-
audit
|
|
112
|
-
schedule
|
|
82
|
+
audit,
|
|
83
|
+
schedule,
|
|
113
84
|
reason: 'status_changed',
|
|
114
|
-
})
|
|
115
|
-
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
value: { previousStatus: status, newStatus: params.nextStatus },
|
|
88
|
+
changed: status !== params.nextStatus || schedule !== null,
|
|
89
|
+
};
|
|
116
90
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
91
|
+
await runCommittedDocumentHook(ctx, {
|
|
92
|
+
phase: 'afterStatusChange',
|
|
93
|
+
documentId: params.documentId,
|
|
94
|
+
documentVersionId,
|
|
95
|
+
revision: committed.revision,
|
|
96
|
+
}, () => invokeHook(hooks?.afterStatusChange, hookCtx));
|
|
97
|
+
return { ...committed.value, documentId: params.documentId, revision: committed.revision };
|
|
120
98
|
});
|
|
121
99
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Unpublish a document by archiving its published version(s).
|
|
124
|
-
*
|
|
125
|
-
* Flow:
|
|
126
|
-
* 1. `hooks.beforeUnpublish({ documentId, collectionPath })`
|
|
127
|
-
* 2. Archive published versions and append the status audit atomically
|
|
128
|
-
* 3. `hooks.afterUnpublish({ documentId, collectionPath, archivedCount })`
|
|
129
|
-
*/
|
|
130
100
|
export async function unpublishDocument(ctx, params) {
|
|
131
101
|
return withLogContext({ domain: 'services', module: 'lifecycle', function: 'unpublishDocument' }, async () => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// changeStatus gate rather than a separate ability.
|
|
102
|
+
params = { ...params };
|
|
103
|
+
const { db, definition, collectionId, collectionPath } = ctx;
|
|
135
104
|
assertActorCanPerform(ctx.requestContext, definition, 'changeStatus');
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
message: `collection '${collectionPath}' has a single-status workflow; unpublish is not supported`,
|
|
141
|
-
details: { collectionPath },
|
|
142
|
-
}).log(ctx.logger);
|
|
143
|
-
}
|
|
105
|
+
const latest = await readDocumentForMutation(ctx, { ...params, lenient: true });
|
|
106
|
+
validateWorkflow(ctx);
|
|
107
|
+
const documentVersionId = latest.document_version_id;
|
|
108
|
+
const hookCtx = { documentId: params.documentId, collectionPath, path: latest.path ?? '' };
|
|
144
109
|
const hooks = await resolveHooks(definition);
|
|
145
|
-
|
|
146
|
-
// specific document/URL (CDN purge, cache-key drop).
|
|
147
|
-
const path = (await db.queries.documents.getCurrentPath({
|
|
148
|
-
collection_id: collectionId,
|
|
149
|
-
document_id: params.documentId,
|
|
150
|
-
})) ?? '';
|
|
151
|
-
await invokeHook(hooks?.beforeUnpublish, {
|
|
152
|
-
documentId: params.documentId,
|
|
153
|
-
collectionPath,
|
|
154
|
-
path,
|
|
155
|
-
});
|
|
110
|
+
await invokeHook(hooks?.beforeUnpublish, hookCtx);
|
|
156
111
|
const audit = requireAuditCapability(db);
|
|
157
112
|
const actor = auditActor(ctx);
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
113
|
+
const committed = await commitGuardedDocumentMutation(ctx, { ...params, previousVersionId: documentVersionId }, async () => {
|
|
114
|
+
const schedule = await cancelPublishScheduleInTransaction(ctx, params.documentId);
|
|
115
|
+
const archivedCount = await db.commands.documents.archivePublishedVersions({
|
|
161
116
|
document_id: params.documentId,
|
|
162
117
|
});
|
|
163
|
-
if (
|
|
118
|
+
if (archivedCount > 0)
|
|
164
119
|
await audit.append({
|
|
165
120
|
documentId: params.documentId,
|
|
166
121
|
collectionId,
|
|
167
|
-
|
|
168
|
-
actorRealm: actor.actorRealm,
|
|
122
|
+
...actor,
|
|
169
123
|
action: AUDIT_ACTIONS.statusChanged,
|
|
170
124
|
field: 'status',
|
|
171
125
|
before: 'published',
|
|
172
126
|
after: 'archived',
|
|
173
127
|
});
|
|
174
|
-
}
|
|
175
128
|
await appendPublishScheduleCancellationAudit({
|
|
176
129
|
ctx,
|
|
177
130
|
audit,
|
|
178
|
-
schedule
|
|
131
|
+
schedule,
|
|
179
132
|
reason: 'unpublished',
|
|
180
133
|
});
|
|
181
|
-
return
|
|
134
|
+
return { value: archivedCount, changed: archivedCount > 0 || schedule !== null };
|
|
182
135
|
});
|
|
183
|
-
await
|
|
136
|
+
await runCommittedDocumentHook(ctx, {
|
|
137
|
+
phase: 'afterUnpublish',
|
|
184
138
|
documentId: params.documentId,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
139
|
+
documentVersionId,
|
|
140
|
+
revision: committed.revision,
|
|
141
|
+
}, () => invokeHook(hooks?.afterUnpublish, { ...hookCtx, archivedCount: committed.value }));
|
|
142
|
+
return {
|
|
143
|
+
archivedCount: committed.value,
|
|
144
|
+
documentId: params.documentId,
|
|
145
|
+
revision: committed.revision,
|
|
146
|
+
};
|
|
190
147
|
});
|
|
191
148
|
}
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
import type { LockedDocumentRevision } from '../../@types/index.js';
|
|
8
9
|
import type { DocumentLifecycleContext } from './context.js';
|
|
9
10
|
export interface UpdateDocumentSystemFieldsResult {
|
|
10
11
|
documentId: string;
|
|
12
|
+
revision: number;
|
|
11
13
|
/** The path actually written, or `undefined` when no path write occurred. */
|
|
12
14
|
path?: string;
|
|
13
15
|
/** Whether either system field actually changed. */
|
|
@@ -58,6 +60,7 @@ export interface UpdateDocumentSystemFieldsResult {
|
|
|
58
60
|
*/
|
|
59
61
|
export declare function updateDocumentSystemFields(ctx: DocumentLifecycleContext, params: {
|
|
60
62
|
documentId: string;
|
|
63
|
+
expectedRevision: number;
|
|
61
64
|
locale?: string;
|
|
62
65
|
/**
|
|
63
66
|
* Explicit path override from the path widget. `null` / empty / omitted
|
|
@@ -75,3 +78,21 @@ export declare function updateDocumentSystemFields(ctx: DocumentLifecycleContext
|
|
|
75
78
|
/** Re-run `afterSystemFieldsChange` when requested values are already current. */
|
|
76
79
|
reconcile?: boolean;
|
|
77
80
|
}): Promise<UpdateDocumentSystemFieldsResult>;
|
|
81
|
+
export type SystemFieldMutationParams = Parameters<typeof updateDocumentSystemFields>[1];
|
|
82
|
+
/** Internal storage phase; caller owns the document guard and revision advancement. */
|
|
83
|
+
export declare function writeSystemFieldsInTransaction(ctx: DocumentLifecycleContext, params: SystemFieldMutationParams, locked: LockedDocumentRevision): Promise<{
|
|
84
|
+
requested: {
|
|
85
|
+
path: boolean;
|
|
86
|
+
availableLocales: boolean;
|
|
87
|
+
};
|
|
88
|
+
pathForCommand: string | undefined;
|
|
89
|
+
pathChanged: boolean;
|
|
90
|
+
availableLocalesChanged: boolean;
|
|
91
|
+
previousPath: string | undefined;
|
|
92
|
+
currentPath: string | undefined;
|
|
93
|
+
previousAvailableLocales: string[];
|
|
94
|
+
currentAvailableLocales: string[];
|
|
95
|
+
}>;
|
|
96
|
+
export declare function finishSystemFieldMutation(ctx: DocumentLifecycleContext, params: SystemFieldMutationParams, outcome: Awaited<ReturnType<typeof writeSystemFieldsInTransaction>> & {
|
|
97
|
+
documentVersionId: string;
|
|
98
|
+
}, revision: number): Promise<UpdateDocumentSystemFieldsResult>;
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { resolveHooks } from '../../@types/index.js';
|
|
9
9
|
import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
10
|
-
import { ERR_AUDIT_UNSUPPORTED, ERR_NOT_FOUND } from '../../lib/errors.js';
|
|
11
|
-
import { withLogContext } from '../../lib/logger.js';
|
|
12
10
|
import { AUDIT_ACTIONS, auditActor, requireAuditCapability, sameLocaleSet } from './audit.js';
|
|
11
|
+
import { runCommittedDocumentHook } from './committed-hook.js';
|
|
13
12
|
import { invokeHook, resolvePathForUpdate, rethrowPathConflict } from './internals.js';
|
|
13
|
+
import { suspendPublishScheduleForEdit } from './publish-schedule-consistency.js';
|
|
14
|
+
import { commitGuardedDocumentMutation } from './revision-guard.js';
|
|
14
15
|
/**
|
|
15
16
|
* Write a document's system-managed, document-grain fields — `path` and the
|
|
16
17
|
* editorial `availableLocales` set — **without** minting a new version or
|
|
@@ -49,127 +50,128 @@ import { invokeHook, resolvePathForUpdate, rethrowPathConflict } from './interna
|
|
|
49
50
|
* @throws {BylineError} ERR_PATH_CONFLICT if the path is already in use.
|
|
50
51
|
*/
|
|
51
52
|
export async function updateDocumentSystemFields(ctx, params) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
actorRealm: actor.actorRealm,
|
|
113
|
-
action: AUDIT_ACTIONS.pathChanged,
|
|
114
|
-
field: 'path',
|
|
115
|
-
before: currentPath ?? null,
|
|
116
|
-
after: pathForCommand,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
if (availableLocalesChanged) {
|
|
120
|
-
await db.commands.documents.setDocumentAvailableLocales({
|
|
121
|
-
documentId: params.documentId,
|
|
122
|
-
collectionId,
|
|
123
|
-
availableLocales: nextLocales,
|
|
124
|
-
});
|
|
125
|
-
await audit.append({
|
|
126
|
-
documentId: params.documentId,
|
|
127
|
-
collectionId,
|
|
128
|
-
actorId: actor.actorId,
|
|
129
|
-
actorRealm: actor.actorRealm,
|
|
130
|
-
action: AUDIT_ACTIONS.localesChanged,
|
|
131
|
-
field: 'availableLocales',
|
|
132
|
-
before: currentLocales,
|
|
133
|
-
after: nextLocales,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
pathForCommand,
|
|
138
|
-
pathChanged,
|
|
139
|
-
availableLocalesChanged,
|
|
140
|
-
previousPath: currentPath,
|
|
141
|
-
currentPath: pathChanged ? pathForCommand : currentPath,
|
|
142
|
-
previousAvailableLocales: [...currentLocales],
|
|
143
|
-
currentAvailableLocales: [...nextLocales],
|
|
144
|
-
};
|
|
53
|
+
params = { ...params, availableLocales: params.availableLocales?.slice() };
|
|
54
|
+
assertActorCanPerform(ctx.requestContext, ctx.definition, 'update');
|
|
55
|
+
// Locale controls path semantics, not version ancestry: metadata-only writes
|
|
56
|
+
// do not create a version and carry only a document revision precondition.
|
|
57
|
+
const committed = await commitGuardedDocumentMutation(ctx, {
|
|
58
|
+
documentId: params.documentId,
|
|
59
|
+
expectedRevision: params.expectedRevision,
|
|
60
|
+
}, async (locked) => {
|
|
61
|
+
const value = await writeSystemFieldsInTransaction(ctx, params, locked);
|
|
62
|
+
const changed = value.pathChanged || value.availableLocalesChanged;
|
|
63
|
+
if (changed)
|
|
64
|
+
await suspendPublishScheduleForEdit(ctx, requireAuditCapability(ctx.db), params.documentId, 'document_metadata_changed');
|
|
65
|
+
return { value: { ...value, documentVersionId: locked.currentVersionId ?? '' }, changed };
|
|
66
|
+
});
|
|
67
|
+
return finishSystemFieldMutation(ctx, params, committed.value, committed.revision);
|
|
68
|
+
}
|
|
69
|
+
/** Internal storage phase; caller owns the document guard and revision advancement. */
|
|
70
|
+
export async function writeSystemFieldsInTransaction(ctx, params, locked) {
|
|
71
|
+
const { db, collectionId, defaultLocale } = ctx;
|
|
72
|
+
const audit = requireAuditCapability(db);
|
|
73
|
+
const actor = auditActor(ctx);
|
|
74
|
+
const requestLocale = params.locale ?? defaultLocale;
|
|
75
|
+
const explicitPath = typeof params.path === 'string' && params.path.length > 0 ? params.path : null;
|
|
76
|
+
const requested = {
|
|
77
|
+
path: explicitPath !== null,
|
|
78
|
+
availableLocales: params.availableLocales !== undefined,
|
|
79
|
+
};
|
|
80
|
+
const requestedLocales = params.availableLocales === undefined ? undefined : [...new Set(params.availableLocales)];
|
|
81
|
+
const sourceLocale = locked.sourceLocale;
|
|
82
|
+
const currentPath = locked.path ?? undefined;
|
|
83
|
+
const currentLocales = [...locked.availableLocales];
|
|
84
|
+
const nextLocales = requestedLocales ?? currentLocales;
|
|
85
|
+
const pathForCommand = resolvePathForUpdate({
|
|
86
|
+
explicitPath,
|
|
87
|
+
currentPath,
|
|
88
|
+
requestLocale,
|
|
89
|
+
sourceLocale,
|
|
90
|
+
documentId: params.documentId,
|
|
91
|
+
logger: ctx.logger,
|
|
92
|
+
});
|
|
93
|
+
const pathChanged = pathForCommand !== undefined && pathForCommand !== currentPath;
|
|
94
|
+
const availableLocalesChanged = requestedLocales !== undefined && !sameLocaleSet(currentLocales, nextLocales);
|
|
95
|
+
if (pathChanged) {
|
|
96
|
+
await db.commands.documents
|
|
97
|
+
.updateDocumentPath({
|
|
98
|
+
documentId: params.documentId,
|
|
99
|
+
collectionId,
|
|
100
|
+
locale: sourceLocale,
|
|
101
|
+
path: pathForCommand,
|
|
102
|
+
})
|
|
103
|
+
.catch((error) => rethrowPathConflict(db, error, pathForCommand, sourceLocale, 'update'));
|
|
104
|
+
await audit.append({
|
|
105
|
+
documentId: params.documentId,
|
|
106
|
+
collectionId,
|
|
107
|
+
actorId: actor.actorId,
|
|
108
|
+
actorRealm: actor.actorRealm,
|
|
109
|
+
action: AUDIT_ACTIONS.pathChanged,
|
|
110
|
+
field: 'path',
|
|
111
|
+
before: currentPath ?? null,
|
|
112
|
+
after: pathForCommand,
|
|
145
113
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
114
|
+
}
|
|
115
|
+
if (availableLocalesChanged) {
|
|
116
|
+
await db.commands.documents.setDocumentAvailableLocales({
|
|
117
|
+
documentId: params.documentId,
|
|
118
|
+
collectionId,
|
|
119
|
+
availableLocales: nextLocales,
|
|
120
|
+
});
|
|
121
|
+
await audit.append({
|
|
122
|
+
documentId: params.documentId,
|
|
123
|
+
collectionId,
|
|
124
|
+
actorId: actor.actorId,
|
|
125
|
+
actorRealm: actor.actorRealm,
|
|
126
|
+
action: AUDIT_ACTIONS.localesChanged,
|
|
127
|
+
field: 'availableLocales',
|
|
128
|
+
before: currentLocales,
|
|
129
|
+
after: nextLocales,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
requested,
|
|
134
|
+
pathForCommand,
|
|
135
|
+
pathChanged,
|
|
136
|
+
availableLocalesChanged,
|
|
137
|
+
previousPath: currentPath,
|
|
138
|
+
currentPath: pathChanged ? pathForCommand : currentPath,
|
|
139
|
+
previousAvailableLocales: currentLocales,
|
|
140
|
+
currentAvailableLocales: nextLocales,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export async function finishSystemFieldMutation(ctx, params, outcome, revision) {
|
|
144
|
+
const changed = outcome.pathChanged || outcome.availableLocalesChanged;
|
|
145
|
+
const reconciliation = !changed && params.reconcile === true;
|
|
146
|
+
if (changed || reconciliation) {
|
|
147
|
+
await runCommittedDocumentHook(ctx, {
|
|
148
|
+
phase: 'afterSystemFieldsChange',
|
|
149
|
+
documentId: params.documentId,
|
|
150
|
+
documentVersionId: outcome.documentVersionId,
|
|
151
|
+
revision,
|
|
152
|
+
}, async () => {
|
|
153
|
+
const hooks = await resolveHooks(ctx.definition);
|
|
150
154
|
await invokeHook(hooks?.afterSystemFieldsChange, {
|
|
151
155
|
documentId: params.documentId,
|
|
152
|
-
collectionPath,
|
|
153
|
-
requested,
|
|
154
|
-
changed: {
|
|
155
|
-
path: outcome.pathChanged,
|
|
156
|
-
availableLocales: outcome.availableLocalesChanged,
|
|
157
|
-
},
|
|
156
|
+
collectionPath: ctx.collectionPath,
|
|
157
|
+
requested: outcome.requested,
|
|
158
|
+
changed: { path: outcome.pathChanged, availableLocales: outcome.availableLocalesChanged },
|
|
158
159
|
reconciliation,
|
|
159
160
|
previousPath: outcome.previousPath,
|
|
160
161
|
currentPath: outcome.currentPath,
|
|
161
162
|
previousAvailableLocales: outcome.previousAvailableLocales,
|
|
162
163
|
currentAvailableLocales: outcome.currentAvailableLocales,
|
|
163
164
|
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
documentId: params.documentId,
|
|
169
|
+
revision,
|
|
170
|
+
path: outcome.pathChanged ? outcome.pathForCommand : undefined,
|
|
171
|
+
changed,
|
|
172
|
+
reconciliation,
|
|
173
|
+
pathChanged: outcome.pathChanged,
|
|
174
|
+
availableLocalesChanged: outcome.availableLocalesChanged,
|
|
175
|
+
availableLocalesWritten: outcome.availableLocalesChanged,
|
|
176
|
+
};
|
|
175
177
|
}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import { type TreeAuditCapability } from './audit.js';
|
|
9
|
-
import type { TreeDeleteMutationResult } from '../../@types/index.js';
|
|
9
|
+
import type { StructuralMutationReceipt, TreeDeleteMutationResult } from '../../@types/index.js';
|
|
10
10
|
import type { DocumentLifecycleContext } from './context.js';
|
|
11
11
|
interface PlaceParams {
|
|
12
|
+
expectedRevision: number;
|
|
12
13
|
documentId: string;
|
|
13
14
|
parentDocumentId: string | null;
|
|
14
15
|
beforeDocumentId?: string | null;
|
|
@@ -20,16 +21,17 @@ interface PlaceParams {
|
|
|
20
21
|
/** Place, re-parent, or reorder a node; explicit no-op retries may reconcile hooks. */
|
|
21
22
|
export declare function placeTreeNode(ctx: DocumentLifecycleContext, params: PlaceParams): Promise<{
|
|
22
23
|
orderKey: string;
|
|
23
|
-
}>;
|
|
24
|
-
/**
|
|
24
|
+
} & StructuralMutationReceipt>;
|
|
25
|
+
/** Internal placement, only inside the initiating create/save transaction. */
|
|
25
26
|
export declare function appendTreeRoot(ctx: DocumentLifecycleContext, documentId: string): Promise<void>;
|
|
26
|
-
/**
|
|
27
|
+
/** Placement failure aborts the initiating save, including its version and audit. */
|
|
27
28
|
export declare function selfHealTreePlacement(ctx: DocumentLifecycleContext, documentId: string): Promise<void>;
|
|
28
29
|
/** Remove a node to the unplaced state; an already-unplaced node is a true no-op. */
|
|
29
30
|
export declare function removeFromTree(ctx: DocumentLifecycleContext, params: {
|
|
30
31
|
documentId: string;
|
|
32
|
+
expectedRevision: number;
|
|
31
33
|
reconcile?: boolean;
|
|
32
|
-
}): Promise<
|
|
34
|
+
}): Promise<StructuralMutationReceipt>;
|
|
33
35
|
/**
|
|
34
36
|
* Reconcile delete-time edges and append parent plus child-specific audit rows.
|
|
35
37
|
* The caller owns the transaction; delete uses this beside soft-delete/audit.
|
|
@@ -40,5 +42,6 @@ export declare function firePromoteTreeChange(ctx: DocumentLifecycleContext, doc
|
|
|
40
42
|
/** Standalone audited reconciliation used by internal tooling and tests. */
|
|
41
43
|
export declare function promoteChildrenAndRemove(ctx: DocumentLifecycleContext, params: {
|
|
42
44
|
documentId: string;
|
|
43
|
-
|
|
45
|
+
expectedRevision: number;
|
|
46
|
+
}): Promise<StructuralMutationReceipt>;
|
|
44
47
|
export {};
|