@byline/core 3.20.4 → 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 +72 -31
- package/dist/@types/db-types.d.ts +93 -47
- package/dist/@types/field-data-types.d.ts +8 -10
- package/dist/@types/field-data-types.js +1 -1
- package/dist/@types/field-data-types.test.node.d.ts +1 -0
- package/dist/@types/field-data-types.test.node.js +70 -0
- package/dist/@types/field-types.d.ts +25 -1
- package/dist/@types/index.d.ts +1 -0
- package/dist/@types/index.js +1 -0
- package/dist/@types/query-predicate.d.ts +3 -3
- package/dist/@types/relation-types.d.ts +38 -0
- package/dist/@types/relation-types.js +8 -0
- package/dist/@types/search-types.d.ts +3 -4
- package/dist/@types/site-config.d.ts +52 -13
- 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/codegen/fixtures/all-fields.d.ts +342 -0
- package/dist/codegen/fixtures/all-fields.expected.d.ts +120 -0
- package/dist/codegen/fixtures/all-fields.expected.js +1 -0
- package/dist/codegen/fixtures/all-fields.js +94 -0
- package/dist/codegen/index.d.ts +16 -0
- package/dist/codegen/index.js +431 -0
- package/dist/codegen/index.test.node.d.ts +1 -0
- package/dist/codegen/index.test.node.js +230 -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 +3 -3
- 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 +10 -1
- package/dist/query/parse-where.js +146 -2
- package/dist/query/parse-where.test.node.js +60 -1
- package/dist/schemas/zod/builder.js +7 -3
- package/dist/schemas/zod/builder.test.node.d.ts +1 -0
- package/dist/schemas/zod/builder.test.node.js +49 -0
- package/dist/services/collection-bootstrap.d.ts +1 -1
- package/dist/services/collection-bootstrap.test.node.js +30 -59
- package/dist/services/discover-counter-groups.d.ts +1 -1
- 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.d.ts +2 -2
- package/dist/services/document-lifecycle/create.js +18 -8
- 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.d.ts +4 -4
- package/dist/services/document-lifecycle/update.js +11 -5
- package/dist/services/document-lifecycle.test.node.js +426 -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 +3 -2
- package/dist/services/index.js +3 -2
- package/dist/services/normalize-numeric-fields.d.ts +23 -0
- package/dist/services/normalize-numeric-fields.js +89 -0
- package/dist/services/normalize-numeric-fields.test.node.d.ts +8 -0
- package/dist/services/normalize-numeric-fields.test.node.js +99 -0
- package/dist/services/populate.d.ts +9 -23
- 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 +21 -3
- package/dist/services/richtext-populate.js +160 -19
- package/dist/services/richtext-populate.test.node.js +523 -2
- package/dist/services/validate-search-config.d.ts +1 -1
- package/dist/storage/collection-fingerprint.js +6 -0
- package/dist/storage/collection-fingerprint.test.node.js +21 -0
- package/dist/utils/root-relative-redirect.d.ts +6 -0
- package/dist/utils/root-relative-redirect.js +37 -0
- package/package.json +7 -2
|
@@ -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 {};
|
|
@@ -5,47 +5,40 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
9
|
-
* Document-tree lifecycle service — the unversioned structural mutations for
|
|
10
|
-
* `tree: true` collections (docs/04-collections/03-document-trees.md). Wraps the storage adapter's
|
|
11
|
-
* tree commands so that, like the versioned lifecycle services, they enforce the
|
|
12
|
-
* actor ability and fire a collection hook. Tree writes mint no document version
|
|
13
|
-
* and touch no user fields, so the `afterTreeChange` hook is the *only*
|
|
14
|
-
* invalidation signal for them.
|
|
15
|
-
*
|
|
16
|
-
* The hook payload is the **affected set** — not a single node — because one
|
|
17
|
-
* structural change ripples: the moved node, its descendants (breadcrumb trails
|
|
18
|
-
* changed), the old and new parents, and both sibling lists. Computed with a few
|
|
19
|
-
* cheap tree reads around the write and emitted as one batched event.
|
|
20
|
-
*/
|
|
8
|
+
/** Atomic, audited lifecycle orchestration for document-tree mutations. */
|
|
21
9
|
import { resolveHooks } from '../../@types/index.js';
|
|
22
10
|
import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
11
|
+
import { ERR_TREE_HOOK_COMMITTED, TREE_HOOK_COMMITTED_MARKER } from '../../lib/errors.js';
|
|
23
12
|
import { withLogContext } from '../../lib/logger.js';
|
|
13
|
+
import { AUDIT_ACTIONS, auditActor, requireTreeAuditCapability, } from './audit.js';
|
|
24
14
|
import { invokeHook } from './internals.js';
|
|
25
|
-
/**
|
|
26
|
-
async function immediateParentId(ctx, documentId) {
|
|
27
|
-
const ancestors = await ctx.db.queries.documents.getTreeAncestors({ document_id: documentId });
|
|
28
|
-
// `getTreeAncestors` is root-first, so the last entry (depth 1) is the parent.
|
|
29
|
-
return ancestors.length > 0 ? (ancestors[ancestors.length - 1]?.document_id ?? null) : null;
|
|
30
|
-
}
|
|
31
|
-
/** Document ids of a node and all its descendants (status-agnostic). */
|
|
15
|
+
/** Document ids of a node and descendants, read only for registered hooks. */
|
|
32
16
|
async function subtreeIds(ctx, rootDocumentId) {
|
|
33
17
|
const rows = await ctx.db.queries.documents.getTreeSubtree({
|
|
34
18
|
collectionId: ctx.collectionId,
|
|
35
19
|
rootDocumentId,
|
|
36
20
|
readMode: 'any',
|
|
37
21
|
});
|
|
38
|
-
return rows.map((
|
|
22
|
+
return rows.map((row) => row.document_id);
|
|
23
|
+
}
|
|
24
|
+
/** Coarse collection-wide affected set for explicit no-op reconciliation. */
|
|
25
|
+
async function wholeTreeIds(ctx) {
|
|
26
|
+
const rows = await ctx.db.queries.documents.getTreeSubtree({
|
|
27
|
+
collectionId: ctx.collectionId,
|
|
28
|
+
rootDocumentId: null,
|
|
29
|
+
readMode: 'any',
|
|
30
|
+
});
|
|
31
|
+
return rows.map((row) => row.document_id);
|
|
39
32
|
}
|
|
40
|
-
/** Document ids of
|
|
33
|
+
/** Document ids of one sibling group, read only for registered hooks. */
|
|
41
34
|
async function childIds(ctx, parentDocumentId) {
|
|
42
35
|
const rows = await ctx.db.queries.documents.getTreeChildren({
|
|
43
36
|
collectionId: ctx.collectionId,
|
|
44
37
|
parentDocumentId,
|
|
45
38
|
});
|
|
46
|
-
return rows.map((
|
|
39
|
+
return rows.map((row) => row.document_id);
|
|
47
40
|
}
|
|
48
|
-
/**
|
|
41
|
+
/** Invoke `afterTreeChange` after the transaction has committed. */
|
|
49
42
|
async function fireTreeChange(ctx, definition, event) {
|
|
50
43
|
const hooks = await resolveHooks(definition);
|
|
51
44
|
if (hooks?.afterTreeChange == null)
|
|
@@ -57,128 +50,256 @@ async function fireTreeChange(ctx, definition, event) {
|
|
|
57
50
|
affectedDocumentIds: [...new Set(event.affectedDocumentIds)],
|
|
58
51
|
});
|
|
59
52
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
53
|
+
/** Preserve rejection semantics while exposing that tree data already committed. */
|
|
54
|
+
async function runCommittedTreeHook(event, callback) {
|
|
55
|
+
try {
|
|
56
|
+
await callback();
|
|
57
|
+
}
|
|
58
|
+
catch (cause) {
|
|
59
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
60
|
+
throw ERR_TREE_HOOK_COMMITTED({
|
|
61
|
+
message: `${TREE_HOOK_COMMITTED_MARKER} tree mutation committed but afterTreeChange failed: ${message}`,
|
|
62
|
+
cause,
|
|
63
|
+
details: event,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function placementAction(before, parentDocumentId) {
|
|
68
|
+
if (!before.placed)
|
|
69
|
+
return AUDIT_ACTIONS.treePlaced;
|
|
70
|
+
if (before.parentDocumentId !== parentDocumentId)
|
|
71
|
+
return AUDIT_ACTIONS.treeReparented;
|
|
72
|
+
return AUDIT_ACTIONS.treeReordered;
|
|
73
|
+
}
|
|
74
|
+
/** Append one locked place/reparent/reorder result to the audit log. */
|
|
75
|
+
async function appendPlacementAudit(ctx, capability, params, mutation) {
|
|
76
|
+
const actor = auditActor(ctx);
|
|
77
|
+
await capability.append({
|
|
78
|
+
documentId: params.documentId,
|
|
79
|
+
collectionId: ctx.collectionId,
|
|
80
|
+
actorId: actor.actorId,
|
|
81
|
+
actorRealm: actor.actorRealm,
|
|
82
|
+
action: placementAction(mutation.before, params.parentDocumentId),
|
|
83
|
+
field: 'tree',
|
|
84
|
+
before: mutation.before,
|
|
85
|
+
after: {
|
|
86
|
+
...mutation.after,
|
|
87
|
+
beforeDocumentId: params.beforeDocumentId ?? null,
|
|
88
|
+
afterDocumentId: params.afterDocumentId ?? null,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Locked storage inspection/mutation and audit append in one transaction. */
|
|
93
|
+
async function auditedPlace(ctx, params) {
|
|
94
|
+
const capability = requireTreeAuditCapability(ctx.db);
|
|
95
|
+
let mutation;
|
|
96
|
+
await capability.withTransaction(async () => {
|
|
97
|
+
mutation = await capability.place({
|
|
84
98
|
collectionId: ctx.collectionId,
|
|
85
99
|
documentId: params.documentId,
|
|
86
100
|
parentDocumentId: params.parentDocumentId,
|
|
87
101
|
beforeDocumentId: params.beforeDocumentId ?? null,
|
|
88
102
|
afterDocumentId: params.afterDocumentId ?? null,
|
|
103
|
+
ifUnplaced: params.ifUnplaced,
|
|
89
104
|
});
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
if (mutation.changed)
|
|
106
|
+
await appendPlacementAudit(ctx, capability, params, mutation);
|
|
107
|
+
});
|
|
108
|
+
return mutation;
|
|
109
|
+
}
|
|
110
|
+
/** Place, re-parent, or reorder a node; explicit no-op retries may reconcile hooks. */
|
|
111
|
+
export async function placeTreeNode(ctx, params) {
|
|
112
|
+
return withLogContext({ domain: 'services', module: 'tree', function: 'placeTreeNode' }, async () => {
|
|
113
|
+
assertActorCanPerform(ctx.requestContext, ctx.collectionPath, 'update');
|
|
114
|
+
const hooks = await resolveHooks(ctx.definition);
|
|
115
|
+
const wantsEvent = hooks?.afterTreeChange != null;
|
|
116
|
+
const mutation = await auditedPlace(ctx, params);
|
|
117
|
+
const orderKey = mutation.after.orderKey;
|
|
118
|
+
if (orderKey == null)
|
|
119
|
+
throw new Error('placed tree mutation did not return an order key');
|
|
120
|
+
if (!mutation.changed) {
|
|
121
|
+
if (wantsEvent && params.reconcile === true) {
|
|
122
|
+
await runCommittedTreeHook({ change: 'place', documentId: params.documentId }, async () => {
|
|
123
|
+
await fireTreeChange(ctx, ctx.definition, {
|
|
124
|
+
change: 'place',
|
|
125
|
+
documentId: params.documentId,
|
|
126
|
+
affectedDocumentIds: await wholeTreeIds(ctx),
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { orderKey };
|
|
131
|
+
}
|
|
132
|
+
if (wantsEvent) {
|
|
133
|
+
await runCommittedTreeHook({ change: 'place', documentId: params.documentId }, async () => {
|
|
134
|
+
const [subtree, newSiblings] = await Promise.all([
|
|
135
|
+
subtreeIds(ctx, params.documentId),
|
|
136
|
+
childIds(ctx, params.parentDocumentId),
|
|
137
|
+
]);
|
|
138
|
+
await fireTreeChange(ctx, ctx.definition, {
|
|
139
|
+
change: 'place',
|
|
140
|
+
documentId: params.documentId,
|
|
141
|
+
affectedDocumentIds: [
|
|
142
|
+
...subtree,
|
|
143
|
+
...(mutation.before.parentDocumentId ? [mutation.before.parentDocumentId] : []),
|
|
144
|
+
...(params.parentDocumentId ? [params.parentDocumentId] : []),
|
|
145
|
+
...mutation.beforeSiblingDocumentIds,
|
|
146
|
+
...newSiblings,
|
|
147
|
+
],
|
|
148
|
+
});
|
|
103
149
|
});
|
|
104
150
|
}
|
|
105
|
-
return
|
|
151
|
+
return { orderKey };
|
|
106
152
|
});
|
|
107
153
|
}
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
*/
|
|
154
|
+
/** Best-effort create/self-heal primitive that never moves an already-placed node. */
|
|
155
|
+
export async function appendTreeRoot(ctx, documentId) {
|
|
156
|
+
await auditedPlace(ctx, { documentId, parentDocumentId: null, ifUnplaced: true });
|
|
157
|
+
}
|
|
158
|
+
/** Best-effort post-version repair; locked `ifUnplaced` closes the check/write race. */
|
|
159
|
+
export async function selfHealTreePlacement(ctx, documentId) {
|
|
160
|
+
if (ctx.definition.tree !== true)
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
await appendTreeRoot(ctx, documentId);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
ctx.logger.error({ err, documentId }, 'failed to self-heal tree placement on update');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Remove a node to the unplaced state; an already-unplaced node is a true no-op. */
|
|
113
170
|
export async function removeFromTree(ctx, params) {
|
|
114
171
|
return withLogContext({ domain: 'services', module: 'tree', function: 'removeFromTree' }, async () => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const hooks = await resolveHooks(definition);
|
|
172
|
+
assertActorCanPerform(ctx.requestContext, ctx.collectionPath, 'update');
|
|
173
|
+
const hooks = await resolveHooks(ctx.definition);
|
|
118
174
|
const wantsEvent = hooks?.afterTreeChange != null;
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
await
|
|
123
|
-
|
|
124
|
-
|
|
175
|
+
const capability = requireTreeAuditCapability(ctx.db);
|
|
176
|
+
const actor = auditActor(ctx);
|
|
177
|
+
let mutation;
|
|
178
|
+
await capability.withTransaction(async () => {
|
|
179
|
+
mutation = await capability.remove({
|
|
180
|
+
collectionId: ctx.collectionId,
|
|
181
|
+
documentId: params.documentId,
|
|
182
|
+
includeSubtree: wantsEvent,
|
|
183
|
+
});
|
|
184
|
+
if (!mutation.changed)
|
|
185
|
+
return;
|
|
186
|
+
await capability.append({
|
|
187
|
+
documentId: params.documentId,
|
|
188
|
+
collectionId: ctx.collectionId,
|
|
189
|
+
actorId: actor.actorId,
|
|
190
|
+
actorRealm: actor.actorRealm,
|
|
191
|
+
action: AUDIT_ACTIONS.treeRemoved,
|
|
192
|
+
field: 'tree',
|
|
193
|
+
before: { ...mutation.before, mode: 'remove' },
|
|
194
|
+
after: { ...mutation.after, mode: 'remove' },
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
if (!mutation || !wantsEvent)
|
|
198
|
+
return;
|
|
199
|
+
const committedMutation = mutation;
|
|
200
|
+
if (!committedMutation.changed) {
|
|
201
|
+
if (params.reconcile === true) {
|
|
202
|
+
await runCommittedTreeHook({ change: 'remove', documentId: params.documentId }, async () => {
|
|
203
|
+
await fireTreeChange(ctx, ctx.definition, {
|
|
204
|
+
change: 'remove',
|
|
205
|
+
documentId: params.documentId,
|
|
206
|
+
affectedDocumentIds: [
|
|
207
|
+
...(await wholeTreeIds(ctx)),
|
|
208
|
+
...committedMutation.beforeSubtreeDocumentIds,
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
await runCommittedTreeHook({ change: 'remove', documentId: params.documentId }, async () => {
|
|
216
|
+
await fireTreeChange(ctx, ctx.definition, {
|
|
125
217
|
change: 'remove',
|
|
126
218
|
documentId: params.documentId,
|
|
127
219
|
affectedDocumentIds: [
|
|
128
|
-
|
|
129
|
-
...
|
|
130
|
-
|
|
131
|
-
|
|
220
|
+
...committedMutation.beforeSubtreeDocumentIds,
|
|
221
|
+
...(committedMutation.before.parentDocumentId
|
|
222
|
+
? [committedMutation.before.parentDocumentId]
|
|
223
|
+
: []),
|
|
224
|
+
...committedMutation.beforeSiblingDocumentIds,
|
|
132
225
|
],
|
|
133
226
|
});
|
|
134
|
-
}
|
|
227
|
+
});
|
|
135
228
|
});
|
|
136
229
|
}
|
|
137
230
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* (promote) and `child → cascade` (leave) foreign keys, which only fire on a
|
|
141
|
-
* *hard* row delete. Byline deletes are soft (the document row survives), so the
|
|
142
|
-
* tree must be reconciled here. Fires one `afterTreeChange` (`promote-on-delete`)
|
|
143
|
-
* covering the deleted node, the promoted children, and their subtrees.
|
|
144
|
-
*
|
|
145
|
-
* Best-effort and idempotent: a node with no children or no edge row is a no-op.
|
|
146
|
-
* Called by the document delete lifecycle for `tree: true` collections.
|
|
231
|
+
* Reconcile delete-time edges and append parent plus child-specific audit rows.
|
|
232
|
+
* The caller owns the transaction; delete uses this beside soft-delete/audit.
|
|
147
233
|
*/
|
|
234
|
+
export async function reconcileTreeOnDeleteInTransaction(ctx, documentId, capability) {
|
|
235
|
+
const result = await capability.promoteAndRemove({
|
|
236
|
+
collectionId: ctx.collectionId,
|
|
237
|
+
documentId,
|
|
238
|
+
});
|
|
239
|
+
const actor = auditActor(ctx);
|
|
240
|
+
for (const child of result.promoted) {
|
|
241
|
+
await capability.append({
|
|
242
|
+
documentId: child.documentId,
|
|
243
|
+
collectionId: ctx.collectionId,
|
|
244
|
+
actorId: actor.actorId,
|
|
245
|
+
actorRealm: actor.actorRealm,
|
|
246
|
+
action: AUDIT_ACTIONS.treeReparented,
|
|
247
|
+
field: 'tree',
|
|
248
|
+
before: { ...child.before, mode: 'promoteOnDelete', removedParentDocumentId: documentId },
|
|
249
|
+
after: { ...child.after, mode: 'promoteOnDelete', removedParentDocumentId: documentId },
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (result.removed.changed || result.promoted.length > 0) {
|
|
253
|
+
await capability.append({
|
|
254
|
+
documentId,
|
|
255
|
+
collectionId: ctx.collectionId,
|
|
256
|
+
actorId: actor.actorId,
|
|
257
|
+
actorRealm: actor.actorRealm,
|
|
258
|
+
action: AUDIT_ACTIONS.treeRemoved,
|
|
259
|
+
field: 'tree',
|
|
260
|
+
before: {
|
|
261
|
+
...result.removed.before,
|
|
262
|
+
mode: 'promoteChildren',
|
|
263
|
+
children: result.promoted.length,
|
|
264
|
+
},
|
|
265
|
+
after: {
|
|
266
|
+
...result.removed.after,
|
|
267
|
+
mode: 'promoteChildren',
|
|
268
|
+
promotedDocumentIds: result.promoted.map((child) => child.documentId),
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
/** Fire the promotion invalidation after its transaction commits. */
|
|
275
|
+
export async function firePromoteTreeChange(ctx, documentId, result) {
|
|
276
|
+
const hooks = await resolveHooks(ctx.definition);
|
|
277
|
+
if (hooks?.afterTreeChange == null || (!result.removed.changed && result.promoted.length === 0)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const affected = new Set([
|
|
281
|
+
documentId,
|
|
282
|
+
...result.removed.beforeSiblingDocumentIds,
|
|
283
|
+
...(result.removed.before.parentDocumentId ? [result.removed.before.parentDocumentId] : []),
|
|
284
|
+
]);
|
|
285
|
+
for (const child of result.promoted) {
|
|
286
|
+
for (const id of await subtreeIds(ctx, child.documentId))
|
|
287
|
+
affected.add(id);
|
|
288
|
+
}
|
|
289
|
+
await fireTreeChange(ctx, ctx.definition, {
|
|
290
|
+
change: 'promote-on-delete',
|
|
291
|
+
documentId,
|
|
292
|
+
affectedDocumentIds: affected,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/** Standalone audited reconciliation used by internal tooling and tests. */
|
|
148
296
|
export async function promoteChildrenAndRemove(ctx, params) {
|
|
149
297
|
return withLogContext({ domain: 'services', module: 'tree', function: 'promoteChildrenAndRemove' }, async () => {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const children = await childIds(ctx, params.documentId);
|
|
157
|
-
const affected = new Set([params.documentId]);
|
|
158
|
-
if (wantsEvent) {
|
|
159
|
-
for (const childId of children) {
|
|
160
|
-
for (const id of await subtreeIds(ctx, childId))
|
|
161
|
-
affected.add(id);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
// Promote each child to root. `placeTreeNode` with no neighbours mints a
|
|
165
|
-
// fresh root-group key, so promoted orphans no longer carry a stale
|
|
166
|
-
// per-parent key.
|
|
167
|
-
for (const childId of children) {
|
|
168
|
-
await db.commands.documents.placeTreeNode({
|
|
169
|
-
collectionId: ctx.collectionId,
|
|
170
|
-
documentId: childId,
|
|
171
|
-
parentDocumentId: null,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
// The deleted node leaves the tree.
|
|
175
|
-
await db.commands.documents.removeFromTree({ documentId: params.documentId });
|
|
176
|
-
if (wantsEvent) {
|
|
177
|
-
await fireTreeChange(ctx, definition, {
|
|
178
|
-
change: 'promote-on-delete',
|
|
179
|
-
documentId: params.documentId,
|
|
180
|
-
affectedDocumentIds: affected,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
298
|
+
const capability = requireTreeAuditCapability(ctx.db);
|
|
299
|
+
let result;
|
|
300
|
+
await capability.withTransaction(async () => {
|
|
301
|
+
result = await reconcileTreeOnDeleteInTransaction(ctx, params.documentId, capability);
|
|
302
|
+
});
|
|
303
|
+
await firePromoteTreeChange(ctx, params.documentId, result);
|
|
183
304
|
});
|
|
184
305
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|