objection 3.8.0 → 3.9.1

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.
Files changed (47) hide show
  1. package/lib/model/AjvValidator.js +22 -6
  2. package/lib/model/Model.js +2 -0
  3. package/lib/model/graph/ModelGraphNode.js +2 -16
  4. package/lib/model/modelJsonAttributes.js +20 -11
  5. package/lib/model/modelQueryProps.js +78 -6
  6. package/lib/model/modelSet.js +6 -8
  7. package/lib/model/modelToJson.js +9 -2
  8. package/lib/model/modelUtils.js +1 -1
  9. package/lib/model/modelValidate.js +2 -1
  10. package/lib/model/modelValues.js +43 -50
  11. package/lib/queryBuilder/QueryBuilder.js +17 -7
  12. package/lib/queryBuilder/graph/GraphData.js +39 -0
  13. package/lib/queryBuilder/graph/GraphFetcher.js +7 -1
  14. package/lib/queryBuilder/graph/GraphOptions.js +66 -12
  15. package/lib/queryBuilder/graph/GraphUpsert.js +92 -10
  16. package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
  17. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
  18. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +7 -6
  19. package/lib/queryBuilder/graph/insert/GraphInsert.js +66 -22
  20. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -13
  21. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +16 -3
  22. package/lib/queryBuilder/join/JoinResultParser.js +13 -8
  23. package/lib/queryBuilder/join/RelationJoiner.js +3 -2
  24. package/lib/queryBuilder/join/TableNode.js +6 -68
  25. package/lib/queryBuilder/operations/DeleteOperation.js +20 -0
  26. package/lib/queryBuilder/operations/InsertOperation.js +4 -2
  27. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -0
  28. package/lib/queryBuilder/operations/LimitPerParentOperation.js +22 -15
  29. package/lib/queryBuilder/operations/UpdateOperation.js +40 -3
  30. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +1 -1
  31. package/lib/queryBuilder/operations/select/Selection.js +25 -3
  32. package/lib/relations/RelationFindOperation.js +7 -4
  33. package/lib/relations/RelationOwner.js +2 -5
  34. package/lib/relations/RelationProperty.js +3 -14
  35. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
  36. package/lib/relations/hasMany/HasManyInsertOperation.js +4 -0
  37. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +1 -1
  38. package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
  39. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +1 -16
  40. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +80 -21
  41. package/lib/utils/knexUtils.js +7 -0
  42. package/lib/utils/objectUtils.js +5 -0
  43. package/lib/utils/parseFieldExpression.js +17 -5
  44. package/lib/utils/promiseUtils/map.js +24 -7
  45. package/lib/utils/wrapDbError.js +14 -0
  46. package/package.json +1 -1
  47. package/typings/objection/index.d.ts +21 -11
@@ -161,8 +161,11 @@ class GraphOptions {
161
161
 
162
162
  return (
163
163
  !node &&
164
+ !hasIgnoredOtherModel(currentNode, graphData) &&
164
165
  !this._hasOption(currentNode, NO_UNRELATE) &&
165
- this.shouldUnrelateIgnoreDisable(currentNode)
166
+ (this.shouldUnrelateIgnoreDisable(currentNode) ||
167
+ // The unrelate replaces a delete, so `noDelete` disables it too.
168
+ (!this._hasOption(currentNode, NO_DELETE) && shouldUnrelateKeptRow(currentNode, graphData)))
166
169
  );
167
170
  }
168
171
 
@@ -173,8 +176,11 @@ class GraphOptions {
173
176
 
174
177
  return (
175
178
  !node &&
179
+ !hasIgnoredOtherModel(currentNode, graphData) &&
176
180
  !this._hasOption(currentNode, NO_DELETE) &&
177
- !this.shouldUnrelateIgnoreDisable(currentNode)
181
+ !this.shouldUnrelateIgnoreDisable(currentNode) &&
182
+ // Rows that the graph keeps in another place must not be deleted (#214).
183
+ !graphData.keptNodeForRow(currentNode)
178
184
  );
179
185
  }
180
186
 
@@ -182,6 +188,18 @@ class GraphOptions {
182
188
  return this.shouldInsert(node, graphData) || this.shouldRelate(node, graphData);
183
189
  }
