@byline/db-mysql 4.8.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/LICENSE +373 -0
- package/README.md +283 -0
- package/dist/database/schema/auth.d.ts +951 -0
- package/dist/database/schema/auth.js +226 -0
- package/dist/database/schema/common.d.ts +168 -0
- package/dist/database/schema/common.js +130 -0
- package/dist/database/schema/index.d.ts +3419 -0
- package/dist/database/schema/index.js +895 -0
- package/dist/database/schema/schema-pins.test.node.d.ts +52 -0
- package/dist/database/schema/schema-pins.test.node.js +398 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +151 -0
- package/dist/lib/boot-check.d.ts +22 -0
- package/dist/lib/boot-check.js +42 -0
- package/dist/lib/boot-check.test.node.d.ts +8 -0
- package/dist/lib/boot-check.test.node.js +46 -0
- package/dist/lib/db-manager.d.ts +54 -0
- package/dist/lib/db-manager.js +49 -0
- package/dist/lib/test-db.d.ts +37 -0
- package/dist/lib/test-db.js +111 -0
- package/dist/lib/test-helper.d.ts +33 -0
- package/dist/lib/test-helper.js +68 -0
- package/dist/modules/admin/admin-permissions-repository.d.ts +35 -0
- package/dist/modules/admin/admin-permissions-repository.js +101 -0
- package/dist/modules/admin/admin-preferences-repository.d.ts +62 -0
- package/dist/modules/admin/admin-preferences-repository.js +156 -0
- package/dist/modules/admin/admin-roles-repository.d.ts +11 -0
- package/dist/modules/admin/admin-roles-repository.js +209 -0
- package/dist/modules/admin/admin-store.d.ts +20 -0
- package/dist/modules/admin/admin-store.js +30 -0
- package/dist/modules/admin/admin-users-repository.d.ts +11 -0
- package/dist/modules/admin/admin-users-repository.js +303 -0
- package/dist/modules/admin/index.d.ts +27 -0
- package/dist/modules/admin/index.js +27 -0
- package/dist/modules/admin/refresh-tokens-repository.d.ts +34 -0
- package/dist/modules/admin/refresh-tokens-repository.js +160 -0
- package/dist/modules/audit/audit-commands.d.ts +29 -0
- package/dist/modules/audit/audit-commands.js +40 -0
- package/dist/modules/audit/audit-queries.d.ts +41 -0
- package/dist/modules/audit/audit-queries.js +169 -0
- package/dist/modules/counters/counters-commands.d.ts +54 -0
- package/dist/modules/counters/counters-commands.js +121 -0
- package/dist/modules/counters/tests/counters-concurrency.test.d.ts +8 -0
- package/dist/modules/counters/tests/counters-concurrency.test.js +111 -0
- package/dist/modules/storage/classify-error.d.ts +34 -0
- package/dist/modules/storage/classify-error.js +50 -0
- package/dist/modules/storage/classify-error.test.node.d.ts +8 -0
- package/dist/modules/storage/classify-error.test.node.js +89 -0
- package/dist/modules/storage/normalize-row.d.ts +55 -0
- package/dist/modules/storage/normalize-row.js +135 -0
- package/dist/modules/storage/normalize-row.test.node.d.ts +8 -0
- package/dist/modules/storage/normalize-row.test.node.js +89 -0
- package/dist/modules/storage/storage-commands.d.ts +443 -0
- package/dist/modules/storage/storage-commands.js +1263 -0
- package/dist/modules/storage/storage-insert.d.ts +21 -0
- package/dist/modules/storage/storage-insert.js +152 -0
- package/dist/modules/storage/storage-queries.d.ts +805 -0
- package/dist/modules/storage/storage-queries.js +1815 -0
- package/dist/modules/storage/storage-store-manifest.d.ts +77 -0
- package/dist/modules/storage/storage-store-manifest.js +168 -0
- package/dist/modules/storage/storage-utils.d.ts +49 -0
- package/dist/modules/storage/storage-utils.js +76 -0
- package/dist/modules/storage/tests/dialect-pins.integration.test.d.ts +8 -0
- package/dist/modules/storage/tests/dialect-pins.integration.test.js +266 -0
- package/dist/modules/storage/tests/storage-commands.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-commands.test.js +324 -0
- package/dist/modules/storage/tests/storage-document-paths.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-document-paths.test.js +214 -0
- package/dist/modules/storage/tests/storage-document-tree.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-document-tree.test.js +361 -0
- package/dist/modules/storage/tests/storage-queries.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-queries.test.js +685 -0
- package/dist/modules/storage/tests/storage-status-and-lifecycle.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-status-and-lifecycle.test.js +268 -0
- package/package.json +91 -0
|
@@ -0,0 +1,361 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Live-database verification of the Task 9B document-tree command surface —
|
|
10
|
+
* `DocumentCommands.placeTreeNode`, `removeFromTree`, and
|
|
11
|
+
* `promoteChildrenAndRemoveFromTree`. Mirrors the write-half of
|
|
12
|
+
* `@byline/db-conformance`'s `document-tree` suite
|
|
13
|
+
* (`packages/db-conformance/src/suites/document-tree.ts`), but that suite
|
|
14
|
+
* also exercises `getTreeChildren` / `getTreeAncestors` / `getTreeSubtree`
|
|
15
|
+
* (Task 10 read surface, not implemented yet) — so every assertion here reads
|
|
16
|
+
* `byline_document_relationships` directly via raw SQL instead, and only
|
|
17
|
+
* behaviours observable from the write commands alone are covered. Once
|
|
18
|
+
* Task 10 lands, `document-tree` becomes runnable end to end and is the
|
|
19
|
+
* authoritative gate; this file is the interim live-database regression
|
|
20
|
+
* guard for the command half.
|
|
21
|
+
*/
|
|
22
|
+
import { ErrorCodes, TREE_PLACEMENT_STALE_MARKER } from '@byline/core';
|
|
23
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
24
|
+
import { setupTestDB, teardownTestDB } from '../../../lib/test-helper.js';
|
|
25
|
+
const timestamp = Date.now();
|
|
26
|
+
function first(rows) {
|
|
27
|
+
const row = rows[0];
|
|
28
|
+
if (row == null)
|
|
29
|
+
throw new Error('expected at least one row, got none');
|
|
30
|
+
return row;
|
|
31
|
+
}
|
|
32
|
+
async function queryRows(pool, sql, params) {
|
|
33
|
+
const [rows] = await pool.query(sql, params);
|
|
34
|
+
return rows;
|
|
35
|
+
}
|
|
36
|
+
/** Read one ordered sibling group directly from `byline_document_relationships`. */
|
|
37
|
+
async function siblingIds(pool, parentDocumentId) {
|
|
38
|
+
// MySQL's null-safe equality (`<=>`) so `parentDocumentId: null` matches
|
|
39
|
+
// NULL rows (root siblings) the same way `parent_document_id IS NULL` does.
|
|
40
|
+
const rows = await queryRows(pool, 'SELECT child_document_id FROM byline_document_relationships WHERE parent_document_id <=> ? ORDER BY order_key', [parentDocumentId]);
|
|
41
|
+
return rows.map((r) => r.child_document_id);
|
|
42
|
+
}
|
|
43
|
+
async function edgeRow(pool, documentId) {
|
|
44
|
+
const rows = await queryRows(pool, 'SELECT child_document_id, parent_document_id, order_key FROM byline_document_relationships WHERE child_document_id = ?', [documentId]);
|
|
45
|
+
return rows[0];
|
|
46
|
+
}
|
|
47
|
+
const TreeCollectionConfig = {
|
|
48
|
+
path: `tree-test-${timestamp}`,
|
|
49
|
+
labels: { singular: 'TreeTest', plural: 'TreeTests' },
|
|
50
|
+
fields: [{ name: 'title', type: 'text' }],
|
|
51
|
+
};
|
|
52
|
+
const OtherCollectionConfig = {
|
|
53
|
+
path: `tree-other-test-${timestamp}`,
|
|
54
|
+
labels: { singular: 'TreeOther', plural: 'TreeOthers' },
|
|
55
|
+
fields: [{ name: 'title', type: 'text' }],
|
|
56
|
+
};
|
|
57
|
+
describe('DocumentCommands tree mutations (mysql, live database)', () => {
|
|
58
|
+
let testDb;
|
|
59
|
+
let rawPool;
|
|
60
|
+
let treeCollectionId;
|
|
61
|
+
let otherCollectionId;
|
|
62
|
+
async function createDoc(collectionId, config, title) {
|
|
63
|
+
const created = await testDb.commandBuilders.documents.createDocumentVersion({
|
|
64
|
+
collectionId,
|
|
65
|
+
collectionVersion: 1,
|
|
66
|
+
collectionConfig: config,
|
|
67
|
+
action: 'create',
|
|
68
|
+
documentData: { title },
|
|
69
|
+
locale: 'all',
|
|
70
|
+
status: 'published',
|
|
71
|
+
});
|
|
72
|
+
return created.document.document_id;
|
|
73
|
+
}
|
|
74
|
+
beforeAll(async () => {
|
|
75
|
+
testDb = setupTestDB([TreeCollectionConfig, OtherCollectionConfig]);
|
|
76
|
+
rawPool = testDb.pool;
|
|
77
|
+
treeCollectionId = first(await testDb.commandBuilders.collections.create(TreeCollectionConfig.path, TreeCollectionConfig)).id;
|
|
78
|
+
otherCollectionId = first(await testDb.commandBuilders.collections.create(OtherCollectionConfig.path, OtherCollectionConfig)).id;
|
|
79
|
+
});
|
|
80
|
+
afterAll(async () => {
|
|
81
|
+
await testDb.commandBuilders.collections.delete(treeCollectionId);
|
|
82
|
+
await testDb.commandBuilders.collections.delete(otherCollectionId);
|
|
83
|
+
await teardownTestDB();
|
|
84
|
+
});
|
|
85
|
+
it('places roots and children, ordered per-parent', async () => {
|
|
86
|
+
const a = await createDoc(treeCollectionId, TreeCollectionConfig, 'Root A');
|
|
87
|
+
const b = await createDoc(treeCollectionId, TreeCollectionConfig, 'Root B');
|
|
88
|
+
const c = await createDoc(treeCollectionId, TreeCollectionConfig, 'Child C');
|
|
89
|
+
const d = await createDoc(treeCollectionId, TreeCollectionConfig, 'Child D');
|
|
90
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
91
|
+
collectionId: treeCollectionId,
|
|
92
|
+
documentId: a,
|
|
93
|
+
parentDocumentId: null,
|
|
94
|
+
});
|
|
95
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
96
|
+
collectionId: treeCollectionId,
|
|
97
|
+
documentId: b,
|
|
98
|
+
parentDocumentId: null,
|
|
99
|
+
beforeDocumentId: a,
|
|
100
|
+
});
|
|
101
|
+
expect(await siblingIds(rawPool, null)).toEqual(expect.arrayContaining([a, b]));
|
|
102
|
+
const roots = await siblingIds(rawPool, null);
|
|
103
|
+
expect(roots.indexOf(a)).toBeLessThan(roots.indexOf(b));
|
|
104
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
105
|
+
collectionId: treeCollectionId,
|
|
106
|
+
documentId: c,
|
|
107
|
+
parentDocumentId: a,
|
|
108
|
+
});
|
|
109
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
110
|
+
collectionId: treeCollectionId,
|
|
111
|
+
documentId: d,
|
|
112
|
+
parentDocumentId: a,
|
|
113
|
+
beforeDocumentId: c,
|
|
114
|
+
});
|
|
115
|
+
expect(await siblingIds(rawPool, a)).toEqual([c, d]);
|
|
116
|
+
});
|
|
117
|
+
it('reorders siblings in place — upsert keeps one row per document', async () => {
|
|
118
|
+
const a = await createDoc(treeCollectionId, TreeCollectionConfig, 'P A');
|
|
119
|
+
const x = await createDoc(treeCollectionId, TreeCollectionConfig, 'X');
|
|
120
|
+
const y = await createDoc(treeCollectionId, TreeCollectionConfig, 'Y');
|
|
121
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
122
|
+
collectionId: treeCollectionId,
|
|
123
|
+
documentId: a,
|
|
124
|
+
parentDocumentId: null,
|
|
125
|
+
});
|
|
126
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
127
|
+
collectionId: treeCollectionId,
|
|
128
|
+
documentId: x,
|
|
129
|
+
parentDocumentId: a,
|
|
130
|
+
});
|
|
131
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
132
|
+
collectionId: treeCollectionId,
|
|
133
|
+
documentId: y,
|
|
134
|
+
parentDocumentId: a,
|
|
135
|
+
beforeDocumentId: x,
|
|
136
|
+
});
|
|
137
|
+
expect(await siblingIds(rawPool, a)).toEqual([x, y]);
|
|
138
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
139
|
+
collectionId: treeCollectionId,
|
|
140
|
+
documentId: y,
|
|
141
|
+
parentDocumentId: a,
|
|
142
|
+
afterDocumentId: x,
|
|
143
|
+
});
|
|
144
|
+
const kids = await siblingIds(rawPool, a);
|
|
145
|
+
expect(kids).toEqual([y, x]);
|
|
146
|
+
expect(kids.length).toBe(2); // no duplicate row from the upsert
|
|
147
|
+
});
|
|
148
|
+
it('rejects a stale target neighbour group as a conflict', async () => {
|
|
149
|
+
const parentA = await createDoc(treeCollectionId, TreeCollectionConfig, 'Conflict Parent A');
|
|
150
|
+
const parentB = await createDoc(treeCollectionId, TreeCollectionConfig, 'Conflict Parent B');
|
|
151
|
+
const neighbour = await createDoc(treeCollectionId, TreeCollectionConfig, 'Conflict Neighbour');
|
|
152
|
+
const node = await createDoc(treeCollectionId, TreeCollectionConfig, 'Conflict Node');
|
|
153
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
154
|
+
collectionId: treeCollectionId,
|
|
155
|
+
documentId: neighbour,
|
|
156
|
+
parentDocumentId: parentA,
|
|
157
|
+
});
|
|
158
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
159
|
+
collectionId: treeCollectionId,
|
|
160
|
+
documentId: neighbour,
|
|
161
|
+
parentDocumentId: parentB,
|
|
162
|
+
});
|
|
163
|
+
await expect(testDb.commandBuilders.documents.placeTreeNode({
|
|
164
|
+
collectionId: treeCollectionId,
|
|
165
|
+
documentId: node,
|
|
166
|
+
parentDocumentId: parentA,
|
|
167
|
+
beforeDocumentId: neighbour,
|
|
168
|
+
})).rejects.toMatchObject({
|
|
169
|
+
code: ErrorCodes.CONFLICT,
|
|
170
|
+
message: expect.stringContaining(TREE_PLACEMENT_STALE_MARKER),
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it('allows only one concurrent placement into the same asserted sibling gap', async () => {
|
|
174
|
+
const parent = await createDoc(treeCollectionId, TreeCollectionConfig, 'Gap Parent');
|
|
175
|
+
const left = await createDoc(treeCollectionId, TreeCollectionConfig, 'Gap Left');
|
|
176
|
+
const right = await createDoc(treeCollectionId, TreeCollectionConfig, 'Gap Right');
|
|
177
|
+
const nodeFirst = await createDoc(treeCollectionId, TreeCollectionConfig, 'Gap First');
|
|
178
|
+
const nodeSecond = await createDoc(treeCollectionId, TreeCollectionConfig, 'Gap Second');
|
|
179
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
180
|
+
collectionId: treeCollectionId,
|
|
181
|
+
documentId: left,
|
|
182
|
+
parentDocumentId: parent,
|
|
183
|
+
});
|
|
184
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
185
|
+
collectionId: treeCollectionId,
|
|
186
|
+
documentId: right,
|
|
187
|
+
parentDocumentId: parent,
|
|
188
|
+
beforeDocumentId: left,
|
|
189
|
+
});
|
|
190
|
+
const placeInSameGap = (documentId) => testDb.commandBuilders.documents.placeTreeNode({
|
|
191
|
+
collectionId: treeCollectionId,
|
|
192
|
+
documentId,
|
|
193
|
+
parentDocumentId: parent,
|
|
194
|
+
beforeDocumentId: left,
|
|
195
|
+
afterDocumentId: right,
|
|
196
|
+
});
|
|
197
|
+
const results = await Promise.allSettled([
|
|
198
|
+
placeInSameGap(nodeFirst),
|
|
199
|
+
placeInSameGap(nodeSecond),
|
|
200
|
+
]);
|
|
201
|
+
const fulfilled = results.filter((r) => r.status === 'fulfilled');
|
|
202
|
+
const rejected = results.filter((r) => r.status === 'rejected');
|
|
203
|
+
expect(fulfilled).toHaveLength(1);
|
|
204
|
+
expect(rejected).toHaveLength(1);
|
|
205
|
+
expect(rejected[0]).toMatchObject({ reason: { code: ErrorCodes.CONFLICT } });
|
|
206
|
+
const winner = results[0]?.status === 'fulfilled' ? nodeFirst : nodeSecond;
|
|
207
|
+
const loser = winner === nodeFirst ? nodeSecond : nodeFirst;
|
|
208
|
+
expect(await siblingIds(rawPool, parent)).toEqual([left, winner, right]);
|
|
209
|
+
expect(await edgeRow(rawPool, loser)).toBeUndefined(); // never placed
|
|
210
|
+
});
|
|
211
|
+
it('rejects placement when the moving node was deleted after the placement snapshot', async () => {
|
|
212
|
+
const node = await createDoc(treeCollectionId, TreeCollectionConfig, 'Deleted Moving Node');
|
|
213
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
214
|
+
collectionId: treeCollectionId,
|
|
215
|
+
documentId: node,
|
|
216
|
+
parentDocumentId: null,
|
|
217
|
+
});
|
|
218
|
+
await testDb.commandBuilders.documents.softDeleteDocument({ document_id: node });
|
|
219
|
+
await expect(testDb.commandBuilders.documents.placeTreeNode({
|
|
220
|
+
collectionId: treeCollectionId,
|
|
221
|
+
documentId: node,
|
|
222
|
+
parentDocumentId: null,
|
|
223
|
+
})).rejects.toMatchObject({ code: ErrorCodes.CONFLICT });
|
|
224
|
+
});
|
|
225
|
+
it('rejects a cross-collection neighbour as structural validation', async () => {
|
|
226
|
+
const parent = await createDoc(treeCollectionId, TreeCollectionConfig, 'Foreign Neighbour Parent');
|
|
227
|
+
const node = await createDoc(treeCollectionId, TreeCollectionConfig, 'Foreign Neighbour Node');
|
|
228
|
+
const foreign = await createDoc(otherCollectionId, OtherCollectionConfig, 'Foreign Neighbour');
|
|
229
|
+
await expect(testDb.commandBuilders.documents.placeTreeNode({
|
|
230
|
+
collectionId: treeCollectionId,
|
|
231
|
+
documentId: node,
|
|
232
|
+
parentDocumentId: parent,
|
|
233
|
+
afterDocumentId: foreign,
|
|
234
|
+
})).rejects.toMatchObject({ code: ErrorCodes.VALIDATION });
|
|
235
|
+
});
|
|
236
|
+
it('rejects a self-parent and a cycle', async () => {
|
|
237
|
+
const a = await createDoc(treeCollectionId, TreeCollectionConfig, 'CA');
|
|
238
|
+
const c = await createDoc(treeCollectionId, TreeCollectionConfig, 'CC');
|
|
239
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
240
|
+
collectionId: treeCollectionId,
|
|
241
|
+
documentId: a,
|
|
242
|
+
parentDocumentId: null,
|
|
243
|
+
});
|
|
244
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
245
|
+
collectionId: treeCollectionId,
|
|
246
|
+
documentId: c,
|
|
247
|
+
parentDocumentId: a,
|
|
248
|
+
});
|
|
249
|
+
await expect(testDb.commandBuilders.documents.placeTreeNode({
|
|
250
|
+
collectionId: treeCollectionId,
|
|
251
|
+
documentId: a,
|
|
252
|
+
parentDocumentId: a,
|
|
253
|
+
})).rejects.toMatchObject({ code: ErrorCodes.VALIDATION });
|
|
254
|
+
// A is C's ancestor, so making A a child of C is a cycle — exercises the
|
|
255
|
+
// `WITH RECURSIVE chain AS (...)` cycle guard.
|
|
256
|
+
await expect(testDb.commandBuilders.documents.placeTreeNode({
|
|
257
|
+
collectionId: treeCollectionId,
|
|
258
|
+
documentId: a,
|
|
259
|
+
parentDocumentId: c,
|
|
260
|
+
})).rejects.toMatchObject({ code: ErrorCodes.VALIDATION });
|
|
261
|
+
});
|
|
262
|
+
it('re-parents atomically — one edge row, updated in place', async () => {
|
|
263
|
+
const a = await createDoc(treeCollectionId, TreeCollectionConfig, 'RA');
|
|
264
|
+
const b = await createDoc(treeCollectionId, TreeCollectionConfig, 'RB');
|
|
265
|
+
const c = await createDoc(treeCollectionId, TreeCollectionConfig, 'RC');
|
|
266
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
267
|
+
collectionId: treeCollectionId,
|
|
268
|
+
documentId: a,
|
|
269
|
+
parentDocumentId: null,
|
|
270
|
+
});
|
|
271
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
272
|
+
collectionId: treeCollectionId,
|
|
273
|
+
documentId: b,
|
|
274
|
+
parentDocumentId: null,
|
|
275
|
+
});
|
|
276
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
277
|
+
collectionId: treeCollectionId,
|
|
278
|
+
documentId: c,
|
|
279
|
+
parentDocumentId: a,
|
|
280
|
+
});
|
|
281
|
+
expect(await siblingIds(rawPool, a)).toEqual([c]);
|
|
282
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
283
|
+
collectionId: treeCollectionId,
|
|
284
|
+
documentId: c,
|
|
285
|
+
parentDocumentId: b,
|
|
286
|
+
});
|
|
287
|
+
expect(await siblingIds(rawPool, a)).toEqual([]);
|
|
288
|
+
expect(await siblingIds(rawPool, b)).toEqual([c]);
|
|
289
|
+
const edge = await edgeRow(rawPool, c);
|
|
290
|
+
expect(edge.parent_document_id).toBe(b);
|
|
291
|
+
});
|
|
292
|
+
it('removeFromTree returns a node to the unplaced state and is idempotent', async () => {
|
|
293
|
+
const a = await createDoc(treeCollectionId, TreeCollectionConfig, 'UA');
|
|
294
|
+
const c = await createDoc(treeCollectionId, TreeCollectionConfig, 'UC');
|
|
295
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
296
|
+
collectionId: treeCollectionId,
|
|
297
|
+
documentId: a,
|
|
298
|
+
parentDocumentId: null,
|
|
299
|
+
});
|
|
300
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
301
|
+
collectionId: treeCollectionId,
|
|
302
|
+
documentId: c,
|
|
303
|
+
parentDocumentId: a,
|
|
304
|
+
});
|
|
305
|
+
expect(await siblingIds(rawPool, a)).toEqual([c]);
|
|
306
|
+
const removed = await testDb.commandBuilders.documents.removeFromTree({
|
|
307
|
+
collectionId: treeCollectionId,
|
|
308
|
+
documentId: c,
|
|
309
|
+
});
|
|
310
|
+
expect(removed.changed).toBe(true);
|
|
311
|
+
expect(await siblingIds(rawPool, a)).toEqual([]);
|
|
312
|
+
expect(await edgeRow(rawPool, c)).toBeUndefined();
|
|
313
|
+
// Idempotent — removing an already-unplaced node is a no-op.
|
|
314
|
+
await expect(testDb.commandBuilders.documents.removeFromTree({
|
|
315
|
+
collectionId: treeCollectionId,
|
|
316
|
+
documentId: c,
|
|
317
|
+
})).resolves.toMatchObject({ changed: false });
|
|
318
|
+
});
|
|
319
|
+
it('promoteChildrenAndRemoveFromTree promotes children to root and removes the parent edge', async () => {
|
|
320
|
+
const parent = await createDoc(treeCollectionId, TreeCollectionConfig, 'Promote Parent');
|
|
321
|
+
const grandparent = await createDoc(treeCollectionId, TreeCollectionConfig, 'Promote Grandparent');
|
|
322
|
+
const childOne = await createDoc(treeCollectionId, TreeCollectionConfig, 'Promote Child 1');
|
|
323
|
+
const childTwo = await createDoc(treeCollectionId, TreeCollectionConfig, 'Promote Child 2');
|
|
324
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
325
|
+
collectionId: treeCollectionId,
|
|
326
|
+
documentId: grandparent,
|
|
327
|
+
parentDocumentId: null,
|
|
328
|
+
});
|
|
329
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
330
|
+
collectionId: treeCollectionId,
|
|
331
|
+
documentId: parent,
|
|
332
|
+
parentDocumentId: grandparent,
|
|
333
|
+
});
|
|
334
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
335
|
+
collectionId: treeCollectionId,
|
|
336
|
+
documentId: childOne,
|
|
337
|
+
parentDocumentId: parent,
|
|
338
|
+
});
|
|
339
|
+
await testDb.commandBuilders.documents.placeTreeNode({
|
|
340
|
+
collectionId: treeCollectionId,
|
|
341
|
+
documentId: childTwo,
|
|
342
|
+
parentDocumentId: parent,
|
|
343
|
+
beforeDocumentId: childOne,
|
|
344
|
+
});
|
|
345
|
+
expect(await siblingIds(rawPool, parent)).toEqual([childOne, childTwo]);
|
|
346
|
+
const result = await testDb.commandBuilders.documents.promoteChildrenAndRemoveFromTree({
|
|
347
|
+
collectionId: treeCollectionId,
|
|
348
|
+
documentId: parent,
|
|
349
|
+
});
|
|
350
|
+
expect(result.removed.changed).toBe(true);
|
|
351
|
+
expect(result.promoted.map((p) => p.documentId).sort()).toEqual([childOne, childTwo].sort());
|
|
352
|
+
// Parent edge gone; both children now root-level (parent_document_id NULL).
|
|
353
|
+
expect(await edgeRow(rawPool, parent)).toBeUndefined();
|
|
354
|
+
const oneEdge = await edgeRow(rawPool, childOne);
|
|
355
|
+
const twoEdge = await edgeRow(rawPool, childTwo);
|
|
356
|
+
expect(oneEdge.parent_document_id).toBeNull();
|
|
357
|
+
expect(twoEdge.parent_document_id).toBeNull();
|
|
358
|
+
// No longer children of the (now-removed) parent edge.
|
|
359
|
+
expect(await siblingIds(rawPool, parent)).toEqual([]);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -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 {};
|