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.
- package/README.md +16 -16
- package/lib/model/AjvValidator.js +173 -18
- package/lib/model/Model.js +3 -1
- package/lib/model/graph/ModelGraphNode.js +12 -0
- package/lib/model/modelToJson.js +2 -1
- package/lib/queryBuilder/QueryBuilder.js +339 -47
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
- package/lib/queryBuilder/ReferenceBuilder.js +5 -3
- package/lib/queryBuilder/RelationExpression.js +21 -9
- package/lib/queryBuilder/StaticHookArguments.js +12 -1
- package/lib/queryBuilder/graph/GraphFetcher.js +3 -1
- package/lib/queryBuilder/graph/GraphOptions.js +21 -2
- package/lib/queryBuilder/graph/GraphUpsert.js +52 -0
- package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +20 -14
- package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
- package/lib/queryBuilder/graph/insert/GraphInsert.js +113 -8
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +10 -1
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +15 -3
- package/lib/queryBuilder/join/JoinResultParser.js +22 -0
- package/lib/queryBuilder/join/RelationJoiner.js +71 -5
- package/lib/queryBuilder/operations/DelegateOperation.js +4 -0
- package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
- package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
- package/lib/queryBuilder/operations/InsertOperation.js +121 -12
- package/lib/queryBuilder/operations/NoneOperation.js +37 -0
- package/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js +5 -0
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
- package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
- package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +20 -0
- package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
- package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
- package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
- package/lib/queryBuilder/operations/eager/EagerOperation.js +7 -5
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +4 -2
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +9 -20
- package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
- package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +45 -32
- package/lib/queryBuilder/operations/select/Selection.js +95 -4
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
- package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
- package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
- package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +9 -0
- package/lib/queryBuilder/whereGroups.js +125 -0
- package/lib/relations/Relation.js +5 -2
- package/lib/relations/RelationProperty.js +7 -3
- package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +1 -0
- package/lib/relations/belongsToOne/BelongsToOneRelateOperation.js +5 -0
- package/lib/relations/hasMany/HasManyRelateOperation.js +6 -2
- package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
- package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
- package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
- package/lib/relations/manyToMany/ManyToManyRelation.js +70 -48
- package/lib/relations/manyToMany/relate/ManyToManyRelateOperation.js +6 -0
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +8 -2
- package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
- package/lib/utils/deprecate.js +6 -0
- package/lib/utils/identifierMapping.js +23 -3
- package/lib/utils/objectUtils.js +9 -5
- package/lib/utils/parseFieldExpression.js +28 -0
- package/package.json +22 -14
- package/typings/objection/index.d.ts +339 -68
|
@@ -24,7 +24,13 @@ class StaticHookArguments {
|
|
|
24
24
|
|
|
25
25
|
get asFindQuery() {
|
|
26
26
|
return () => {
|
|
27
|
-
|
|
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
|
-
!
|
|
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
|
-
!
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 [
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
const dependencyNode = edge.getOtherNode(node);
|
|
173
|
+
visitedNodes.add(node);
|
|
106
174
|
|
|
107
|
-
|
|
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
|
-
|
|
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 }),
|