objection 3.1.5 → 3.3.0

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 (69) hide show
  1. package/README.md +16 -16
  2. package/lib/model/AjvValidator.js +173 -18
  3. package/lib/model/Model.js +3 -1
  4. package/lib/model/graph/ModelGraphNode.js +12 -0
  5. package/lib/model/modelToJson.js +2 -1
  6. package/lib/queryBuilder/QueryBuilder.js +339 -47
  7. package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
  8. package/lib/queryBuilder/ReferenceBuilder.js +5 -3
  9. package/lib/queryBuilder/RelationExpression.js +21 -9
  10. package/lib/queryBuilder/StaticHookArguments.js +12 -1
  11. package/lib/queryBuilder/graph/GraphFetcher.js +3 -1
  12. package/lib/queryBuilder/graph/GraphOptions.js +21 -2
  13. package/lib/queryBuilder/graph/GraphUpsert.js +52 -0
  14. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +20 -14
  15. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
  16. package/lib/queryBuilder/graph/insert/GraphInsert.js +113 -8
  17. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +10 -1
  18. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +15 -3
  19. package/lib/queryBuilder/join/JoinResultParser.js +22 -0
  20. package/lib/queryBuilder/join/RelationJoiner.js +71 -5
  21. package/lib/queryBuilder/operations/DelegateOperation.js +4 -0
  22. package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
  23. package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
  24. package/lib/queryBuilder/operations/InsertOperation.js +121 -12
  25. package/lib/queryBuilder/operations/NoneOperation.js +37 -0
  26. package/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js +5 -0
  27. package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
  28. package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
  29. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +20 -0
  30. package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
  31. package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
  32. package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
  33. package/lib/queryBuilder/operations/eager/EagerOperation.js +7 -5
  34. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +4 -2
  35. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +9 -20
  36. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
  37. package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
  38. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +45 -32
  39. package/lib/queryBuilder/operations/select/Selection.js +95 -4
  40. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
  41. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
  42. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
  43. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
  44. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
  45. package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +9 -0
  46. package/lib/queryBuilder/whereGroups.js +125 -0
  47. package/lib/relations/Relation.js +5 -2
  48. package/lib/relations/RelationProperty.js +7 -3
  49. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +1 -0
  50. package/lib/relations/belongsToOne/BelongsToOneRelateOperation.js +5 -0
  51. package/lib/relations/hasMany/HasManyRelateOperation.js +6 -2
  52. package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
  53. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
  54. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
  55. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
  56. package/lib/relations/manyToMany/ManyToManyRelation.js +70 -48
  57. package/lib/relations/manyToMany/relate/ManyToManyRelateOperation.js +6 -0
  58. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
  59. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
  60. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
  61. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
  62. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +8 -2
  63. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
  64. package/lib/utils/deprecate.js +6 -0
  65. package/lib/utils/identifierMapping.js +23 -3
  66. package/lib/utils/objectUtils.js +9 -5
  67. package/lib/utils/parseFieldExpression.js +28 -0
  68. package/package.json +22 -14
  69. package/typings/objection/index.d.ts +339 -68
@@ -24,7 +24,13 @@ class StaticHookArguments {
24
24
 
25
25
  get asFindQuery() {
26
26
  return () => {
27
- return this[BUILDER_SYMBOL].toFindQuery().clearWithGraphFetched().runAfter(asArray);
27
+ // The `runAfter()` callbacks of the query being executed (including the
28
+ // ones installed by `throwIfNotFound()` and `traverse()`) are meant for
29
+ // its result, not for the result of this find query.
30
+ return this[BUILDER_SYMBOL].toFindQuery()
31
+ .clear('runAfter')
32
+ .clearWithGraphFetched()
33
+ .runAfter(toArray);
28
34
  };
29
35
  }
30
36
 
@@ -97,6 +103,11 @@ class StaticHookArguments {
97
103
  }
98
104
  }
99
105
 
106
+ // Like `asArray()`, but an empty `first()` result becomes an empty array.
107
+ function toArray(result) {
108
+ return result === undefined ? [] : asArray(result);
109
+ }
110
+
100
111
  function getRelation(op) {
101
112
  return op.relation;
102
113
  }