184
190
 
191
+ // The graph ignores models that it neither inserts, relates nor patches, like
192
+ // models marked with `#unrelate` or `#delete` and models skipped by `noInsert`
193
+ // or `noRelate`. Inserted models have ids and can look like models to relate
194
+ // afterwards, so we check that they were inserted too.
195
+ shouldIgnore(node, graphData) {
196
+ return (
197
+ !node.userData.inserted &&
198
+ !this.shouldInsertOrRelate(node, graphData) &&
199
+ !this.shouldPatchOrUpdateIgnoreDisable(node, graphData)
200
+ );
201
+ }
202
+
185
203
  shouldDeleteOrUnrelate(currentNode, graphData) {
186
204
  return this.shouldDelete(currentNode, graphData) || this.shouldUnrelate(currentNode, graphData);
187
205
  }
@@ -264,27 +282,30 @@ function parseRelationExpression(optionName, expression) {
264
282
  }
265
283
 
266
284
  // Relation paths are matched by relation name, so aliases would never match.
267
- const alias = findAlias(parsedExpression.node);
285
+ const alias = findNode(parsedExpression.node, (node) => node.$name !== node.$relation);
268
286
 
269
287
  if (alias) {
270
288
  throw createError(`aliases like "${alias.$relation} as ${alias.$name}" are not supported`);
271
289
  }
272
290
 
291
+ // `*` and `^` only have a meaning at the end of a relation path. Anywhere else
292
+ // they are parsed as relation names that would never match.
293
+ const token = findNode(parsedExpression.node, (node) => /^(\*|\^\d*)$/.test(node.$relation));
294
+
295
+ if (token) {
296
+ throw createError(`"${token.$relation}" is only supported at the end of a relation path`);
297
+ }
298
+
273
299
  return parsedExpression;
274
300
  }
275
301
 
