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.
@@ -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, keptNodes = new Map(), laterNodes = new Set() }) {
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.actions = null;
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
- // Leave `nodes` to a later pass of the graph.
38
- defer(nodes) {
39
- this.deferredNodes = new Set(nodes);
40
- this.nodes = this.nodes.filter((node) => !this.deferredNodes.has(node));
41
- this._activeNodes = null;
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
- cacheDecisions() {
45
- this._intents = new Map();
46
- this._nodeActions = new Map();
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
- return node.toBeUnrelatedOrDeleted
69
- ? 'remove'
70
- : node.isReference
71
- ? 'relate'
72
- : node.isMissing
73
- ? insertMissing
74
- ? 'insert'
75
- : this.findInsertingNode(node)
76
- ? 'relate'
77
- : 'missing'
78
- : node.isDbReference || this._canRelate(node)
79
- ? 'relate'
80
- : node.current
81
- ? 'patch'
82
- : !node.hasId || insertMissing
83
- ? 'insert'
84
- : 'missing';
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
- prune() {
192
+ _prune() {
186
193
  const isInsertOnly = this.graphOptions.isInsertOnly();
187
- const passNodes = new Set([...this.nodes, ...this.currentNodes]);
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.filter((node) => passNodes.has(node))) {
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, before the first write, since the
297
- // writes change the models that the rules read. Only call this after
298
- // pruning.
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, or that an earlier pass
340
- // inserted, is unrelated here, unless one of these places sets the same key
341
- // column, by relating, patching or inserting it through such a relation, or
342
- // through its own BelongsToOneRelation, e.g. `parent` for `children`: Then
343
- // that place writes the key, which may not be nullable. A has-one row that
344
- // its own BelongsToOneRelation or a later pass keeps is still unrelated if
345
- // this pass fills its slot with another row, as the foreign key may be
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
- ((isKeptByOwner || this.deferredNodes.has(keptNode)) && this._hasNewRowInSlot(currentNode))
363
+ (isKeptByOwner && this._hasNewRowInSlot(currentNode))
362
364
  );
363
365
  }
364
366
 
365
- // Return true if this pass inserts or relates a row into the slot of the
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 nodes that keep the row of `node` in other places of the graph, by
380
- // relating or patching it there, or that an earlier pass inserted.
381
- getKeptNodes(node) {
382
- return this.keptNodes.get(node.rowKey) || [];
383
- }
384
-
385
- // The nodes that keep their rows in the next passes, once this one is
386
- // written: the ones that it decided to keep before its writes, and the ones
387
- // that it inserts.
388
- getNextKeptNodes() {
389
- const keptNodes = new Map(this.keptNodes);
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 their rows by relating or patching them, by row, also
401
- // the ones that earlier passes decided to keep or inserted. A row may be kept
402
- // in several places.
403
- get keptNodes() {
404
- if (!this._keptNodesByRowKey) {
405
- this._keptNodesByRowKey = new Map(this.earlierKeptNodes);
406
-
407
- for (const node of [...this.activeNodes, ...this.deferredNodes]) {
408
- const action = this.actionFor(node);
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. Remove the BelongsToOne rows that the owners no longer reference.
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, and the ones of
29
- // owners that earlier passes wrote, whose keys are patched in once their
30
- // BelongsToOne rows of this pass are written. Deferred keys are written
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 this pass were written by an earlier one, or are never
127
- // written, see `checkReferences()`:
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;