objection 4.0.0-rc.2 → 4.0.0-rc.3
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/lib/queryBuilder/graph/GraphLoader.js +201 -126
- package/lib/queryBuilder/graph/GraphNode.js +10 -3
- package/lib/queryBuilder/graph/GraphOptions.js +24 -49
- package/lib/queryBuilder/graph/GraphPlan.js +92 -108
- package/lib/queryBuilder/graph/GraphSchedule.js +27 -18
- package/lib/queryBuilder/graph/GraphUpsert.js +46 -548
- package/lib/queryBuilder/graph/GraphWriter.js +257 -14
- package/lib/queryBuilder/graph/graphChecks.js +497 -0
- package/lib/queryBuilder/graph/graphKeys.js +109 -404
- package/package.json +2 -2
- package/typings/objection/index.d.ts +437 -85
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
import { hasSameColumns, hasSameKey } from './graphKeys.js';
|
|
1
|
+
import { hasOwnerKey, hasSameColumns, hasSameKey } from './graphKeys.js';
|
|
2
|
+
import { groupBy } from '../../utils/objectUtils.js';
|
|
2
3
|
|
|
3
4
|
// What a graph insert or upsert does with each model of the graph and each row
|
|
4
5
|
// of the current graph: The rules that decide it, and the pruning of branches
|
|
5
6
|
// that nothing is written in.
|
|
6
7
|
export class GraphPlan {
|
|
7
|
-
constructor({ nodes, graphOptions
|
|
8
|
+
constructor({ nodes, graphOptions }) {
|
|
8
9
|
this.nodes = nodes;
|
|
9
|
-
// The nodes that an earlier pass left to this one before writing them:
|
|
10
|
-
this.laterNodes = laterNodes;
|
|
11
|
-
this.currentNodes = [];
|
|
12
|
-
this.deferredNodes = new Set();
|
|
13
|
-
// The nodes that earlier passes of the graph decided to keep or inserted,
|
|
14
|
-
// by row:
|
|
15
|
-
this.earlierKeptNodes = keptNodes;
|
|
16
10
|
this.graphOptions = graphOptions;
|
|
17
|
-
this.
|
|
18
|
-
// The decisions only change while the current graph is loaded, and are
|
|
19
|
-
// cached once it is, see `cacheDecisions()`:
|
|
20
|
-
this._intents = null;
|
|
21
|
-
this._nodeActions = null;
|
|
22
|
-
this._activeNodes = null;
|
|
23
|
-
this._activeCurrentNodes = null;
|
|
24
|
-
this._keptNodesByRowKey = null;
|
|
11
|
+
this.reset();
|
|
25
12
|
}
|
|
26
13
|
|
|
27
14
|
get activeNodes() {
|
|
@@ -34,16 +21,24 @@ export class GraphPlan {
|
|
|
34
21
|
));
|
|
35
22
|
}
|
|
36
23
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
24
|
+
// Forget the current graph and what was decided with it, before it's loaded.
|
|
25
|
+
// The decisions only change while it's loaded, and are cached once it is,
|
|
26
|
+
// see `plan()`.
|
|
27
|
+
reset() {
|
|
28
|
+
for (const node of this.nodes) {
|
|
29
|
+
node.current = null;
|
|
30
|
+
node.row = null;
|
|
31
|
+
node.isMissing = false;
|
|
32
|
+
node.isPruned = false;
|
|
33
|
+
}
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
this.
|
|
46
|
-
this.
|
|
35
|
+
this.currentNodes = [];
|
|
36
|
+
this.actions = null;
|
|
37
|
+
this._intents = null;
|
|
38
|
+
this._nodeActions = null;
|
|
39
|
+
this._activeNodes = null;
|
|
40
|
+
this._activeCurrentNodes = null;
|
|
41
|
+
this._keptNodesByRowKey = null;
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
// What the data asks to do with `node`, before the options that disable it:
|
|
@@ -65,23 +60,35 @@ export class GraphPlan {
|
|
|
65
60
|
_getIntent(node) {
|
|
66
61
|
const insertMissing = this.graphOptions.has(node, 'insertMissing');
|
|
67
62
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
63
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
64
|
+
return 'remove';
|
|
65
|
+
} else if (node.isReference) {
|
|
66
|
+
return 'relate';
|
|
67
|
+
} else if (node.isMissing) {
|
|
68
|
+
return insertMissing ? 'insert' : this.findInsertingNode(node) ? 'relate' : 'missing';
|
|
69
|
+
} else if (node.isDbReference || this._canRelate(node)) {
|
|
70
|
+
return 'relate';
|
|
71
|
+
} else if (node.current) {
|
|
72
|
+
return 'patch';
|
|
73
|
+
} else if (!node.hasId || insertMissing) {
|
|
74
|
+
return 'insert';
|
|
75
|
+
} else {
|
|
76
|
+
return this._isRelatedByOthers(node) ? 'relate' : 'missing';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Return true if another model of the graph relates the row of `node` to the
|
|
81
|
+
// same place, the same relation of the same row, as below the owner that a
|
|
82
|
+
// moved row names.
|
|
83
|
+
_isRelatedByOthers(node) {
|
|
84
|
+
return this.nodes.some(
|
|
85
|
+
(other) =>
|
|
86
|
+
other !== node &&
|
|
87
|
+
other.rowKey === node.rowKey &&
|
|
88
|
+
other.relation === node.relation &&
|
|
89
|
+
other.parent?.rowKey === node.parent?.rowKey &&
|
|
90
|
+
this._canRelate(other),
|
|
91
|
+
);
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
// Return true if the model of the `#ref` model `node` writes what `node`
|
|
@@ -182,13 +189,11 @@ export class GraphPlan {
|
|
|
182
189
|
|
|
183
190
|
// Leave out the branches that require no writes. We never do anything below
|
|
184
191
|
// models that are removed, and we never delete recursively.
|
|
185
|
-
|
|
192
|
+
_prune() {
|
|
186
193
|
const isInsertOnly = this.graphOptions.isInsertOnly();
|
|
187
|
-
|
|
188
|
-
// The rows that were fetched for pruned nodes are left alone too. Nodes of
|
|
189
|
-
// later passes aren't pruned here:
|
|
194
|
+
// The rows that were fetched for pruned nodes are left alone too:
|
|
190
195
|
const pruneNodes = (nodes) => {
|
|
191
|
-
for (const node of nodes
|
|
196
|
+
for (const node of nodes) {
|
|
192
197
|
node.isPruned = true;
|
|
193
198
|
|
|
194
199
|
for (const row of node.row ? [node.row, ...node.row.descendants] : []) {
|
|
@@ -210,10 +215,7 @@ export class GraphPlan {
|
|
|
210
215
|
(node) =>
|
|
211
216
|
!node.current &&
|
|
212
217
|
this.intentFor(node) !== 'insert' &&
|
|
213
|
-
!(
|
|
214
|
-
this.actionFor(node) === 'relate' &&
|
|
215
|
-
(node.row || (node.isReference && !this.graphOptions.isInsertOnly()))
|
|
216
|
-
),
|
|
218
|
+
!(this.actionFor(node) === 'relate' && (node.row || (node.isReference && !isInsertOnly))),
|
|
217
219
|
),
|
|
218
220
|
);
|
|
219
221
|
|
|
@@ -289,14 +291,16 @@ export class GraphPlan {
|
|
|
289
291
|
pruneDescendants(markedNodes);
|
|
290
292
|
pruneDescendants(markedNodes.map((node) => node.current).filter(Boolean));
|
|
291
293
|
}
|
|
292
|
-
|
|
293
|
-
return this;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
// Decide what to do with each node once
|
|
297
|
-
// writes change the models that the rules
|
|
298
|
-
//
|
|
296
|
+
// Decide what to do with each node once the current graph is loaded, and
|
|
297
|
+
// before the first write, since the writes change the models that the rules
|
|
298
|
+
// read: Cache the decisions, leave out the branches that require no writes,
|
|
299
|
+
// and set `actions`.
|
|
299
300
|
plan() {
|
|
301
|
+
this._intents = new Map();
|
|
302
|
+
this._nodeActions = new Map();
|
|
303
|
+
this._prune();
|
|
300
304
|
const actions = new Map();
|
|
301
305
|
|
|
302
306
|
for (const node of this.activeNodes) {
|
|
@@ -308,7 +312,6 @@ export class GraphPlan {
|
|
|
308
312
|
}
|
|
309
313
|
|
|
310
314
|
this.actions = actions;
|
|
311
|
-
return this;
|
|
312
315
|
}
|
|
313
316
|
|
|
314
317
|
_canRelate(node) {
|
|
@@ -336,14 +339,13 @@ export class GraphPlan {
|
|
|
336
339
|
);
|
|
337
340
|
}
|
|
338
341
|
|
|
339
|
-
// A row that the graph keeps in another place
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
// unique (#398).
|
|
342
|
+
// A row that the graph keeps in another place is unrelated here, unless one
|
|
343
|
+
// of these places sets the same key column, by relating, patching or
|
|
344
|
+
// inserting it through such a relation, or through its own
|
|
345
|
+
// BelongsToOneRelation, e.g. `parent` for `children`: Then that place writes
|
|
346
|
+
// the key, which may not be nullable. A has-one row that its own
|
|
347
|
+
// BelongsToOneRelation keeps is still unrelated if the graph fills its slot
|
|
348
|
+
// with another row, as the foreign key may be unique (#398).
|
|
347
349
|
_isUnrelatedWhenKept(currentNode, keptNode) {
|
|
348
350
|
const { relation } = currentNode;
|
|
349
351
|
const isKeptInRelation = hasSameKey(relation, keptNode.relation);
|
|
@@ -358,11 +360,11 @@ export class GraphPlan {
|
|
|
358
360
|
return (
|
|
359
361
|
relation.keySide !== 'related' ||
|
|
360
362
|
!(isKeptInRelation || isKeptByOwner) ||
|
|
361
|
-
(
|
|
363
|
+
(isKeptByOwner && this._hasNewRowInSlot(currentNode))
|
|
362
364
|
);
|
|
363
365
|
}
|
|
364
366
|
|
|
365
|
-
// Return true if
|
|
367
|
+
// Return true if the graph inserts or relates a row into the slot of the
|
|
366
368
|
// has-one relation that `currentNode` is in.
|
|
367
369
|
_hasNewRowInSlot(currentNode) {
|
|
368
370
|
return (
|
|
@@ -376,51 +378,33 @@ export class GraphPlan {
|
|
|
376
378
|
);
|
|
377
379
|
}
|
|
378
380
|
|
|
379
|
-
// The
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
return
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
for (const node of this.activeNodes) {
|
|
392
|
-
if (this.actions.get(node) === 'insert') {
|
|
393
|
-
addKeptNode(keptNodes, node);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return keptNodes;
|
|
381
|
+
// The BelongsToOne relations whose current rows `node` removes with `null`,
|
|
382
|
+
// `#unrelate` or `#delete`, and whose keys it clears.
|
|
383
|
+
getRemovedOwnerRelations(node) {
|
|
384
|
+
return (node.existing?.activeChildren || [])
|
|
385
|
+
.filter(
|
|
386
|
+
(currentChild) =>
|
|
387
|
+
hasOwnerKey(currentChild.relation) &&
|
|
388
|
+
(node.obj[currentChild.relation.name] === null ||
|
|
389
|
+
currentChild.node?.toBeUnrelatedOrDeleted) &&
|
|
390
|
+
this.actions.get(currentChild) !== 'keep',
|
|
391
|
+
)
|
|
392
|
+
.map(({ relation }) => relation);
|
|
398
393
|
}
|
|
399
394
|
|
|
400
|
-
// The nodes that keep
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (node.obj.$hasId() && (action === 'relate' || action === 'patch')) {
|
|
411
|
-
addKeptNode(this._keptNodesByRowKey, node);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return this._keptNodesByRowKey;
|
|
395
|
+
// The nodes that keep the row of `node` in other places of the graph, by
|
|
396
|
+
// relating or patching it there. A row may be kept in several places.
|
|
397
|
+
getKeptNodes(node) {
|
|
398
|
+
this._keptNodesByRowKey ??= groupBy(
|
|
399
|
+
this.activeNodes.filter(
|
|
400
|
+
(node) => node.obj.$hasId() && ['relate', 'patch'].includes(this.actionFor(node)),
|
|
401
|
+
),
|
|
402
|
+
(node) => node.rowKey,
|
|
403
|
+
);
|
|
404
|
+
return this._keptNodesByRowKey.get(node.rowKey) || [];
|
|
417
405
|
}
|
|
418
406
|
}
|
|
419
407
|
|
|
420
|
-
function addKeptNode(keptNodes, node) {
|
|
421
|
-
keptNodes.set(node.rowKey, [...(keptNodes.get(node.rowKey) || []), node]);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
408
|
// For `HasMany` and `HasOne` relations, the relate prop is the id of the
|
|
425
409
|
// related model. Use the id state of the input graph, since the parent's
|
|
426
410
|
// insert copies the foreign key into the related model, which can complete
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasOwnerKey, hasSameKey } from './graphKeys.js';
|
|
1
|
+
import { hasOwnerKey, hasSameColumns, hasSameKey } from './graphKeys.js';
|
|
2
2
|
import { groupBy } from '../../utils/objectUtils.js';
|
|
3
3
|
|
|
4
4
|
// Order the writes of a planned graph in steps that keep keys and constraints
|
|
@@ -14,7 +14,9 @@ import { groupBy } from '../../utils/objectUtils.js';
|
|
|
14
14
|
// same order, as the foreign key may be unique.
|
|
15
15
|
// 4. Patch the changed rows, the other relates, and the keys of owners whose
|
|
16
16
|
// BelongsToOne rows were inserted, related or removed.
|
|
17
|
-
// 5.
|
|
17
|
+
// 5. Unrelate the rows that their own to-many relation on their own key leaves
|
|
18
|
+
// out, e.g. a pet in its own `siblings`, after their own writes.
|
|
19
|
+
// 6. Remove the BelongsToOne rows that the owners no longer reference.
|
|
18
20
|
// Each step writes a row that goes into the slot of a HasOneRelation after the
|
|
19
21
|
// row that moves out of it, unless that closes a cycle.
|
|
20
22
|
// `insertGraph()` only inserts.
|
|
@@ -25,16 +27,9 @@ export class GraphSchedule {
|
|
|
25
27
|
// The nodes that each node waits for, through its keys, its references and
|
|
26
28
|
// the rows that move out of the slots that it goes into:
|
|
27
29
|
this.dependencies = createDependencies(plan, this.keys);
|
|
28
|
-
// Dependencies that aren't waited for, to break cycles
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
// after the inserts:
|
|
32
|
-
const activeNodes = new Set(plan.activeNodes);
|
|
33
|
-
this.deferredDependencies = new Set(
|
|
34
|
-
[...this.dependencies].flatMap(([node, dependencies]) =>
|
|
35
|
-
activeNodes.has(node) ? [] : dependencies.filter((dependency) => dependency.relation),
|
|
36
|
-
),
|
|
37
|
-
);
|
|
30
|
+
// Dependencies that aren't waited for, to break cycles. Deferred keys are
|
|
31
|
+
// written after the inserts:
|
|
32
|
+
this.deferredDependencies = new Set();
|
|
38
33
|
// Relates whose rows are moved to their new owners before the removals:
|
|
39
34
|
this.movedNodes = new Set(
|
|
40
35
|
plan.graphOptions.isInsertOnly()
|
|
@@ -76,12 +71,19 @@ export class GraphSchedule {
|
|
|
76
71
|
} else {
|
|
77
72
|
const removedNodes = activeCurrentNodes.filter((node) => actions.get(node) !== 'keep');
|
|
78
73
|
const isReferencedByOwner = (currentNode) => currentNode.relation.keySide === 'owner';
|
|
74
|
+
// A row that its own to-many relation on its own key leaves out, e.g. a
|
|
75
|
+
// pet in its own `siblings`, is unrelated after its own writes, which keep
|
|
76
|
+
// its key. A row in its own slot leaves it before another one goes in:
|
|
77
|
+
const isOwnRow = ({ relation, parent, rowKey }) =>
|
|
78
|
+
rowKey === parent.rowKey &&
|
|
79
|
+
!relation.isOneToOne() &&
|
|
80
|
+
hasSameColumns(relation.relatedProp, relation.ownerProp);
|
|
79
81
|
|
|
80
82
|
return [
|
|
81
83
|
...this._planLevels([...this.movedNodes]).map(({ nodes }) => ({ kind: 'move', nodes })),
|
|
82
84
|
{
|
|
83
85
|
kind: 'remove',
|
|
84
|
-
nodes: removedNodes.filter((node) => !isReferencedByOwner(node)),
|
|
86
|
+
nodes: removedNodes.filter((node) => !isReferencedByOwner(node) && !isOwnRow(node)),
|
|
85
87
|
areReferenced: true,
|
|
86
88
|
},
|
|
87
89
|
...insertSteps,
|
|
@@ -98,6 +100,11 @@ export class GraphSchedule {
|
|
|
98
100
|
);
|
|
99
101
|
}),
|
|
100
102
|
).map(({ nodes }) => ({ kind: 'patch', nodes })),
|
|
103
|
+
{
|
|
104
|
+
kind: 'remove',
|
|
105
|
+
nodes: removedNodes.filter(isOwnRow),
|
|
106
|
+
areReferenced: true,
|
|
107
|
+
},
|
|
101
108
|
{
|
|
102
109
|
kind: 'remove',
|
|
103
110
|
// Owners unrelate their BelongsToOne rows by patching their keys:
|
|
@@ -123,8 +130,8 @@ export class GraphSchedule {
|
|
|
123
130
|
// The keys of models with rows are known, also of related models that were
|
|
124
131
|
// fetched, except for the props that the graph fills in:
|
|
125
132
|
const hasRow = (node) => !!(node.referencedNode || node).existing;
|
|
126
|
-
// Nodes outside
|
|
127
|
-
//
|
|
133
|
+
// Nodes outside of `nodes` are written in other steps, or never, see
|
|
134
|
+
// `checkReferences()`:
|
|
128
135
|
const getPendingDependencies = (node) =>
|
|
129
136
|
(this.dependencies.get(node) || []).filter(
|
|
130
137
|
(dependency) =>
|
|
@@ -347,7 +354,6 @@ export class GraphSchedule {
|
|
|
347
354
|
// their cycle: the strongly connected components of these references, found
|
|
348
355
|
// with Tarjan's algorithm.
|
|
349
356
|
findReferenceCycles(nodes) {
|
|
350
|
-
const getRow = (node) => node.referencedNode || node;
|
|
351
357
|
const nodesByRow = groupBy(nodes, getRow);
|
|
352
358
|
const getReferencedNodes = (node) =>
|
|
353
359
|
(this.dependencies.get(node) || [])
|
|
@@ -453,8 +459,6 @@ function createDependencies(plan, keys) {
|
|
|
453
459
|
// A place waits for the inserts and `#ref{}` values that bind the slots of
|
|
454
460
|
// its keys. A dependency on the same row through the same key becomes such
|
|
455
461
|
// a wait, and stays deferrable to break cycles. The other waits aren't:
|
|
456
|
-
const getRow = (node) => node.referencedNode || node;
|
|
457
|
-
|
|
458
462
|
for (const { node: waitedNode, prop } of keys.waitsFor(node)) {
|
|
459
463
|
const dependency = (dependencies.get(node) || []).find(
|
|
460
464
|
(dependency) =>
|
|
@@ -507,6 +511,11 @@ function getSlotNode(node) {
|
|
|
507
511
|
: null;
|
|
508
512
|
}
|
|
509
513
|
|
|
514
|
+
// The row of `node`: a `#ref` model is the model it refers to.
|
|
515
|
+
function getRow(node) {
|
|
516
|
+
return node.referencedNode || node;
|
|
517
|
+
}
|
|
518
|
+
|
|
510
519
|
// Return the key props of `node` on its side of the relation of `dependency`.
|
|
511
520
|
function getKeyProps({ relation, related }, node) {
|
|
512
521
|
return (related === node ? relation.relatedProp : relation.ownerProp).props;
|