@@ -141,7 +141,9 @@ function shouldSelectColumn(column, selects, node) {
141
141
  !selects.has(column) &&
142
142
  column !== modelClass.propertyNameToColumnName(modelClass.dbRefProp) &&
143
143
  column !== modelClass.propertyNameToColumnName(modelClass.uidRefProp) &&
144
- column !== modelClass.propertyNameToColumnName(modelClass.uidProp)
144
+ column !== modelClass.propertyNameToColumnName(modelClass.uidProp) &&
145
+ column !== modelClass.propertyNameToColumnName(modelClass.graphUnrelateProp) &&
146
+ column !== modelClass.propertyNameToColumnName(modelClass.graphDeleteProp)
145
147
  );
146
148
  }
147
149
 
@@ -51,6 +51,10 @@ class GraphOptions {
51
51
 
52
52
  // Like `shouldRelate` but ignores settings that explicitly disable relate operations.
53
53
  shouldRelateIgnoreDisable(node, graphData) {
54
+ if (node.toBeUnrelated || node.toBeDeleted) {
55
+ return false;
56
+ }
57
+
54
58
  if (node.isReference || node.isDbReference) {
55
59
  return true;
56
60
  }
@@ -71,6 +75,10 @@ class GraphOptions {
71
75
 
72
76
  // Like `shouldInsert` but ignores settings that explicitly disable insert operations.
73
77
  shouldInsertIgnoreDisable(node, graphData) {
78
+ if (node.toBeUnrelated || node.toBeDeleted) {
79
+ return false;
80
+ }
81
+
74
82
  return (
75
83
  !getCurrentNode(node, graphData) &&
76
84
  !this.shouldRelateIgnoreDisable(node, graphData) &&
@@ -89,6 +97,10 @@ class GraphOptions {
89
97
  // Like `shouldPatch() || shouldUpdate()` but ignores settings that explicitly disable
90
98
  // update or patch operations.
91
99
  shouldPatchOrUpdateIgnoreDisable(node, graphData) {
100
+ if (node.toBeUnrelated || node.toBeDeleted) {
101
+ return false;
102
+ }
103
+
92
104
  if (this.shouldRelate(node, graphData)) {
93
105
  // We should update all nodes that are going to be related. Note that
94
106
  // we don't actually update anything unless there is something to update
@@ -121,16 +133,23 @@ class GraphOptions {
121
133
  }
122
134
 
123
135
  shouldUnrelate(currentNode, graphData) {
136
+ const node = getNode(currentNode, graphData.graph);
137
+ if (node && node.toBeUnrelated) return true;
138
+
124
139
  return (
125
- !getNode(currentNode, graphData.graph) &&
140
+ !node &&
126
141
  !this._hasOption(currentNode, NO_UNRELATE) &&
127
142
  this.shouldUnrelateIgnoreDisable(currentNode)
128
143
  );
129
144
  }
130
145
 
131
146
  shouldDelete(currentNode, graphData) {
147
+ const node = getNode(currentNode, graphData.graph);
148
+ // `#unrelate` takes precedence over `#delete` if both are set.
149
+ if (node && node.toBeDeleted && !node.toBeUnrelated) return true;
150
+
132
151
  return (
133
- !getNode(currentNode, graphData.graph) &&
152
+ !node &&
134
153
  !this._hasOption(currentNode, NO_DELETE) &&
135
154
  !this.shouldUnrelateIgnoreDisable(currentNode)
136
155
  );
@@ -14,6 +14,7 @@ const { RelationExpression } = require('../RelationExpression');
14
14
  const { GraphNodeDbExistence } = require('./GraphNodeDbExistence');
15
15
  const { GraphData } = require('./GraphData');
16
16
  const { uniqBy, asArray, isObject } = require('../../utils/objectUtils');
17
+ const { deprecate } = require('../../utils/deprecate');
17
18
 
18
19
  class GraphUpsert {
19
20
  constructor({ rootModelClass, objects, upsertOptions }) {
@@ -28,6 +29,23 @@ class GraphUpsert {
28
29
  return { skipValidation: true };
29
30
  }
30
31
 
32
+ // A graph is written using many insert queries (one per batch and table),
33
+ // so there's no single insert query an `onConflict` clause could be applied
34
+ // to. Even applying it to the root table only would be unsound: rows skipped
35
+ // by `ignore()` aren't returned, which leaves the root models without ids,
36
+ // so that related rows would be inserted with missing or wrong foreign keys.
37
+ // The clause is ignored, and a warning is logged once per method.
38
+ // TODO: Throw an error instead in objection 4.0.
39
+ static warnOnConflict(builder, method) {
40
+ if (builder.has(isOnConflictOperation)) {
41
+ deprecate(
42
+ `onConflict(), ignore() and merge() are not supported by ${method}(). ` +
43
+ 'Insert the conflicting rows with a separate insert() query instead. ' +
44
+ 'This will throw in objection 4.0.',
45
+ );
46
+ }
47
+ }
48
+
31
49
  run(builder) {
32
50
  const modelClass = builder.modelClass();
33
51
  const graphOptions = new GraphOptions(this.upsertOpt);
@@ -91,6 +109,7 @@ function pruneGraphs() {
91
109
 
92
110
  if (!graphData.graphOptions.isInsertOnly()) {
93
111
  pruneDeletedBranches(graphData);
112
+ pruneUnrelatedOrDeletedBranches(graphData);
94
113
  }
95
114
 
96
115
  return graphData;
@@ -136,6 +155,19 @@ function pruneDeletedBranches(graphData) {
136
155
  removeNodesFromGraph(new Set(rootsNotInRelation), currentGraph);
137
156
  }
138
157
 
158
+ // Models marked with `#unrelate` or `#delete` are unrelated or deleted
159
+ // as a whole. Their relations are ignored, both in the upserted graph
160
+ // and in the current graph.
161
+ function pruneUnrelatedOrDeletedBranches(graphData) {
162
+ const { graph, currentGraph } = graphData;
163
+
164
+ const roots = findRoots(graph.nodes.filter((node) => node.toBeUnrelatedOrDeleted));
165
+ const currentRoots = roots.map((node) => currentGraph.nodeForNode(node)).filter(Boolean);
166
+
167
+ removeBranchesFromGraph(roots, graph);
168
+ removeBranchesFromGraph(currentRoots, currentGraph);
169
+ }
170
+
139
171
  function findRoots(nodes) {
140
172
  const nodeSet = new Set(nodes);
141
173
 
@@ -192,6 +224,7 @@ function checkForErrors(builder) {
192
224
  checkForNotFoundErrors(graphData, builder);
193
225
  checkForUnallowedRelationErrors(graphData, builder);
194
226
  checkForUnallowedReferenceErrors(graphData, builder);
227
+ checkForRootUnrelateOrDeleteErrors(graphData, builder);
195
228
 
196
229
  if (graphData.graphOptions.isInsertOnly()) {
197
230
  checkForHasManyRelateErrors(graphData);
@@ -207,6 +240,8 @@ function checkForNotFoundErrors(graphData, builder) {
207
240
  for (const node of graph.nodes) {
208
241
  if (
209
242
  node.obj.$hasId() &&
243
+ !node.toBeUnrelated &&
244
+ !node.toBeDeleted &&
210
245
  !graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
211
246
  !graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
212
247
  !currentGraph.nodeForNode(node)
@@ -259,6 +294,19 @@ function checkForUnallowedReferenceErrors(graphData, builder) {
259
294
  }
260
295
  }
261
296
 
297
+ function checkForRootUnrelateOrDeleteErrors(graphData, builder) {
298
+ for (const node of graphData.graph.nodes) {
299
+ if (!node.parentEdge && node.toBeUnrelatedOrDeleted) {
300
+ const { graphUnrelateProp, graphDeleteProp } = node.modelClass;
301
+
302
+ throw builder.modelClass().createValidationError({
303
+ type: ValidationErrorType.InvalidGraph,
304
+ message: `${graphUnrelateProp} and ${graphDeleteProp} can only be used for related models, not for root models`,
305
+ });
306
+ }
307
+ }
308
+ }
309
+
262
310
  function checkForHasManyRelateErrors(graphData) {
263
311
  const { graph, graphOptions } = graphData;
264
312
 
@@ -296,6 +344,10 @@ function executeActions(builder, actions) {
296
344
  );
297
345
  }
298
346
 
347
+ function isOnConflictOperation(op) {
348
+ return op.name === 'onConflict' || op.name === 'ignore' || op.name === 'merge';
349
+ }
350
+
299
351
  function returnResult(objects, isArray) {
300
352
  return () => (isArray ? objects : objects[0]);
301
353
  }
@@ -40,20 +40,26 @@ class GraphDeleteAction extends GraphAction {
40
40
  const nodesByParent = groupBy(nodes, getParent);
41
41
 
42
42
  nodesByParent.forEach((nodes, parentNode) => {
43
- const shouldUnrelate = this.graphOptions.shouldUnrelate(nodes[0], this.graphData);
44
-
45
- const builder = parentNode.obj.$relatedQuery(relation.name).childQueryOf(parentBuilder);
46
-
47
- if (!relation.isObjectionBelongsToOneRelation) {
48
- // This is useless in case of BelongsToOneRelation.
49
- builder.findByIds(nodes.map((node) => node.obj.$id()));
50
- }
51
-
52
- for (const node of nodes) {
53
- node.userData.deleted = true;
54
- }
55
-
56
- builders.push(shouldUnrelate ? builder.unrelate() : builder.delete());
43
+ // Nodes of the same relation and parent can still require different
44
+ // operations when they are marked with `#unrelate` or `#delete`.
45
+ const nodesByShouldUnrelate = groupBy(nodes, (node) =>
46
+ this.graphOptions.shouldUnrelate(node, this.graphData),
47
+ );
48
+
49
+ 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
+ builder.findByIds(nodes.map((node) => node.obj.$id()));
55
+ }
56
+
57
+ for (const node of nodes) {
58
+ node.userData.deleted = true;
59
+ }
60
+
61
+ builders.push(shouldUnrelate ? builder.unrelate() : builder.delete());
62
+ });
57
63
  });
58
64
  });
59
65
 
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const { GraphAction } = require('../GraphAction');
4
+ const { groupBy } = require('../../../utils/objectUtils');
5
+ const promiseUtils = require('../../../utils/promiseUtils');
6
+
7
+ /**
8
+ * Patches in the foreign keys of `BelongsToOneRelation` edges that `GraphInsert`
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.
13
+ */
14
+ class DeferredRelationGraphInsertAction extends GraphAction {
15
+ constructor(graphData, { edges }) {
16
+ super(graphData);
17
+
18
+ // `BelongsToOneRelation` edges whose owner node needs to be patched.
19
+ this.edges = edges;
20
+ }
21
+
22
+ run(builder) {
23
+ const edgesByOwner = groupBy(this.edges, (edge) => edge.ownerNode);
24
+ const owners = [...edgesByOwner.keys()];
25
+ const concurrency = this._getConcurrency(builder, owners);
26
+
27
+ return promiseUtils.map(
28
+ owners,
29
+ (owner) => this._patchOwner(builder, owner, edgesByOwner.get(owner)),
30
+ { concurrency },
31
+ );
32
+ }
33
+
34
+ _patchOwner(builder, ownerNode, edges) {
35
+ const { modelClass } = ownerNode;
36
+ const ownerIdProp = modelClass.getIdRelationProperty();
37
+ const patch = {};
38
+
39
+ for (const { relation } of edges) {
40
+ const { ownerProp } = relation;
41
+
42
+ ownerProp.forEach((i) => {
43
+ ownerProp.patch(patch, i, ownerProp.getProp(ownerNode.obj, i));
44
+ });
45
+ }
46
+
47
+ return modelClass
48
+ .query()
49
+ .childQueryOf(builder)
50
+ .patch(patch)
51
+ .whereInComposite(ownerIdProp.refs(builder), [ownerIdProp.getProps(ownerNode.obj)]);
52
+ }
53
+ }
54
+
55
+ module.exports = {
56
+ DeferredRelationGraphInsertAction,
57
+ };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { JoinRowGraphInsertAction } = require('./JoinRowGraphInsertAction');
4
+ const { DeferredRelationGraphInsertAction } = require('./DeferredRelationGraphInsertAction');
4
5
  const { GraphInsertAction } = require('./GraphInsertAction');
5
6
  const { GraphOperation } = require('../GraphOperation');
6
7
  const { ModelGraphEdge } = require('../../../model/graph/ModelGraphEdge');
@@ -9,10 +10,15 @@ class GraphInsert extends GraphOperation {
9
10
  constructor(...args) {
10
11
  super(...args);
11
12
  this.dependencies = this._createDependencyMap();
13
+ this.deferredEdges = new Set();
12
14
  }
13
15
 
14
16
  createActions() {
15
- return [...this._createNormalActions(), ...this._createJoinRowActions()];
17
+ return [
18
+ ...this._createNormalActions(),
19
+ ...this._createDeferredRelationActions(),
20
+ ...this._createJoinRowActions(),
21
+ ];
16
22
  }
17
23
 
18
24
  _createDependencyMap() {
@@ -30,6 +36,12 @@ class GraphInsert extends GraphOperation {
30
36
  }
31
37
 
32
38
  _createRelationDependency(edge, dependencies) {
39
+ if (edge.relatedNode.toBeUnrelatedOrDeleted) {
40
+ // Models marked with `#unrelate` or `#delete` are never inserted or
41
+ // related, so their keys must not be copied to the owner (or vice versa).
42
+ return;
43
+ }
44
+
33
45
  if (edge.relation.isObjectionHasManyRelation) {
34
46
  // In case of HasManyRelation the related node depends on the owner node
35
47
  // because the related node has the foreign key.
@@ -70,6 +82,10 @@ class GraphInsert extends GraphOperation {
70
82
  });
71
83
 
72
84
  if (nodesToInsert.length === 0) {
85
+ if (handledNodes.size !== this.graph.nodes.length && this._breakCycle(handledNodes)) {
86
+ continue;
87
+ }
88
+
73
89
  break;
74
90
  }
75
91
 
@@ -77,6 +93,7 @@ class GraphInsert extends GraphOperation {
77
93
  new GraphInsertAction(this.graphData, {
78
94
  nodes: nodesToInsert,
79
95
  dependencies: this.dependencies,
96
+ deferredEdges: this.deferredEdges,
80
97
  }),
81
98
  );
82
99
 
@@ -92,19 +109,71 @@ class GraphInsert extends GraphOperation {
92
109
  return actions;
93
110
  }
94
111
 
95
- _isHandled(node, handledNodes) {
96
- return handledNodes.has(node);
112
+ // Tries to break a dependency cycle by deferring one `BelongsToOneRelation`
113
+ // edge that points to a `#ref` node. The owner node is then inserted without
114
+ // the foreign key, which is patched in by `DeferredRelationGraphInsertAction`
115
+ // once the referenced node has been inserted. Cycles can only exist through
116
+ // `#ref` nodes, since the graph is a tree otherwise.
117
+ _breakCycle(handledNodes) {
118
+ for (const node of this.graph.nodes) {
119
+ if (this._isHandled(node, handledNodes)) {
120
+ continue;
121
+ }
122
+
123
+ for (const edge of this._pendingDependencyEdges(node, handledNodes)) {
124
+ if (
125
+ this._canDeferEdge(edge, node) &&
126
+ this._isBlockedBy(edge.relatedNode, node, handledNodes, new Set())
127
+ ) {
128
+ this.deferredEdges.add(edge);
129
+ return true;
130
+ }
131
+ }
132
+ }
133
+
134
+ return false;
97
135
  }
98
136
 
99
- _hasDependencies(node, handledNodes) {
100
- if (!this.dependencies.has(node)) {
137
+ _canDeferEdge(edge, node) {
138
+ return (
139
+ edge.type === ModelGraphEdge.Type.Relation &&
140
+ edge.relation.isObjectionBelongsToOneRelation &&
141
+ edge.isOwnerNode(node) &&
142
+ edge.relatedNode.isReference &&
143
+ // Both ends need to be inserted for the foreign key to be patched in later.
144
+ this.graphOptions.shouldInsert(node, this.graphData) &&
145
+ this.graphOptions.shouldInsert(edge.relatedNode.referencedNode, this.graphData) &&
146
+ !this._isReadByPropertyReference(node, edge.relation.ownerProp)
147
+ );
148
+ }
149
+
150
+ // Returns true if a `#ref{}` property reference reads one of the `props` of
151
+ // `node`. Such references may be resolved before a deferred prop is set,
152
+ // which would resolve them to `undefined`.
153
+ _isReadByPropertyReference(node, props) {
154
+ return node.refEdges.some((edge) => {
155
+ return (
156
+ edge.refType === ModelGraphEdge.ReferenceType.Property &&
157
+ edge.isRelatedNode(node) &&
158
+ props.props.includes(edge.refRelatedDataPath[0])
159
+ );
160
+ });
161
+ }
162
+
163
+ // Returns true if `node` (transitively) waits for `blockingNode` to be handled.
164
+ _isBlockedBy(node, blockingNode, handledNodes, visitedNodes) {
165
+ if (node === blockingNode) {
166
+ return true;
167
+ }
168
+
169
+ if (visitedNodes.has(node)) {
101
170
  return false;
102
171
  }
103
172
 
104
- for (const edge of this.dependencies.get(node)) {
105
- const dependencyNode = edge.getOtherNode(node);
173
+ visitedNodes.add(node);
106
174
 
107
- if (!handledNodes.has(dependencyNode) && !this.currentGraph.nodeForNode(dependencyNode)) {
175
+ for (const edge of this._pendingDependencyEdges(node, handledNodes)) {
176
+ if (this._isBlockedBy(edge.getOtherNode(node), blockingNode, handledNodes, visitedNodes)) {
108
177
  return true;
109
178
  }
110
179
  }
@@ -112,6 +181,42 @@ class GraphInsert extends GraphOperation {
112
181
  return false;
113
182
  }
114
183
 
184
+ _createDeferredRelationActions() {
185
+ if (this.deferredEdges.size === 0) {
186
+ return [];
187
+ }
188
+
189
+ return [
190
+ new DeferredRelationGraphInsertAction(this.graphData, {
191
+ edges: [...this.deferredEdges],
192
+ }),
193
+ ];
194
+ }
195
+
196
+ _isHandled(node, handledNodes) {
197
+ return handledNodes.has(node);
198
+ }
199
+
200
+ _hasDependencies(node, handledNodes) {
201
+ return this._pendingDependencyEdges(node, handledNodes).length > 0;
202
+ }
203
+
204
+ // Returns the dependency edges of `node` whose other node still needs to be
205
+ // inserted first. Deferred edges are ignored.
206
+ _pendingDependencyEdges(node, handledNodes) {
207
+ const edges = this.dependencies.get(node) || [];
208
+
209
+ return edges.filter((edge) => {
210
+ const dependencyNode = edge.getOtherNode(node);
211
+
212
+ return (
213
+ !this.deferredEdges.has(edge) &&
214
+ !handledNodes.has(dependencyNode) &&
215
+ !this.currentGraph.nodeForNode(dependencyNode)
216
+ );
217
+ });
218
+ }
219
+
115
220
  _markHandled(node, handledNodes) {
116
221
  handledNodes.add(node);
117
222
 
@@ -14,12 +14,15 @@ const promiseUtils = require('../../../utils/promiseUtils');
14
14
  * when needed.
15
15
  */
16
16
  class GraphInsertAction extends GraphAction {
17
- constructor(graphData, { nodes, dependencies }) {
17
+ constructor(graphData, { nodes, dependencies, deferredEdges }) {
18
18
  super(graphData);
19
19
 
20
20
  // Nodes to insert.
21
21
  this.nodes = nodes;
22
22
  this.dependencies = dependencies;
23
+ // Dependency edges that are resolved once the dependency node has been
24
+ // inserted, and are patched in by `DeferredRelationGraphInsertAction`.
25
+ this.deferredEdges = deferredEdges;
23
26
  }
24
27
 
25
28
  run(builder) {
@@ -63,6 +66,10 @@ class GraphInsertAction extends GraphAction {
63
66
 
64
67
  if (edges) {
65
68
  for (const edge of edges) {
69
+ if (this.deferredEdges.has(edge)) {
70
+ continue;
71
+ }
72
+
66
73
  // `node` needs `dependencyNode` to have been inserted (and it has been).
67
74
  const dependencyNode = edge.getOtherNode(node);
68
75
 
@@ -174,6 +181,8 @@ class GraphInsertAction extends GraphAction {
174
181
 
175
182
  for (const node of nodes) {
176
183
  delete node.obj[modelClass.uidProp];
184
+ delete node.obj[modelClass.graphUnrelateProp];
185
+ delete node.obj[modelClass.graphDeleteProp];
177
186
  node.obj.$validate(null, { dataPath: node.dataPathKey });
178
187
  }
179
188
 
@@ -75,6 +75,8 @@ class GraphPatchAction extends GraphAction {
75
75
  delete node.obj[node.modelClass.uidProp];
76
76
  delete node.obj[node.modelClass.uidRefProp];
77
77
  delete node.obj[node.modelClass.dbRefProp];
78
+ delete node.obj[node.modelClass.graphUnrelateProp];
79
+ delete node.obj[node.modelClass.graphDeleteProp];
78
80
 
79
81
  node.obj.$validate(null, {
80
82
  dataPath: node.dataPathKey,
@@ -122,7 +124,9 @@ class GraphPatchAction extends GraphAction {
122
124
  edge.isOwnerNode(node) &&
123
125
  edge.relation &&
124
126
  edge.relation.isObjectionBelongsToOneRelation &&
125
- edge.relation.relatedProp.hasProps(edge.relatedNode.obj)
127
+ edge.relation.relatedProp.hasProps(edge.relatedNode.obj) &&
128
+ // Models marked with `#unrelate` or `#delete` are handled below.
129
+ !edge.relatedNode.toBeUnrelatedOrDeleted
126
130
  ) {
127
131
  const { relation } = edge;
128
132
 
@@ -153,7 +157,8 @@ class GraphPatchAction extends GraphAction {
153
157
  if (
154
158
  edge.isOwnerNode(currentNode) &&
155
159
  edge.relation.isObjectionBelongsToOneRelation &&
156
- node.obj[edge.relation.name] === null &&
160
+ (node.obj[edge.relation.name] === null ||
161
+ isToBeUnrelatedOrDeleted(this.graph.nodeForNode(edge.relatedNode))) &&
157
162
  this.graphOptions.shouldDeleteOrUnrelate(edge.relatedNode, this.graphData)
158
163
  ) {
159
164
  const { relation } = edge;
@@ -231,7 +236,10 @@ class GraphPatchAction extends GraphAction {
231
236
  }
232
237
 
233
238
  _createRootBuilder(node) {
234
- return node.obj.$query();
239
+ const currentNode = this.currentGraph.nodeForNode(node);
240
+ const currentObj = currentNode && currentNode.obj;
241
+
242
+ return currentObj ? currentObj.$query() : node.obj.$query();
235
243
  }
236
244
  }
237
245
 
@@ -250,6 +258,10 @@ function nonStrictEquals(val1, val2) {
250
258
  return jsonEquals(val1, val2);
251
259
  }
252
260
 
261
+ function isToBeUnrelatedOrDeleted(node) {
262
+ return !!node && node.toBeUnrelatedOrDeleted;
263
+ }
264
+
253
265
  module.exports = {
254
266
  GraphPatchAction,
255
267
  };
@@ -22,6 +22,8 @@ class JoinResultParser {
22
22
  return flatRows;
23
23
  }
24
24
 
25
+ this._checkIdColumns(flatRows[0]);
26
+
25
27
  this.columnsByTableNode = this._createColumns(flatRows[0]);
26
28
  this.parentMap = new Map();
27
29
  this.rootModels = [];
@@ -93,6 +95,26 @@ class JoinResultParser {
93
95
  }
94
96
  }
95
97
 
98
+ _checkIdColumns(row) {
99
+ for (const tableNode of this.tableTree.nodes) {
100
+ const idColumns = tableNode.modelClass.getIdColumnArray();
101
+ const hasIdColumns =
102
+ idColumns.length > 0 &&
103
+ idColumns.every((column) =>
104
+ Object.prototype.hasOwnProperty.call(row, tableNode.getColumnAliasForColumn(column)),
105
+ );
106
+
107
+ if (!hasIdColumns) {
108
+ throw new Error(
109
+ `withGraphJoined: the id column(s) [${idColumns.join(', ')}] of model ${tableNode.modelClass.name} ` +
110
+ `are missing from the result, so the joined rows can't be grouped into models. ` +
111
+ `withGraphJoined requires all models in the relation expression to have a primary key ` +
112
+ `(idColumn) that exists in the table. Use withGraphFetched for models without one.`,
113
+ );
114
+ }
115
+ }
116
+ }
117
+
96
118
  _createColumns(row) {
97
119
  const columns = Object.keys(row).map((columnAlias) =>
98
120
  JoinResultColumn.create({ tableTree: this.tableTree, columnAlias }),