276
- function findAlias(node) {
302
+ function findNode(node, predicate) {
277
303
  for (const childName of node.$childNames) {
278
304
  const child = node[childName];
305
+ const found = predicate(child) ? child : findNode(child, predicate);
279
306
 
280
- if (child.$name !== child.$relation) {
281
- return child;
282
- }
283
-
284
- const alias = findAlias(child);
285
-
286
- if (alias) {
287
- return alias;
307
+ if (found) {
308
+ return found;
288
309
  }
289
310
  }
290
311
 
@@ -344,6 +365,39 @@ function hasRelateProp(node) {
344
365
  }
345
366
  }
346
367
 
368
+ // An ignored model that isn't the current model of a BelongsToOneRelation
369
+ // doesn't replace it, so we keep the current one instead of removing it as
370
+ // missing.
371
+ function hasIgnoredOtherModel(currentNode, graphData) {
372
+ const { relation } = currentNode.parentEdge;
373
+ const parentNode = getNode(currentNode.parentNode, graphData.graph);
374
+
375
+ return (
376
+ relation.isObjectionBelongsToOneRelation &&
377
+ !!parentNode &&
378
+ parentNode.edges.some(
379
+ (edge) =>
380
+ edge.relation === relation &&
381
+ edge.isOwnerNode(parentNode) &&
382
+ graphData.graphOptions.shouldIgnore(edge.relatedNode, graphData),
383
+ )
384
+ );
385
+ }
386
+
387
+ // A row that the graph keeps in another place is unrelated here instead of
388
+ // being deleted, unless it's a has-many row that the graph relates through the
389
+ // same relation: That relate moves it by overwriting the foreign key, which
390
+ // may not be nullable.
391
+ function shouldUnrelateKeptRow(currentNode, graphData) {
392
+ const keptNode = graphData.keptNodeForRow(currentNode);
393
+ const { relation } = currentNode.parentEdge;
394
+
395
+ return (
396
+ !!keptNode &&
397
+ !(relation.isObjectionHasManyRelation && keptNode.parentEdge?.relation === relation)
398
+ );
399
+ }
400
+
347
401
  function getNode(currentNode, graph) {
348
402
  if (!graph || !currentNode) {
349
403
  return null;
@@ -6,7 +6,7 @@ const { createNotModelError } = require('../../model/graph/ModelGraphBuilder');
6
6
  const { GraphFetcher } = require('../graph/GraphFetcher');
7
7
  const { GraphInsert } = require('./insert/GraphInsert');
8
8
  const { GraphPatch } = require('./patch/GraphPatch');
9
- const { GraphDelete } = require('./delete/GraphDelete');
9
+ const { GraphDelete, GraphDereferencedBelongsToOneDelete } = require('./delete/GraphDelete');
10
10
  const { GraphRecursiveUpsert } = require('./recursiveUpsert/GraphRecursiveUpsert');
11
11
  const { GraphOptions, OPTION_NAMES } = require('./GraphOptions');
12
12
  const { ValidationErrorType } = require('../../model/ValidationError');
@@ -37,7 +37,7 @@ class GraphUpsert {
37
37
  // by `ignore()` aren't returned, which leaves the root models without ids,
38
38
  // so that related rows would be inserted with missing or wrong foreign keys.
39
39
  // The clause is ignored, and a warning is logged once per method.
40
- // TODO: Throw an error instead in objection 4.0.
40
+ // TODO: Throw an error instead in objection 4.0 (#53).
41
41
  static warnOnConflict(builder, method) {
42
42
  if (builder.has(isOnConflictOperation)) {
43
43
  warnOnce(
@@ -60,13 +60,14 @@ class GraphUpsert {
60
60
  .then(pruneGraphs())
61
61
  .then(checkForJoinTableRelatedTableErrors(builder))
62
62
  .then(executeOperations(builder))
63
+ .then(removeReferenceProps(graph.nodes))
63
64
  .then(returnResult(this.objects, this.isArray));
64
65
  }
65
66
  }
66
67
 
67
68
  // Misspelled options are silently ignored, so the operation runs with the
68
69
  // default behaviour instead (#84). A warning is logged once per option.
69
- // TODO: Throw an error instead in objection 4.0.
70
+ // TODO: Throw an error instead in objection 4.0 (#53).
70
71
  function warnOnUnknownOptions(upsertOptions) {
71
72
  // Recursive upserts pass on the already checked options of their parent.
72
73
  if (upsertOptions instanceof GraphOptions) {
@@ -128,6 +129,7 @@ function fetchCurrentGraph(builder, graphOptions, graph) {
128
129
  function pruneGraphs() {
129
130
  return (graphData) => {
130
131
  pruneRelatedBranches(graphData);
132
+ pruneSkippedBranches(graphData);
131
133
 
132
134
  if (!graphData.graphOptions.isInsertOnly()) {
133
135
  pruneDeletedBranches(graphData);
@@ -149,6 +151,31 @@ function pruneRelatedBranches(graphData) {
149
151
  removeBranchesFromGraph(findRoots(relateNodes), graphData.graph);
150
152
  }
151
153
 
154
+ // Nothing is written below nodes that `noInsert` skips, as their descendants
155
+ // would be orphans (#215). Branches that references from outside point into
156
+ // are kept, since the references need their models.
157
+ function pruneSkippedBranches(graphData) {
158
+ const { graph, graphOptions } = graphData;
159
+
160
+ const skippedNodes = graph.nodes.filter((node) => {
161
+ return (
162
+ graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
163
+ !graphOptions.shouldInsert(node, graphData) &&
164
+ !isReferencedFromOutside(node.descendantRelationNodes)
165
+ );
166
+ });
167
+
168
+ removeBranchesFromGraph(findRoots(skippedNodes), graph);
169
+ }
170
+
171
+ function isReferencedFromOutside(nodes) {
172
+ const nodeSet = new Set(nodes);
173
+
174
+ return nodes.some((node) =>
175
+ node.refEdges.some((edge) => edge.isRelatedNode(node) && !nodeSet.has(edge.ownerNode)),
176
+ );
177
+ }
178
+
152
179
  function pruneDeletedBranches(graphData) {
153
180
  const { graph, currentGraph } = graphData;
154
181
 
@@ -253,6 +280,7 @@ function checkForErrors(builder) {
253
280
 
254
281
  if (graphData.graphOptions.isInsertOnly()) {
255
282
  checkForHasManyRelateErrors(graphData);
283
+ warnOnInsertBelowRelate(graphData);
256
284
  }
257
285
 
258
286
  return graphData;
@@ -290,10 +318,18 @@ function checkForJoinTableRelatedTableErrors(builder) {
290
318
 
291
319
  function checkForNotFoundErrors(graphData, builder) {
292
320
  const { graphOptions, currentGraph, graph } = graphData;
321
+ // The branches below related nodes are upserted and checked by nested
322
+ // `upsertGraph` calls, against the current graphs of the related nodes:
323
+ const nestedNodes = new Set(
324
+ graph.nodes
325
+ .filter((node) => graphOptions.shouldRelate(node, graphData))
326
+ .flatMap((node) => node.descendantRelationNodes),
327
+ );
293
328
 
294
329
  for (const node of graph.nodes) {
295
330
  if (
296
331
  node.obj.$hasId() &&
332
+ !nestedNodes.has(node) &&
297
333
  !node.toBeUnrelatedOrDeleted &&
298
334
  !graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
299
335
  !graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
@@ -400,18 +436,52 @@ function checkForHasManyRelateErrors(graphData) {
400
436
  }
401
437
  }
402
438
 
439
+ // `upsertGraph` upserts the branches below related models recursively, but
440
+ // `insertGraph` prunes them, which drops the models that would be inserted
441
+ // there (#217). A warning is logged once.
442
+ // TODO: Throw an error instead in objection 4.0 (#53).
443
+ function warnOnInsertBelowRelate(graphData) {
444
+ const { graph, graphOptions } = graphData;
445
+
446
+ const hasInsertBelowRelate = graph.nodes.some(
447
+ (node) =>
448
+ graphOptions.shouldRelate(node, graphData) &&
449
+ node.descendantRelationNodes.some((descendantNode) =>
450
+ graphOptions.shouldInsert(descendantNode, graphData),
451
+ ),
452
+ );
453
+
454
+ if (hasInsertBelowRelate) {
455
+ warnOnce(
456
+ 'insertGraph() ignores new models below related models. ' +
457
+ 'Insert them with a separate query or use upsertGraph() instead. ' +
458
+ 'This will throw in objection 4.0.',
459
+ );
460
+ }
461
+ }
462
+
403
463
  function executeOperations(builder) {
404
464
  return (graphData) => {
405
465
  const operations = graphData.graphOptions.isInsertOnly()
406
466
  ? [GraphInsert]
407
- : [GraphDelete, GraphInsert, GraphPatch, GraphRecursiveUpsert];
408
-
409
- return operations.reduce((promise, Operation) => {
410
- const operation = new Operation(graphData);
411
- const actions = operation.createActions();
467
+ : [
468
+ GraphDelete,
469
+ GraphInsert,
470
+ GraphPatch,
471
+ GraphDereferencedBelongsToOneDelete,
472
+ GraphRecursiveUpsert,
473
+ ];
474
+
475
+ // Create all actions before chaining the first ones, which would run even
476
+ // if creating the actions of a later operation throws, e.g. for a cycle.
477
+ const operationActions = operations.map((Operation) =>
478
+ new Operation(graphData).createActions(),
479
+ );
412
480
 
413
- return promise.then(() => executeActions(builder, actions));
414
- }, Promise.resolve());
481
+ return operationActions.reduce(
482
+ (promise, actions) => promise.then(() => executeActions(builder, actions)),
483
+ Promise.resolve(),
484
+ );
415
485
  };
416
486
  }
417
487
 
@@ -422,6 +492,18 @@ function executeActions(builder, actions) {
422
492
  );
423
493
  }
424
494
 
495
+ // The reference markers only describe the input graph, don't return them.
496
+ // `nodes` are the nodes before pruning, since the results of pruned branches
497
+ // contain markers too.
498
+ function removeReferenceProps(nodes) {
499
+ return () => {
500
+ for (const node of nodes) {
501
+ delete node.obj[node.modelClass.uidRefProp];
502
+ delete node.obj[node.modelClass.dbRefProp];
503
+ }
504
+ };
505
+ }
506
+
425
507
  function isOnConflictOperation(op) {
426
508
  return op.name === 'onConflict' || op.name === 'ignore' || op.name === 'merge';
427
509
  }
@@ -2,19 +2,72 @@
2
2
 
3
3
  const { GraphOperation } = require('../GraphOperation');
4
4
  const { GraphDeleteAction } = require('./GraphDeleteAction');
5
+ const { getPropertyTypes } = require('../../../model/modelJsonAttributes');
5
6
 
6
7
  class GraphDelete extends GraphOperation {
8
+ // The owner of a BelongsToOneRelation row references it until `GraphPatch`
9
+ // points the owner to a new row or to null. These rows are deleted
10
+ // afterwards by `GraphDereferencedBelongsToOneDelete`, all others before.
11
+ get deletesDereferencedRows() {
12
+ return false;
13
+ }
14
+
7
15
  createActions() {
8
16
  return [
9
17
  new GraphDeleteAction(this.graphData, {
10
- nodes: this.currentGraph.nodes.filter((currentNode) =>
11
- this.graphOptions.shouldDeleteOrUnrelate(currentNode, this.graphData),
18
+ nodes: this.currentGraph.nodes.filter(
19
+ (currentNode) =>
20
+ this.graphOptions.shouldDeleteOrUnrelate(currentNode, this.graphData) &&
21
+ isDereferencedBelongsToOneNode(currentNode, this.graphData) ===
22
+ this.deletesDereferencedRows,
12
23
  ),
24
+ areReferenced: !this.deletesDereferencedRows,
13
25
  }),
14
26
  ];
15
27
  }
16
28
  }
17
29
 
30
+ class GraphDereferencedBelongsToOneDelete extends GraphDelete {
31
+ get deletesDereferencedRows() {
32
+ return true;
33
+ }
34
+ }
35
+
36
+ function isDereferencedBelongsToOneNode(currentNode, graphData) {
37
+ const { graph, graphOptions } = graphData;
38
+ const relation = currentNode.parentEdge?.relation;
39
+ const parentNode = graph.nodeForNode(currentNode.parentNode);
40
+
41
+ if (relation?.isObjectionBelongsToOneRelation && parentNode) {
42
+ const node = graph.nodeForNode(currentNode);
43
+ const isReplaced = parentNode.edges.some(
44
+ (edge) =>
45
+ edge.relation === relation &&
46
+ edge.isOwnerNode(parentNode) &&
47
+ !graphOptions.shouldIgnore(edge.relatedNode, graphData),
48
+ );
49
+ const isRemoved =
50
+ parentNode.obj[relation.name] === null || (!!node && node.toBeUnrelatedOrDeleted);
51
+
52
+ // Removing the row sets the owner's foreign key to null, but only where
53
+ // its schema allows it. Otherwise we delete the row first and leave the
54
+ // owner's reference to the database, as the column may be `NOT NULL`.
55
+ return (
56
+ isReplaced ||
57
+ (isRemoved &&
58
+ relation.ownerProp.props.every((prop) => isNullable(relation.ownerModelClass, prop)))
59
+ );
60
+ } else {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ function isNullable(modelClass, prop) {
66
+ const schema = modelClass.getJsonSchema()?.properties?.[prop];
67
+ return !!schema && (schema.nullable === true || getPropertyTypes(schema).includes('null'));
68
+ }
69
+
18
70
  module.exports = {
19
71
  GraphDelete,
72
+ GraphDereferencedBelongsToOneDelete,
20
73
  };
@@ -5,10 +5,14 @@ const { groupBy } = require('../../../utils/objectUtils');
5
5
  const promiseUtils = require('../../../utils/promiseUtils');
6
6
 
7
7
  class GraphDeleteAction extends GraphAction {
8
- constructor(graphData, { nodes }) {
8
+ constructor(graphData, { nodes, areReferenced = true }) {
9
9
  super(graphData);
10
10
  // Nodes to delete.
11
11
  this.nodes = nodes;
12
+ // Whether the parents still reference the nodes. Rows of
13
+ // BelongsToOneRelations can be deleted after their owners were patched to
14
+ // reference new rows or null, so they can't be found through the relation.
15
+ this.areReferenced = areReferenced;
12
16
  }
13
17
 
14
18
  run(builder) {
@@ -47,10 +51,14 @@ class GraphDeleteAction extends GraphAction {
47
51
  );
48
52
 
49
53
  nodesByShouldUnrelate.forEach((nodes, shouldUnrelate) => {
50
- const builder = parentNode.obj.$relatedQuery(relation.name).childQueryOf(parentBuilder);
51
-
52
- if (!relation.isObjectionBelongsToOneRelation) {
53
- // This is useless in case of BelongsToOneRelation.
54
+ const builder = (
55
+ this.areReferenced
56
+ ? parentNode.obj.$relatedQuery(relation.name)
57
+ : relation.relatedModelClass.query()
58
+ ).childQueryOf(parentBuilder);
59
+
60
+ if (!this.areReferenced || !relation.isObjectionBelongsToOneRelation) {
61
+ // This is useless in case of a referenced BelongsToOneRelation.
54
62
  builder.findByIds(nodes.map((node) => node.obj.$id()));
55
63
  }
56
64
 
@@ -7,9 +7,8 @@ const promiseUtils = require('../../../utils/promiseUtils');
7
7
  /**
8
8
  * Patches in the foreign keys of `BelongsToOneRelation` edges that `GraphInsert`
9
9
  * had to defer to break a dependency cycle. The owner nodes of these edges were
10
- * inserted without the foreign keys. `GraphInsertAction` already copied them to
11
- * the owner objects once the related nodes were inserted, so all that's left to
12
- * do is one patch query per owner.
10
+ * inserted without the foreign keys, which are copied from the related nodes
11
+ * and patched in with one query per owner.
13
12
  */
14
13
  class DeferredRelationGraphInsertAction extends GraphAction {
15
14
  constructor(graphData, { edges }) {
@@ -36,11 +35,13 @@ class DeferredRelationGraphInsertAction extends GraphAction {
36
35
  const ownerIdProp = modelClass.getIdRelationProperty();
37
36
  const patch = {};
38
37
 
39
- for (const { relation } of edges) {
40
- const { ownerProp } = relation;
38
+ for (const { relation, relatedNode } of edges) {
39
+ const { ownerProp, relatedProp } = relation;
41
40
 
42
41
  ownerProp.forEach((i) => {
43
- ownerProp.patch(patch, i, ownerProp.getProp(ownerNode.obj, i));
42
+ const value = relatedProp.getProp(relatedNode.obj, i);
43
+ ownerProp.setProp(ownerNode.obj, i, value);
44
+ ownerProp.patch(patch, i, value);
44
45
  });
45
46
  }
46
47
 
@@ -46,7 +46,12 @@ class GraphInsert extends GraphOperation {
46
46
  // In case of HasManyRelation the related node depends on the owner node
47
47
  // because the related node has the foreign key.
48
48
  this._addDependency(edge.relatedNode, edge, dependencies);
49
- } else if (edge.relation.isObjectionBelongsToOneRelation) {
49
+ } else if (
50
+ edge.relation.isObjectionBelongsToOneRelation &&
51
+ // Ignored models don't replace the current one, so their keys must not
52
+ // be copied to the owner either.
53
+ !this.graphOptions.shouldIgnore(edge.relatedNode, this.graphData)
54
+ ) {
50
55
  // In case of BelongsToOneRelation the owner node depends on the related
51
56
  // node because the owner node has the foreign key.
52
57
  this._addDependency(edge.ownerNode, edge, dependencies);
@@ -78,10 +83,23 @@ class GraphInsert extends GraphOperation {
78
83
  // new ones. We need to pass all nodes to `GraphInsertActions` so that we
79
84
  // can resolve all dependencies.
80
85
  const nodesToInsert = this.graph.nodes.filter((node) => {
81
- return !this._isHandled(node, handledNodes) && !this._hasDependencies(node, handledNodes);
86
+ return (
87
+ !node.isReference && !handledNodes.has(node) && !this._hasDependencies(node, handledNodes)
88
+ );
89
+ });
90
+
91
+ // `#ref` nodes aren't inserted, so they can be resolved in the same
92
+ // action as the nodes they depend on, once these are inserted.
93
+ const insertedNodes = new Set([...handledNodes, ...nodesToInsert]);
94
+ const refNodes = this.graph.nodes.filter((node) => {
95
+ return (
96
+ node.isReference && !handledNodes.has(node) && !this._hasDependencies(node, insertedNodes)
97
+ );
82
98
  });
83
99
 
84
- if (nodesToInsert.length === 0) {
100
+ const nodes = [...nodesToInsert, ...refNodes];
101
+
102
+ if (nodes.length === 0) {
85
103
  if (handledNodes.size !== this.graph.nodes.length && this._breakCycle(handledNodes)) {
86
104
  continue;
87
105
  }
@@ -92,13 +110,14 @@ class GraphInsert extends GraphOperation {
92
110
  actions.push(
93
111
  new GraphInsertAction(this.graphData, {
94
112
  nodes: nodesToInsert,
113
+ refNodes,
95
114
  dependencies: this.dependencies,
96
115
  deferredEdges: this.deferredEdges,
97
116
  }),
98
117
  );
99
118
 
100
- for (const node of nodesToInsert) {
101
- this._markHandled(node, handledNodes);
119
+ for (const node of nodes) {
120
+ handledNodes.add(node);
102
121
  }
103
122
  }
104
123
 
@@ -116,7 +135,7 @@ class GraphInsert extends GraphOperation {
116
135
  // `#ref` nodes, since the graph is a tree otherwise.
117
136
  _breakCycle(handledNodes) {
118
137
  for (const node of this.graph.nodes) {
119
- if (this._isHandled(node, handledNodes)) {
138
+ if (handledNodes.has(node)) {
120
139
  continue;
121
140
  }
122
141
 
@@ -195,16 +214,12 @@ class GraphInsert extends GraphOperation {
195
214
  ];
196
215
  }
197
216
 
198
- _isHandled(node, handledNodes) {
199
- return handledNodes.has(node);
200
- }
201
-
202
217
  _hasDependencies(node, handledNodes) {
203
218
  return this._pendingDependencyEdges(node, handledNodes).length > 0;
204
219
  }
205
220
 
206
221
  // Returns the dependency edges of `node` whose other node still needs to be
207
- // inserted first. Deferred edges are ignored.
222
+ // handled first. Deferred edges are ignored.
208
223
  _pendingDependencyEdges(node, handledNodes) {
209
224
  const edges = this.dependencies.get(node) || [];
210
225
 
@@ -214,22 +229,51 @@ class GraphInsert extends GraphOperation {
214
229
  return (
215
230
  !this.deferredEdges.has(edge) &&
216
231
  !handledNodes.has(dependencyNode) &&
217
- !this.currentGraph.nodeForNode(dependencyNode)
232
+ (!this.currentGraph.nodeForNode(dependencyNode) ||
233
+ this._readsFilledProps(edge, dependencyNode))
218
234
  );
219
235
  });
220
236
  }
221
237
 
222
- _markHandled(node, handledNodes) {
223
- handledNodes.add(node);
224
-
225
- // The referencing nodes are all references that don't
226
- // represent any real entity. They are simply intermediate nodes
227
- // that depend on this node. Once this node is handled, we can
228
- // also mark those nodes as handled as there is nothing to actually
229
- // insert.
230
- for (const refNode of node.referencingNodes) {
231
- this._markHandled(refNode, handledNodes);
238
+ // Returns true if the reference `edge` reads props that the graph fills into
239
+ // `node`. The keys of existing models are known right away, but these props
240
+ // only once they're filled in (#356).
241
+ _readsFilledProps(edge, node) {
242
+ if (edge.type === ModelGraphEdge.Type.Relation) {
243
+ return false;
232
244
  }
245
+
246
+ const filledProps = this._getFilledProps(node);
247
+ // A `#ref` node stands in for `node` in its relations, which read only
248
+ // their keys from it:
249
+ const readProps =
250
+ edge.refType === ModelGraphEdge.ReferenceType.Property
251
+ ? [edge.refRelatedDataPath[0]]
252
+ : this._getRelationProps(edge.ownerNode.edges, edge.ownerNode);
253
+
254
+ return readProps.some((prop) => filledProps.includes(prop));
255
+ }
256
+
257
+ // Returns the props of `node` that the graph fills in from other models,
258
+ // through its relations and its property references.
259
+ _getFilledProps(node) {
260
+ const edges = this.dependencies.get(node) || [];
261
+
262
+ return [
263
+ ...this._getRelationProps(edges, node),
264
+ ...edges
265
+ .filter((edge) => edge.refType === ModelGraphEdge.ReferenceType.Property)
266
+ .map((edge) => edge.refOwnerDataPath[0]),
267
+ ];
268
+ }
269
+
270
+ // Returns the props of `node` that the relation edges among `edges` join on.
271
+ _getRelationProps(edges, node) {
272
+ return edges
273
+ .filter((edge) => edge.type === ModelGraphEdge.Type.Relation)
274
+ .flatMap((edge) => {
275
+ return (edge.isOwnerNode(node) ? edge.relation.ownerProp : edge.relation.relatedProp).props;
276
+ });
233
277
  }
234
278
 
235
279
  _createJoinRowActions() {
@@ -14,22 +14,24 @@ const promiseUtils = require('../../../utils/promiseUtils');
14
14
  * when needed.
15
15
  */
16
16
  class GraphInsertAction extends GraphAction {
17
- constructor(graphData, { nodes, dependencies, deferredEdges }) {
17
+ constructor(graphData, { nodes, refNodes, dependencies, deferredEdges }) {
18
18
  super(graphData);
19
19
 
20
- // Nodes to insert.
20
+ // Nodes to insert, and `#ref` nodes to resolve once they are inserted.
21
21
  this.nodes = nodes;
22
+ this.refNodes = refNodes;
22
23
  this.dependencies = dependencies;
23
24
  // Dependency edges that are resolved once the dependency node has been
24
25
  // inserted, and are patched in by `DeferredRelationGraphInsertAction`.
25
26
  this.deferredEdges = deferredEdges;
26
27
  }
27
28
 
28
- run(builder) {
29
+ async run(builder) {
29
30
  const batches = this._createInsertBatches(builder);
30
31
  const concurrency = this._getConcurrency(builder, this.nodes);
31
32
 
32
- return promiseUtils.map(batches, (batch) => this._insertBatch(builder, batch), { concurrency });
33
+ await promiseUtils.map(batches, (batch) => this._insertBatch(builder, batch), { concurrency });
34
+ this._resolveDependencies(this.refNodes);
33
35
  }
34
36
 
35
37
  _createInsertBatches(builder) {
@@ -49,7 +51,6 @@ class GraphInsertAction extends GraphAction {
49
51
  async _insertBatch(parentBuilder, nodes) {
50
52
  await this._beforeInsert(nodes);
51
53
  await this._insert(parentBuilder, nodes);
52
- await this._afterInsert(nodes);
53
54
  }
54
55
 
55
56
  _beforeInsert(nodes) {
@@ -82,6 +83,8 @@ class GraphInsertAction extends GraphAction {
82
83
  _resolveDependency(dependencyNode, edge) {
83
84
  if (edge.type === ModelGraphEdge.Type.Relation && !edge.relation.joinTable) {
84
85
  this._resolveRelationDependency(dependencyNode, edge);
86
+ } else if (edge.refType === ModelGraphEdge.ReferenceType.Object) {
87
+ this._resolveReferences(edge.getOtherNode(dependencyNode));
85
88
  } else if (edge.refType === ModelGraphEdge.ReferenceType.Property) {
86
89
  this._resolvePropertyReferenceNode(dependencyNode, edge);
87
90
  }
@@ -219,14 +222,6 @@ class GraphInsertAction extends GraphAction {
219
222
  }),
220
223
  );
221
224
  }
222
-
223
- _afterInsert(nodes) {
224
- for (const node of nodes) {
225
- for (const refNode of node.referencingNodes) {
226
- this._resolveDependency(refNode, refNode.parentEdge);
227
- }
228
- }
229
- }
230
225
  }
231
226
 
232
227
  function getModelClass(node) {