@byline/core 3.21.0 → 4.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/collection-types.d.ts +66 -31
- package/dist/@types/db-types.d.ts +93 -47
- package/dist/@types/field-types.d.ts +25 -1
- package/dist/@types/query-predicate.d.ts +3 -3
- package/dist/@types/search-types.d.ts +3 -4
- package/dist/@types/site-config.d.ts +51 -12
- package/dist/auth/apply-before-read.d.ts +23 -11
- package/dist/auth/apply-before-read.js +139 -33
- package/dist/auth/apply-before-read.test.node.js +241 -3
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/read-context-scope.d.ts +20 -0
- package/dist/auth/read-context-scope.js +48 -0
- package/dist/config/attach-hooks.d.ts +25 -0
- package/dist/config/attach-hooks.js +130 -0
- package/dist/config/attach-hooks.test.node.d.ts +1 -0
- package/dist/config/attach-hooks.test.node.js +173 -0
- package/dist/config/config-hooks.test.node.d.ts +1 -0
- package/dist/config/config-hooks.test.node.js +56 -0
- package/dist/config/config.d.ts +9 -5
- package/dist/config/config.js +20 -2
- package/dist/config/routes.d.ts +5 -5
- package/dist/config/routes.js +42 -9
- package/dist/config/routes.test.node.d.ts +1 -0
- package/dist/config/routes.test.node.js +152 -0
- package/dist/core.d.ts +2 -2
- package/dist/core.js +20 -14
- package/dist/core.test.node.d.ts +1 -0
- package/dist/core.test.node.js +28 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/lib/errors.d.ts +13 -0
- package/dist/lib/errors.js +14 -0
- package/dist/query/parse-where.d.ts +9 -0
- package/dist/query/parse-where.js +146 -2
- package/dist/query/parse-where.test.node.js +60 -1
- package/dist/services/collection-bootstrap.test.node.js +30 -59
- package/dist/services/discover-counter-groups.test.node.js +23 -0
- package/dist/services/document-lifecycle/audit.d.ts +22 -1
- package/dist/services/document-lifecycle/audit.js +32 -1
- package/dist/services/document-lifecycle/create.js +13 -6
- package/dist/services/document-lifecycle/delete.d.ts +17 -1
- package/dist/services/document-lifecycle/delete.js +91 -26
- package/dist/services/document-lifecycle/index.d.ts +1 -1
- package/dist/services/document-lifecycle/internals.d.ts +0 -20
- package/dist/services/document-lifecycle/internals.js +22 -46
- package/dist/services/document-lifecycle/status.d.ts +1 -1
- package/dist/services/document-lifecycle/status.js +20 -3
- package/dist/services/document-lifecycle/system-fields.d.ts +19 -6
- package/dist/services/document-lifecycle/system-fields.js +112 -74
- package/dist/services/document-lifecycle/tree.d.ts +22 -24
- package/dist/services/document-lifecycle/tree.js +244 -123
- package/dist/services/document-lifecycle/tree.test.node.d.ts +8 -0
- package/dist/services/document-lifecycle/tree.test.node.js +663 -0
- package/dist/services/document-lifecycle/update.js +2 -1
- package/dist/services/document-lifecycle.test.node.js +360 -16
- package/dist/services/document-read.d.ts +14 -6
- package/dist/services/document-read.js +47 -9
- package/dist/services/field-upload.test.node.js +70 -0
- package/dist/services/index.d.ts +2 -2
- package/dist/services/index.js +2 -2
- package/dist/services/populate.d.ts +7 -3
- package/dist/services/populate.js +180 -28
- package/dist/services/populate.test.node.js +59 -0
- package/dist/services/richtext-embed.d.ts +39 -2
- package/dist/services/richtext-embed.js +4 -34
- package/dist/services/richtext-embed.test.node.js +15 -0
- package/dist/services/richtext-populate.d.ts +20 -2
- package/dist/services/richtext-populate.js +160 -19
- package/dist/services/richtext-populate.test.node.js +523 -2
- package/dist/utils/root-relative-redirect.d.ts +6 -0
- package/dist/utils/root-relative-redirect.js +37 -0
- package/package.json +2 -2
|
@@ -17,6 +17,7 @@ import { ERR_PATH_CONFLICT, ErrorCodes } from '../../lib/errors.js';
|
|
|
17
17
|
import { generateKeyBetween } from '../../lib/fractional-index.js';
|
|
18
18
|
import { createReadContext } from '../populate.js';
|
|
19
19
|
import { embedRichTextFields } from '../richtext-embed.js';
|
|
20
|
+
import { createRichTextDocumentReader } from '../richtext-populate.js';
|
|
20
21
|
/**
|
|
21
22
|
* Matches a canonical UUID (any version). Real admin / end-user actors carry
|
|
22
23
|
* UUID ids (`uuidv7`); synthetic actors do not.
|
|
@@ -70,20 +71,38 @@ export async function applyRichTextEmbed(ctx, data) {
|
|
|
70
71
|
// `initBylineCore()` (unit tests, isolated tooling) — they have no
|
|
71
72
|
// adapter to invoke, so this is a soft no-op.
|
|
72
73
|
let embed;
|
|
74
|
+
let richTextPopulate;
|
|
75
|
+
let collections;
|
|
73
76
|
try {
|
|
74
|
-
|
|
77
|
+
const config = getServerConfig();
|
|
78
|
+
embed = config.fields?.richText?.embed;
|
|
79
|
+
richTextPopulate = config.fields?.richText?.populate;
|
|
80
|
+
collections = config.collections;
|
|
75
81
|
}
|
|
76
82
|
catch {
|
|
77
83
|
return;
|
|
78
84
|
}
|
|
79
|
-
if (embed == null)
|
|
85
|
+
if (embed == null || ctx.requestContext == null)
|
|
80
86
|
return;
|
|
87
|
+
const readContext = createReadContext();
|
|
88
|
+
const requestContext = { ...ctx.requestContext, readMode: 'published' };
|
|
81
89
|
await embedRichTextFields({
|
|
82
90
|
fields: ctx.definition.fields,
|
|
83
91
|
collectionPath: ctx.collectionPath,
|
|
84
92
|
data,
|
|
85
93
|
embed,
|
|
86
|
-
readContext
|
|
94
|
+
readContext,
|
|
95
|
+
requestContext,
|
|
96
|
+
readMode: 'published',
|
|
97
|
+
readDocuments: createRichTextDocumentReader({
|
|
98
|
+
db: ctx.db,
|
|
99
|
+
collections,
|
|
100
|
+
requestContext,
|
|
101
|
+
readContext,
|
|
102
|
+
readMode: 'published',
|
|
103
|
+
locale: ctx.defaultLocale,
|
|
104
|
+
richTextPopulate,
|
|
105
|
+
}),
|
|
87
106
|
logger: ctx.logger,
|
|
88
107
|
});
|
|
89
108
|
}
|
|
@@ -103,49 +122,6 @@ export async function maybeAppendOrderKey(ctx, collectionPath) {
|
|
|
103
122
|
});
|
|
104
123
|
return generateKeyBetween(last, null);
|
|
105
124
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Append a document as the **last root** of its `tree: true` collection's tree.
|
|
108
|
-
* Mints a fresh root-group `order_key` after the current trailing root. Used by
|
|
109
|
-
* create's auto-place and update's self-heal so a tree collection never strands a
|
|
110
|
-
* document in the "unplaced" limbo. Issues the storage command directly — the
|
|
111
|
-
* caller has already asserted the relevant ability and this is a system step.
|
|
112
|
-
*/
|
|
113
|
-
export async function appendTreeRoot(ctx, documentId) {
|
|
114
|
-
const roots = await ctx.db.queries.documents.getTreeChildren({
|
|
115
|
-
collectionId: ctx.collectionId,
|
|
116
|
-
parentDocumentId: null,
|
|
117
|
-
});
|
|
118
|
-
await ctx.db.commands.documents.placeTreeNode({
|
|
119
|
-
collectionId: ctx.collectionId,
|
|
120
|
-
documentId,
|
|
121
|
-
parentDocumentId: null,
|
|
122
|
-
beforeDocumentId: roots.at(-1)?.document_id ?? null,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Self-heal a genuinely-*unplaced* document on update: if the collection is a
|
|
127
|
-
* tree and the document has no edge row, append it as a root (mirroring create's
|
|
128
|
-
* auto-place). `getTreeParent` distinguishes unplaced from root, so an existing
|
|
129
|
-
* root or child is left exactly where it is — only strays (e.g. docs created
|
|
130
|
-
* before the flag, or whose create-time auto-place failed) are re-treed.
|
|
131
|
-
*
|
|
132
|
-
* No-op for non-tree collections. Best-effort and post-version: a failure leaves
|
|
133
|
-
* the document saved-but-unplaced and is logged, never thrown. See
|
|
134
|
-
* docs/04-collections/03-document-trees.md.
|
|
135
|
-
*/
|
|
136
|
-
export async function selfHealTreePlacement(ctx, documentId) {
|
|
137
|
-
if (ctx.definition.tree !== true)
|
|
138
|
-
return;
|
|
139
|
-
try {
|
|
140
|
-
const { placed } = await ctx.db.queries.documents.getTreeParent({ document_id: documentId });
|
|
141
|
-
if (placed)
|
|
142
|
-
return;
|
|
143
|
-
await appendTreeRoot(ctx, documentId);
|
|
144
|
-
}
|
|
145
|
-
catch (err) {
|
|
146
|
-
ctx.logger.error({ err, documentId }, 'failed to self-heal tree placement on update');
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
125
|
/** Extract `id` from the document object returned by `createDocumentVersion`. */
|
|
150
126
|
export function extractVersionId(document) {
|
|
151
127
|
return document?.id ?? document?.document_version_id ?? '';
|
|
@@ -33,7 +33,7 @@ export declare function changeDocumentStatus(ctx: DocumentLifecycleContext, para
|
|
|
33
33
|
*
|
|
34
34
|
* Flow:
|
|
35
35
|
* 1. `hooks.beforeUnpublish({ documentId, collectionPath })`
|
|
36
|
-
* 2.
|
|
36
|
+
* 2. Archive published versions and append the status audit atomically
|
|
37
37
|
* 3. `hooks.afterUnpublish({ documentId, collectionPath, archivedCount })`
|
|
38
38
|
*/
|
|
39
39
|
export declare function unpublishDocument(ctx: DocumentLifecycleContext, params: {
|
|
@@ -124,7 +124,7 @@ export async function changeDocumentStatus(ctx, params) {
|
|
|
124
124
|
*
|
|
125
125
|
* Flow:
|
|
126
126
|
* 1. `hooks.beforeUnpublish({ documentId, collectionPath })`
|
|
127
|
-
* 2.
|
|
127
|
+
* 2. Archive published versions and append the status audit atomically
|
|
128
128
|
* 3. `hooks.afterUnpublish({ documentId, collectionPath, archivedCount })`
|
|
129
129
|
*/
|
|
130
130
|
export async function unpublishDocument(ctx, params) {
|
|
@@ -153,8 +153,25 @@ export async function unpublishDocument(ctx, params) {
|
|
|
153
153
|
collectionPath,
|
|
154
154
|
path,
|
|
155
155
|
});
|
|
156
|
-
const
|
|
157
|
-
|
|
156
|
+
const audit = requireAuditCapability(db);
|
|
157
|
+
const actor = auditActor(ctx);
|
|
158
|
+
const archivedCount = await audit.withTransaction(async () => {
|
|
159
|
+
const count = await db.commands.documents.archivePublishedVersions({
|
|
160
|
+
document_id: params.documentId,
|
|
161
|
+
});
|
|
162
|
+
if (count > 0) {
|
|
163
|
+
await audit.append({
|
|
164
|
+
documentId: params.documentId,
|
|
165
|
+
collectionId,
|
|
166
|
+
actorId: actor.actorId,
|
|
167
|
+
actorRealm: actor.actorRealm,
|
|
168
|
+
action: AUDIT_ACTIONS.statusChanged,
|
|
169
|
+
field: 'status',
|
|
170
|
+
before: 'published',
|
|
171
|
+
after: 'archived',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return count;
|
|
158
175
|
});
|
|
159
176
|
await invokeHook(hooks?.afterUnpublish, {
|
|
160
177
|
documentId: params.documentId,
|
|
@@ -10,7 +10,13 @@ export interface UpdateDocumentSystemFieldsResult {
|
|
|
10
10
|
documentId: string;
|
|
11
11
|
/** The path actually written, or `undefined` when no path write occurred. */
|
|
12
12
|
path?: string;
|
|
13
|
-
/** Whether
|
|
13
|
+
/** Whether either system field actually changed. */
|
|
14
|
+
changed: boolean;
|
|
15
|
+
/** Whether a no-op request emitted the reconciliation hook. */
|
|
16
|
+
reconciliation: boolean;
|
|
17
|
+
pathChanged: boolean;
|
|
18
|
+
availableLocalesChanged: boolean;
|
|
19
|
+
/** Whether the advertised-locale set was actually rewritten this call. */
|
|
14
20
|
availableLocalesWritten: boolean;
|
|
15
21
|
}
|
|
16
22
|
/**
|
|
@@ -29,7 +35,8 @@ export interface UpdateDocumentSystemFieldsResult {
|
|
|
29
35
|
*
|
|
30
36
|
* Flow:
|
|
31
37
|
* 1. `assertActorCanPerform('update')` — same auth gate as content writes.
|
|
32
|
-
* 2.
|
|
38
|
+
* 2. Inside the audit transaction, lock the logical document and read its
|
|
39
|
+
* authoritative source locale, path, and advertised locales.
|
|
33
40
|
* 3. Path (when supplied): `resolvePathForUpdate` enforces the source-locale
|
|
34
41
|
* rule (translation-locale path edits are dropped with a warn); a real
|
|
35
42
|
* change is written via `updateDocumentPath`, mapping the unique-constraint
|
|
@@ -37,10 +44,14 @@ export interface UpdateDocumentSystemFieldsResult {
|
|
|
37
44
|
* 4. `availableLocales` (when supplied): rewritten wholesale via
|
|
38
45
|
* `setDocumentAvailableLocales`.
|
|
39
46
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
47
|
+
* Content hooks do not fire because these are not content writes. Actual
|
|
48
|
+
* changes emit `afterSystemFieldsChange` after the audited write commits. A
|
|
49
|
+
* caller can pass `reconcile: true` to emit the same hook for a no-op retry
|
|
50
|
+
* after an earlier post-commit hook failure. Hook failures reject the call but
|
|
51
|
+
* never roll back the already-committed write/audit.
|
|
52
|
+
* Accountability for these mutations is the document-grain audit log: each
|
|
53
|
+
* changed field records a `document.path.changed` /
|
|
54
|
+
* `document.locales.changed` row atomically with the write.
|
|
44
55
|
*
|
|
45
56
|
* @throws {BylineError} ERR_NOT_FOUND if the document does not exist.
|
|
46
57
|
* @throws {BylineError} ERR_PATH_CONFLICT if the path is already in use.
|
|
@@ -61,4 +72,6 @@ export declare function updateDocumentSystemFields(ctx: DocumentLifecycleContext
|
|
|
61
72
|
* included — replaces the set wholesale.
|
|
62
73
|
*/
|
|
63
74
|
availableLocales?: string[];
|
|
75
|
+
/** Re-run `afterSystemFieldsChange` when requested values are already current. */
|
|
76
|
+
reconcile?: boolean;
|
|
64
77
|
}): Promise<UpdateDocumentSystemFieldsResult>;
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
import { resolveHooks } from '../../@types/index.js';
|
|
8
9
|
import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
9
|
-
import { ERR_NOT_FOUND } from '../../lib/errors.js';
|
|
10
|
+
import { ERR_AUDIT_UNSUPPORTED, ERR_NOT_FOUND } from '../../lib/errors.js';
|
|
10
11
|
import { withLogContext } from '../../lib/logger.js';
|
|
11
12
|
import { AUDIT_ACTIONS, auditActor, requireAuditCapability, sameLocaleSet } from './audit.js';
|
|
12
|
-
import { resolvePathForUpdate, rethrowPathConflict } from './internals.js';
|
|
13
|
+
import { invokeHook, resolvePathForUpdate, rethrowPathConflict } from './internals.js';
|
|
13
14
|
/**
|
|
14
15
|
* Write a document's system-managed, document-grain fields — `path` and the
|
|
15
16
|
* editorial `availableLocales` set — **without** minting a new version or
|
|
@@ -26,7 +27,8 @@ import { resolvePathForUpdate, rethrowPathConflict } from './internals.js';
|
|
|
26
27
|
*
|
|
27
28
|
* Flow:
|
|
28
29
|
* 1. `assertActorCanPerform('update')` — same auth gate as content writes.
|
|
29
|
-
* 2.
|
|
30
|
+
* 2. Inside the audit transaction, lock the logical document and read its
|
|
31
|
+
* authoritative source locale, path, and advertised locales.
|
|
30
32
|
* 3. Path (when supplied): `resolvePathForUpdate` enforces the source-locale
|
|
31
33
|
* rule (translation-locale path edits are dropped with a warn); a real
|
|
32
34
|
* change is written via `updateDocumentPath`, mapping the unique-constraint
|
|
@@ -34,58 +36,67 @@ import { resolvePathForUpdate, rethrowPathConflict } from './internals.js';
|
|
|
34
36
|
* 4. `availableLocales` (when supplied): rewritten wholesale via
|
|
35
37
|
* `setDocumentAvailableLocales`.
|
|
36
38
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* Content hooks do not fire because these are not content writes. Actual
|
|
40
|
+
* changes emit `afterSystemFieldsChange` after the audited write commits. A
|
|
41
|
+
* caller can pass `reconcile: true` to emit the same hook for a no-op retry
|
|
42
|
+
* after an earlier post-commit hook failure. Hook failures reject the call but
|
|
43
|
+
* never roll back the already-committed write/audit.
|
|
44
|
+
* Accountability for these mutations is the document-grain audit log: each
|
|
45
|
+
* changed field records a `document.path.changed` /
|
|
46
|
+
* `document.locales.changed` row atomically with the write.
|
|
41
47
|
*
|
|
42
48
|
* @throws {BylineError} ERR_NOT_FOUND if the document does not exist.
|
|
43
49
|
* @throws {BylineError} ERR_PATH_CONFLICT if the path is already in use.
|
|
44
50
|
*/
|
|
45
51
|
export async function updateDocumentSystemFields(ctx, params) {
|
|
46
52
|
return withLogContext({ domain: 'services', module: 'lifecycle', function: 'updateDocumentSystemFields' }, async () => {
|
|
47
|
-
const { db, collectionId, collectionPath, defaultLocale } = ctx;
|
|
53
|
+
const { db, definition, collectionId, collectionPath, defaultLocale } = ctx;
|
|
48
54
|
assertActorCanPerform(ctx.requestContext, collectionPath, 'update');
|
|
49
55
|
const requestLocale = params.locale ?? defaultLocale;
|
|
50
|
-
// Resolve the document's source-locale anchor + current path. Both feed
|
|
51
|
-
// the path source-locale guard below; the fetch also asserts existence.
|
|
52
|
-
const latest = await db.queries.documents.getDocumentById({
|
|
53
|
-
collection_id: collectionId,
|
|
54
|
-
document_id: params.documentId,
|
|
55
|
-
locale: requestLocale,
|
|
56
|
-
reconstruct: true,
|
|
57
|
-
});
|
|
58
|
-
if (latest == null) {
|
|
59
|
-
throw ERR_NOT_FOUND({
|
|
60
|
-
message: 'document not found',
|
|
61
|
-
details: { documentId: params.documentId },
|
|
62
|
-
}).log(ctx.logger);
|
|
63
|
-
}
|
|
64
|
-
const originalData = latest;
|
|
65
|
-
const sourceLocale = originalData.source_locale ?? defaultLocale;
|
|
66
|
-
// Path: honour the same source-locale-only rule the versioned write
|
|
67
|
-
// uses. `resolvePathForUpdate` returns `undefined` to mean "skip the
|
|
68
|
-
// write" (null/empty override, or a translation-locale save).
|
|
69
56
|
const explicitPath = typeof params.path === 'string' && params.path.length > 0 ? params.path : null;
|
|
70
|
-
const
|
|
71
|
-
explicitPath,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
documentId: params.documentId,
|
|
76
|
-
logger: ctx.logger,
|
|
77
|
-
});
|
|
78
|
-
// Both document-grain writes and their audit rows commit atomically.
|
|
79
|
-
// These fields are non-versioned, so the version stream never records
|
|
80
|
-
// them — the audit log is their only accountability home. One audit row
|
|
81
|
-
// per field that actually changed (docs/06-auth-and-security/02-auditability.md).
|
|
82
|
-
const currentPath = originalData.path;
|
|
83
|
-
const currentLocales = originalData.availableLocales ?? [];
|
|
84
|
-
const availableLocalesWritten = params.availableLocales !== undefined;
|
|
57
|
+
const requested = {
|
|
58
|
+
path: explicitPath !== null,
|
|
59
|
+
availableLocales: params.availableLocales !== undefined,
|
|
60
|
+
};
|
|
61
|
+
const requestedLocales = params.availableLocales === undefined ? undefined : [...new Set(params.availableLocales)];
|
|
85
62
|
const audit = requireAuditCapability(db);
|
|
63
|
+
const getDocumentSystemFieldsForUpdate = db.queries.documents.getDocumentSystemFieldsForUpdate;
|
|
64
|
+
if (typeof getDocumentSystemFieldsForUpdate !== 'function') {
|
|
65
|
+
throw ERR_AUDIT_UNSUPPORTED({
|
|
66
|
+
message: 'audited system-field writes require a transaction-scoped lock/read capability',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const lockSystemFields = getDocumentSystemFieldsForUpdate.bind(db.queries.documents);
|
|
86
70
|
const actor = auditActor(ctx);
|
|
87
|
-
|
|
88
|
-
|
|
71
|
+
// The logical document row is the mutex for both document-grain fields.
|
|
72
|
+
// Reading after that lock prevents concurrent writers from auditing a
|
|
73
|
+
// stale before value or omitting an intermediate old path from the event.
|
|
74
|
+
const outcome = await audit.withTransaction(async () => {
|
|
75
|
+
const snapshot = await lockSystemFields({
|
|
76
|
+
collection_id: collectionId,
|
|
77
|
+
document_id: params.documentId,
|
|
78
|
+
});
|
|
79
|
+
if (snapshot == null) {
|
|
80
|
+
throw ERR_NOT_FOUND({
|
|
81
|
+
message: 'document not found',
|
|
82
|
+
details: { documentId: params.documentId },
|
|
83
|
+
}).log(ctx.logger);
|
|
84
|
+
}
|
|
85
|
+
const sourceLocale = snapshot.source_locale ?? defaultLocale;
|
|
86
|
+
const currentPath = snapshot.path ?? undefined;
|
|
87
|
+
const currentLocales = snapshot.availableLocales;
|
|
88
|
+
const nextLocales = requestedLocales ?? currentLocales;
|
|
89
|
+
const pathForCommand = resolvePathForUpdate({
|
|
90
|
+
explicitPath,
|
|
91
|
+
currentPath,
|
|
92
|
+
requestLocale,
|
|
93
|
+
sourceLocale,
|
|
94
|
+
documentId: params.documentId,
|
|
95
|
+
logger: ctx.logger,
|
|
96
|
+
});
|
|
97
|
+
const pathChanged = pathForCommand !== undefined && pathForCommand !== currentPath;
|
|
98
|
+
const availableLocalesChanged = requestedLocales !== undefined && !sameLocaleSet(currentLocales, nextLocales);
|
|
99
|
+
if (pathChanged) {
|
|
89
100
|
await db.commands.documents
|
|
90
101
|
.updateDocumentPath({
|
|
91
102
|
documentId: params.documentId,
|
|
@@ -93,45 +104,72 @@ export async function updateDocumentSystemFields(ctx, params) {
|
|
|
93
104
|
locale: sourceLocale,
|
|
94
105
|
path: pathForCommand,
|
|
95
106
|
})
|
|
96
|
-
.catch((err) => rethrowPathConflict(err, pathForCommand,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
}
|
|
107
|
+
.catch((err) => rethrowPathConflict(err, pathForCommand, sourceLocale));
|
|
108
|
+
await audit.append({
|
|
109
|
+
documentId: params.documentId,
|
|
110
|
+
collectionId,
|
|
111
|
+
actorId: actor.actorId,
|
|
112
|
+
actorRealm: actor.actorRealm,
|
|
113
|
+
action: AUDIT_ACTIONS.pathChanged,
|
|
114
|
+
field: 'path',
|
|
115
|
+
before: currentPath ?? null,
|
|
116
|
+
after: pathForCommand,
|
|
117
|
+
});
|
|
109
118
|
}
|
|
110
|
-
|
|
111
|
-
if (params.availableLocales !== undefined) {
|
|
119
|
+
if (availableLocalesChanged) {
|
|
112
120
|
await db.commands.documents.setDocumentAvailableLocales({
|
|
113
121
|
documentId: params.documentId,
|
|
114
122
|
collectionId,
|
|
115
|
-
availableLocales:
|
|
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,
|
|
116
134
|
});
|
|
117
|
-
if (!sameLocaleSet(currentLocales, params.availableLocales)) {
|
|
118
|
-
await audit.append({
|
|
119
|
-
documentId: params.documentId,
|
|
120
|
-
collectionId,
|
|
121
|
-
actorId: actor.actorId,
|
|
122
|
-
actorRealm: actor.actorRealm,
|
|
123
|
-
action: AUDIT_ACTIONS.localesChanged,
|
|
124
|
-
field: 'availableLocales',
|
|
125
|
-
before: currentLocales,
|
|
126
|
-
after: params.availableLocales,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
135
|
}
|
|
136
|
+
return {
|
|
137
|
+
pathForCommand,
|
|
138
|
+
pathChanged,
|
|
139
|
+
availableLocalesChanged,
|
|
140
|
+
previousPath: currentPath,
|
|
141
|
+
currentPath: pathChanged ? pathForCommand : currentPath,
|
|
142
|
+
previousAvailableLocales: [...currentLocales],
|
|
143
|
+
currentAvailableLocales: [...nextLocales],
|
|
144
|
+
};
|
|
130
145
|
});
|
|
146
|
+
const changed = outcome.pathChanged || outcome.availableLocalesChanged;
|
|
147
|
+
const reconciliation = !changed && params.reconcile === true;
|
|
148
|
+
if (changed || reconciliation) {
|
|
149
|
+
const hooks = await resolveHooks(definition);
|
|
150
|
+
await invokeHook(hooks?.afterSystemFieldsChange, {
|
|
151
|
+
documentId: params.documentId,
|
|
152
|
+
collectionPath,
|
|
153
|
+
requested,
|
|
154
|
+
changed: {
|
|
155
|
+
path: outcome.pathChanged,
|
|
156
|
+
availableLocales: outcome.availableLocalesChanged,
|
|
157
|
+
},
|
|
158
|
+
reconciliation,
|
|
159
|
+
previousPath: outcome.previousPath,
|
|
160
|
+
currentPath: outcome.currentPath,
|
|
161
|
+
previousAvailableLocales: outcome.previousAvailableLocales,
|
|
162
|
+
currentAvailableLocales: outcome.currentAvailableLocales,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
131
165
|
return {
|
|
132
166
|
documentId: params.documentId,
|
|
133
|
-
path: pathForCommand,
|
|
134
|
-
|
|
167
|
+
path: outcome.pathChanged ? outcome.pathForCommand : undefined,
|
|
168
|
+
changed,
|
|
169
|
+
reconciliation,
|
|
170
|
+
pathChanged: outcome.pathChanged,
|
|
171
|
+
availableLocalesChanged: outcome.availableLocalesChanged,
|
|
172
|
+
availableLocalesWritten: outcome.availableLocalesChanged,
|
|
135
173
|
};
|
|
136
174
|
});
|
|
137
175
|
}
|
|
@@ -5,42 +5,40 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
import { type TreeAuditCapability } from './audit.js';
|
|
9
|
+
import type { TreeDeleteMutationResult } from '../../@types/index.js';
|
|
8
10
|
import type { DocumentLifecycleContext } from './context.js';
|
|
9
|
-
|
|
10
|
-
* Place or move a node within the collection's tree (place / reorder /
|
|
11
|
-
* re-parent — one upsert). Asserts the `update` ability, performs the storage
|
|
12
|
-
* write, then fires `afterTreeChange` with the affected set. The affected-set
|
|
13
|
-
* reads are skipped entirely when the collection declares no `afterTreeChange`
|
|
14
|
-
* hook, so the event machinery adds no overhead to collections that don't
|
|
15
|
-
* consume it.
|
|
16
|
-
*/
|
|
17
|
-
export declare function placeTreeNode(ctx: DocumentLifecycleContext, params: {
|
|
11
|
+
interface PlaceParams {
|
|
18
12
|
documentId: string;
|
|
19
13
|
parentDocumentId: string | null;
|
|
20
14
|
beforeDocumentId?: string | null;
|
|
21
15
|
afterDocumentId?: string | null;
|
|
22
|
-
|
|
16
|
+
ifUnplaced?: boolean;
|
|
17
|
+
/** Re-fire the post-commit hook when the requested placement is already current. */
|
|
18
|
+
reconcile?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Place, re-parent, or reorder a node; explicit no-op retries may reconcile hooks. */
|
|
21
|
+
export declare function placeTreeNode(ctx: DocumentLifecycleContext, params: PlaceParams): Promise<{
|
|
23
22
|
orderKey: string;
|
|
24
23
|
}>;
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*/
|
|
24
|
+
/** Best-effort create/self-heal primitive that never moves an already-placed node. */
|
|
25
|
+
export declare function appendTreeRoot(ctx: DocumentLifecycleContext, documentId: string): Promise<void>;
|
|
26
|
+
/** Best-effort post-version repair; locked `ifUnplaced` closes the check/write race. */
|
|
27
|
+
export declare function selfHealTreePlacement(ctx: DocumentLifecycleContext, documentId: string): Promise<void>;
|
|
28
|
+
/** Remove a node to the unplaced state; an already-unplaced node is a true no-op. */
|
|
30
29
|
export declare function removeFromTree(ctx: DocumentLifecycleContext, params: {
|
|
31
30
|
documentId: string;
|
|
31
|
+
reconcile?: boolean;
|
|
32
32
|
}): Promise<void>;
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* (promote) and `child → cascade` (leave) foreign keys, which only fire on a
|
|
37
|
-
* *hard* row delete. Byline deletes are soft (the document row survives), so the
|
|
38
|
-
* tree must be reconciled here. Fires one `afterTreeChange` (`promote-on-delete`)
|
|
39
|
-
* covering the deleted node, the promoted children, and their subtrees.
|
|
40
|
-
*
|
|
41
|
-
* Best-effort and idempotent: a node with no children or no edge row is a no-op.
|
|
42
|
-
* Called by the document delete lifecycle for `tree: true` collections.
|
|
34
|
+
* Reconcile delete-time edges and append parent plus child-specific audit rows.
|
|
35
|
+
* The caller owns the transaction; delete uses this beside soft-delete/audit.
|
|
43
36
|
*/
|
|
37
|
+
export declare function reconcileTreeOnDeleteInTransaction(ctx: DocumentLifecycleContext, documentId: string, capability: TreeAuditCapability): Promise<TreeDeleteMutationResult>;
|
|
38
|
+
/** Fire the promotion invalidation after its transaction commits. */
|
|
39
|
+
export declare function firePromoteTreeChange(ctx: DocumentLifecycleContext, documentId: string, result: TreeDeleteMutationResult): Promise<void>;
|
|
40
|
+
/** Standalone audited reconciliation used by internal tooling and tests. */
|
|
44
41
|
export declare function promoteChildrenAndRemove(ctx: DocumentLifecycleContext, params: {
|
|
45
42
|
documentId: string;
|
|
46
43
|
}): Promise<void>;
|
|
44
|
+
export {};
|