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.
@@ -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, keptNodes = new Map() }) {
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.actions = null;
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
- // Leave `nodes` to a later pass of the graph.
33
- defer(nodes) {
34
- this.deferredNodes = new Set(nodes);
35
- this.nodes = this.nodes.filter((node) => !this.deferredNodes.has(node));
36
- this._activeNodes = null;
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
- cacheDecisions() {
40
- this._intents = new Map();
41
- 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;
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
- return node.toBeUnrelatedOrDeleted
64
- ? 'remove'
65
- : node.isReference
66
- ? 'relate'
67
- : node.isMissing
68
- ? insertMissing
69
- ? 'insert'
70
- : this.findInsertingNode(node)
71
- ? 'relate'
72
- : 'missing'
73
- : node.isDbReference || this._canRelate(node)
74
- ? 'relate'
75
- : node.current
76
- ? 'patch'
77
- : !node.hasId || insertMissing
78
- ? 'insert'
79
- : '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
+ );
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 keptNode = this.keptNodeForRow(currentNode);
175
+ const keptNodes = this.getKeptNodes(currentNode);
150
176
 
151
- if (keptNode && !this._isUnrelatedWhenKept(currentNode, keptNode)) {
177
+ if (keptNodes.some((keptNode) => !this._isUnrelatedWhenKept(currentNode, keptNode))) {
152
178
  return 'keep';
153
- } else if (keptNode || has('unrelate')) {
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
- prune() {
192
+ _prune() {
167
193
  const isInsertOnly = this.graphOptions.isInsertOnly();
168
- const passNodes = new Set([...this.nodes, ...this.currentNodes]);
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.filter((node) => passNodes.has(node))) {
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, before the first write, since the
278
- // writes change the models that the rules read. Only call this after
279
- // 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`.
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 the
321
- // other place relates or patches it through the same key column: Then that
322
- // place writes the key, which may not be nullable. A has-one row that a
323
- // later pass moves is still unrelated if this pass writes another row into
324
- // its slot, as the foreign key may be 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).
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
- !hasSameKey(relation, keptNode.relation) ||
330
- (this.deferredNodes.has(keptNode) && this._hasNewRowInSlot(currentNode))
362
+ !(isKeptInRelation || isKeptByOwner) ||
363
+ (isKeptByOwner && this._hasNewRowInSlot(currentNode))
331
364
  );
332
365
  }
333
366
 
334
- // 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
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
- // Find the node that keeps the row of `currentNode` in another place of the
349
- // graph, by relating or patching it there, also in another pass.
350
- keptNodeForRow(currentNode) {
351
- return this.keptNodes.get(currentNode.rowKey) || null;
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 their rows, by row, also the ones that earlier passes
355
- // decided to keep.
356
- get keptNodes() {
357
- if (!this._keptNodesByRowKey) {
358
- this._keptNodesByRowKey = new Map(this.earlierKeptNodes);
359
-
360
- for (const node of [...this.activeNodes, ...this.deferredNodes]) {
361
- const action = this.actionFor(node);
362
-
363
- if (node.obj.$hasId() && (action === 'relate' || action === 'patch')) {
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