objection 4.0.0-rc.1 → 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 +204 -119
- package/lib/queryBuilder/graph/GraphNode.js +16 -3
- package/lib/queryBuilder/graph/GraphOptions.js +24 -49
- package/lib/queryBuilder/graph/GraphPlan.js +120 -100
- package/lib/queryBuilder/graph/GraphSchedule.js +522 -0
- package/lib/queryBuilder/graph/GraphUpsert.js +47 -494
- package/lib/queryBuilder/graph/GraphWriter.js +250 -762
- package/lib/queryBuilder/graph/graphChecks.js +497 -0
- package/lib/queryBuilder/graph/graphKeys.js +634 -0
- package/package.json +2 -2
- package/typings/objection/index.d.ts +437 -85
|
@@ -1,22 +1,14 @@
|
|
|
1
|
+
import { hasOwnerKey, hasSameColumns, hasSameKey } from './graphKeys.js';
|
|
2
|
+
import { groupBy } from '../../utils/objectUtils.js';
|
|
3
|
+
|
|
1
4
|
// What a graph insert or upsert does with each model of the graph and each row
|
|
2
5
|
// of the current graph: The rules that decide it, and the pruning of branches
|
|
3
6
|
// that nothing is written in.
|
|
4
7
|
export class GraphPlan {
|
|
5
|
-
constructor({ nodes, graphOptions
|
|
8
|
+
constructor({ nodes, graphOptions }) {
|
|
6
9
|
this.nodes = nodes;
|
|
7
|
-
this.currentNodes = [];
|
|
8
|
-
this.deferredNodes = new Set();
|
|
9
|
-
// The nodes that earlier passes of the graph decided to keep, by row:
|
|
10
|
-
this.earlierKeptNodes = keptNodes;
|
|
11
10
|
this.graphOptions = graphOptions;
|
|
12
|
-
this.
|
|
13
|
-
// The decisions only change while the current graph is loaded, and are
|
|
14
|
-
// cached once it is, see `cacheDecisions()`:
|
|
15
|
-
this._intents = null;
|
|
16
|
-
this._nodeActions = null;
|
|
17
|
-
this._activeNodes = null;
|
|
18
|
-
this._activeCurrentNodes = null;
|
|
19
|
-
this._keptNodesByRowKey = null;
|
|
11
|
+
this.reset();
|
|
20
12
|
}
|
|
21
13
|
|
|
22
14
|
get activeNodes() {
|
|
@@ -29,16 +21,24 @@ export class GraphPlan {
|
|
|
29
21
|
));
|
|
30
22
|
}
|
|
31
23
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.
|
|
37
|
-
|
|
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
|
+
}
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
this.
|
|
41
|
-
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;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// What the data asks to do with `node`, before the options that disable it:
|
|
@@ -60,23 +60,49 @@ export class GraphPlan {
|
|
|
60
60
|
_getIntent(node) {
|
|
61
61
|
const insertMissing = this.graphOptions.has(node, 'insertMissing');
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Return true if the model of the `#ref` model `node` writes what `node`
|
|
95
|
+
// would write to the row: The model is inserted, or it's written and its path
|
|
96
|
+
// allows any write that the path of `node` allows.
|
|
97
|
+
isCoveredByModel(node) {
|
|
98
|
+
const { actions, graphOptions } = this;
|
|
99
|
+
const { referencedNode } = node;
|
|
100
|
+
const action = actions.get(referencedNode);
|
|
101
|
+
const allowsWrite = (node) => graphOptions.shouldPatch(node) || graphOptions.shouldUpdate(node);
|
|
102
|
+
return (
|
|
103
|
+
action === 'insert' ||
|
|
104
|
+
(['relate', 'patch'].includes(action) && (allowsWrite(referencedNode) || !allowsWrite(node)))
|
|
105
|
+
);
|
|
80
106
|
}
|
|
81
107
|
|
|
82
108
|
// Return true if the graph writes the branch below `node`: below models at
|
|
@@ -146,11 +172,11 @@ export class GraphPlan {
|
|
|
146
172
|
} else if (this._hasIgnoredOtherModel(currentNode)) {
|
|
147
173
|
return 'keep';
|
|
148
174
|
} else {
|
|
149
|
-
const
|
|
175
|
+
const keptNodes = this.getKeptNodes(currentNode);
|
|
150
176
|
|
|
151
|
-
if (keptNode
|
|
177
|
+
if (keptNodes.some((keptNode) => !this._isUnrelatedWhenKept(currentNode, keptNode))) {
|
|
152
178
|
return 'keep';
|
|
153
|
-
} else if (
|
|
179
|
+
} else if (keptNodes.length > 0 || has('unrelate')) {
|
|
154
180
|
// Rows that the graph keeps in another place must not be deleted
|
|
155
181
|
// (#214). They're unrelated instead, which replaces the delete, so
|
|
156
182
|
// `noDelete` disables it too, unless the `unrelate` option asks for it.
|
|
@@ -163,13 +189,11 @@ export class GraphPlan {
|
|
|
163
189
|
|
|
164
190
|
// Leave out the branches that require no writes. We never do anything below
|
|
165
191
|
// models that are removed, and we never delete recursively.
|
|
166
|
-
|
|
192
|
+
_prune() {
|
|
167
193
|
const isInsertOnly = this.graphOptions.isInsertOnly();
|
|
168
|
-
|
|
169
|
-
// The rows that were fetched for pruned nodes are left alone too. Nodes of
|
|
170
|
-
// later passes aren't pruned here:
|
|
194
|
+
// The rows that were fetched for pruned nodes are left alone too:
|
|
171
195
|
const pruneNodes = (nodes) => {
|
|
172
|
-
for (const node of nodes
|
|
196
|
+
for (const node of nodes) {
|
|
173
197
|
node.isPruned = true;
|
|
174
198
|
|
|
175
199
|
for (const row of node.row ? [node.row, ...node.row.descendants] : []) {
|
|
@@ -191,10 +215,7 @@ export class GraphPlan {
|
|
|
191
215
|
(node) =>
|
|
192
216
|
!node.current &&
|
|
193
217
|
this.intentFor(node) !== 'insert' &&
|
|
194
|
-
!(
|
|
195
|
-
this.actionFor(node) === 'relate' &&
|
|
196
|
-
(node.row || (node.isReference && !this.graphOptions.isInsertOnly()))
|
|
197
|
-
),
|
|
218
|
+
!(this.actionFor(node) === 'relate' && (node.row || (node.isReference && !isInsertOnly))),
|
|
198
219
|
),
|
|
199
220
|
);
|
|
200
221
|
|
|
@@ -270,14 +291,16 @@ export class GraphPlan {
|
|
|
270
291
|
pruneDescendants(markedNodes);
|
|
271
292
|
pruneDescendants(markedNodes.map((node) => node.current).filter(Boolean));
|
|
272
293
|
}
|
|
273
|
-
|
|
274
|
-
return this;
|
|
275
294
|
}
|
|
276
295
|
|
|
277
|
-
// Decide what to do with each node once
|
|
278
|
-
// writes change the models that the rules
|
|
279
|
-
//
|
|
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`.
|
|
280
300
|
plan() {
|
|
301
|
+
this._intents = new Map();
|
|
302
|
+
this._nodeActions = new Map();
|
|
303
|
+
this._prune();
|
|
281
304
|
const actions = new Map();
|
|
282
305
|
|
|
283
306
|
for (const node of this.activeNodes) {
|
|
@@ -289,7 +312,6 @@ export class GraphPlan {
|
|
|
289
312
|
}
|
|
290
313
|
|
|
291
314
|
this.actions = actions;
|
|
292
|
-
return this;
|
|
293
315
|
}
|
|
294
316
|
|
|
295
317
|
_canRelate(node) {
|
|
@@ -317,21 +339,32 @@ export class GraphPlan {
|
|
|
317
339
|
);
|
|
318
340
|
}
|
|
319
341
|
|
|
320
|
-
// A row that the graph keeps in another place is unrelated here, unless
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
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).
|
|
325
349
|
_isUnrelatedWhenKept(currentNode, keptNode) {
|
|
326
350
|
const { relation } = currentNode;
|
|
351
|
+
const isKeptInRelation = hasSameKey(relation, keptNode.relation);
|
|
352
|
+
const isKeptByOwner =
|
|
353
|
+
!isKeptInRelation &&
|
|
354
|
+
keptNode.activeChildren.some(
|
|
355
|
+
(child) =>
|
|
356
|
+
child.relation.keySide === 'owner' &&
|
|
357
|
+
this.actionFor(child) !== 'ignore' &&
|
|
358
|
+
hasSameColumns(child.relation.ownerProp, relation.relatedProp),
|
|
359
|
+
);
|
|
327
360
|
return (
|
|
328
361
|
relation.keySide !== 'related' ||
|
|
329
|
-
!
|
|
330
|
-
(
|
|
362
|
+
!(isKeptInRelation || isKeptByOwner) ||
|
|
363
|
+
(isKeptByOwner && this._hasNewRowInSlot(currentNode))
|
|
331
364
|
);
|
|
332
365
|
}
|
|
333
366
|
|
|
334
|
-
// Return true if
|
|
367
|
+
// Return true if the graph inserts or relates a row into the slot of the
|
|
335
368
|
// has-one relation that `currentNode` is in.
|
|
336
369
|
_hasNewRowInSlot(currentNode) {
|
|
337
370
|
return (
|
|
@@ -345,46 +378,33 @@ export class GraphPlan {
|
|
|
345
378
|
);
|
|
346
379
|
}
|
|
347
380
|
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
return
|
|
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);
|
|
352
393
|
}
|
|
353
394
|
|
|
354
|
-
// The nodes that keep
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
this.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
this._keptNodesByRowKey.set(node.rowKey, node);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
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) || [];
|
|
370
405
|
}
|
|
371
406
|
}
|
|
372
407
|
|
|
373
|
-
// Return true if both relations keep the keys of their related rows in the same
|
|
374
|
-
// columns, also the same JSON paths of them.
|
|
375
|
-
function hasSameKey(relation, otherRelation) {
|
|
376
|
-
const { relatedProp } = relation;
|
|
377
|
-
const otherProp = otherRelation?.keySide === 'related' ? otherRelation.relatedProp : null;
|
|
378
|
-
return (
|
|
379
|
-
!!otherProp &&
|
|
380
|
-
relation.relatedModelClass.getTableName() === otherRelation.relatedModelClass.getTableName() &&
|
|
381
|
-
relatedProp.size === otherProp.size &&
|
|
382
|
-
relatedProp.props.every(
|
|
383
|
-
(_, i) => relatedProp.propDescription(i) === otherProp.propDescription(i),
|
|
384
|
-
)
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
408
|
// For `HasMany` and `HasOne` relations, the relate prop is the id of the
|
|
389
409
|
// related model. Use the id state of the input graph, since the parent's
|
|
390
410
|
// insert copies the foreign key into the related model, which can complete
|