objection 3.1.4 → 3.2.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 +3 -3
- package/lib/model/AjvValidator.js +60 -25
- package/lib/model/Model.js +3 -4
- package/lib/model/graph/ModelGraphNode.js +12 -0
- package/lib/model/modelToJson.js +2 -1
- package/lib/queryBuilder/QueryBuilder.js +156 -45
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +1 -1
- package/lib/queryBuilder/RelationExpression.js +8 -8
- package/lib/queryBuilder/graph/GraphFetcher.js +3 -1
- package/lib/queryBuilder/graph/GraphOptions.js +21 -2
- package/lib/queryBuilder/graph/GraphUpsert.js +30 -0
- package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +20 -14
- package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -0
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +2 -0
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +15 -3
- package/lib/queryBuilder/join/RelationJoiner.js +17 -0
- package/lib/queryBuilder/operations/DelegateOperation.js +4 -0
- package/lib/queryBuilder/operations/NoneOperation.js +37 -0
- package/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js +5 -0
- package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +20 -0
- package/lib/queryBuilder/operations/eager/EagerOperation.js +3 -5
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +8 -2
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +15 -3
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +8 -2
- package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +9 -0
- 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 +1 -0
- package/lib/relations/manyToMany/relate/ManyToManyRelateOperation.js +6 -0
- package/lib/utils/identifierMapping.js +8 -2
- package/lib/utils/objectUtils.js +9 -5
- package/package.json +17 -13
- package/typings/objection/index.d.ts +77 -39
|
@@ -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
|
);
|
|
@@ -91,6 +91,7 @@ function pruneGraphs() {
|
|
|
91
91
|
|
|
92
92
|
if (!graphData.graphOptions.isInsertOnly()) {
|
|
93
93
|
pruneDeletedBranches(graphData);
|
|
94
|
+
pruneUnrelatedOrDeletedBranches(graphData);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
return graphData;
|
|
@@ -136,6 +137,19 @@ function pruneDeletedBranches(graphData) {
|
|
|
136
137
|
removeNodesFromGraph(new Set(rootsNotInRelation), currentGraph);
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
// Models marked with `#unrelate` or `#delete` are unrelated or deleted
|
|
141
|
+
// as a whole. Their relations are ignored, both in the upserted graph
|
|
142
|
+
// and in the current graph.
|
|
143
|
+
function pruneUnrelatedOrDeletedBranches(graphData) {
|
|
144
|
+
const { graph, currentGraph } = graphData;
|
|
145
|
+
|
|
146
|
+
const roots = findRoots(graph.nodes.filter((node) => node.toBeUnrelatedOrDeleted));
|
|
147
|
+
const currentRoots = roots.map((node) => currentGraph.nodeForNode(node)).filter(Boolean);
|
|
148
|
+
|
|
149
|
+
removeBranchesFromGraph(roots, graph);
|
|
150
|
+
removeBranchesFromGraph(currentRoots, currentGraph);
|
|
151
|
+
}
|
|
152
|
+
|
|
139
153
|
function findRoots(nodes) {
|
|
140
154
|
const nodeSet = new Set(nodes);
|
|
141
155
|
|
|
@@ -192,6 +206,7 @@ function checkForErrors(builder) {
|
|
|
192
206
|
checkForNotFoundErrors(graphData, builder);
|
|
193
207
|
checkForUnallowedRelationErrors(graphData, builder);
|
|
194
208
|
checkForUnallowedReferenceErrors(graphData, builder);
|
|
209
|
+
checkForRootUnrelateOrDeleteErrors(graphData, builder);
|
|
195
210
|
|
|
196
211
|
if (graphData.graphOptions.isInsertOnly()) {
|
|
197
212
|
checkForHasManyRelateErrors(graphData);
|
|
@@ -207,6 +222,8 @@ function checkForNotFoundErrors(graphData, builder) {
|
|
|
207
222
|
for (const node of graph.nodes) {
|
|
208
223
|
if (
|
|
209
224
|
node.obj.$hasId() &&
|
|
225
|
+
!node.toBeUnrelated &&
|
|
226
|
+
!node.toBeDeleted &&
|
|
210
227
|
!graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
|
|
211
228
|
!graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
|
|
212
229
|
!currentGraph.nodeForNode(node)
|
|
@@ -259,6 +276,19 @@ function checkForUnallowedReferenceErrors(graphData, builder) {
|
|
|
259
276
|
}
|
|
260
277
|
}
|
|
261
278
|
|
|
279
|
+
function checkForRootUnrelateOrDeleteErrors(graphData, builder) {
|
|
280
|
+
for (const node of graphData.graph.nodes) {
|
|
281
|
+
if (!node.parentEdge && node.toBeUnrelatedOrDeleted) {
|
|
282
|
+
const { graphUnrelateProp, graphDeleteProp } = node.modelClass;
|
|
283
|
+
|
|
284
|
+
throw builder.modelClass().createValidationError({
|
|
285
|
+
type: ValidationErrorType.InvalidGraph,
|
|
286
|
+
message: `${graphUnrelateProp} and ${graphDeleteProp} can only be used for related models, not for root models`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
262
292
|
function checkForHasManyRelateErrors(graphData) {
|
|
263
293
|
const { graph, graphOptions } = graphData;
|
|
264
294
|
|
|
@@ -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
|
|
|
@@ -30,6 +30,12 @@ class GraphInsert extends GraphOperation {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
_createRelationDependency(edge, dependencies) {
|
|
33
|
+
if (edge.relatedNode.toBeUnrelatedOrDeleted) {
|
|
34
|
+
// Models marked with `#unrelate` or `#delete` are never inserted or
|
|
35
|
+
// related, so their keys must not be copied to the owner (or vice versa).
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
if (edge.relation.isObjectionHasManyRelation) {
|
|
34
40
|
// In case of HasManyRelation the related node depends on the owner node
|
|
35
41
|
// because the related node has the foreign key.
|
|
@@ -174,6 +174,8 @@ class GraphInsertAction extends GraphAction {
|
|
|
174
174
|
|
|
175
175
|
for (const node of nodes) {
|
|
176
176
|
delete node.obj[modelClass.uidProp];
|
|
177
|
+
delete node.obj[modelClass.graphUnrelateProp];
|
|
178
|
+
delete node.obj[modelClass.graphDeleteProp];
|
|
177
179
|
node.obj.$validate(null, { dataPath: node.dataPathKey });
|
|
178
180
|
}
|
|
179
181
|
|
|
@@ -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
|
};
|
|
@@ -81,6 +81,23 @@ class RelationJoiner {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
releaseInternalRootSelection(column) {
|
|
85
|
+
const rootTableNode = this.tableTree && this.tableTree.rootNode;
|
|
86
|
+
|
|
87
|
+
if (!rootTableNode || !this.internalSelections) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const alias = rootTableNode.getColumnAliasForColumn(column);
|
|
92
|
+
const count = this.internalSelections.length;
|
|
93
|
+
|
|
94
|
+
this.internalSelections = this.internalSelections.filter(
|
|
95
|
+
(it) => it.table !== rootTableNode.alias || it.alias !== alias,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return this.internalSelections.length !== count;
|
|
99
|
+
}
|
|
100
|
+
|
|
84
101
|
parseResult(builder, flatRows) {
|
|
85
102
|
const parser = JoinResultParser.create({
|
|
86
103
|
tableTree: this._getTableTree(builder),
|
|
@@ -11,6 +11,10 @@ class DelegateOperation extends QueryBuilderOperation {
|
|
|
11
11
|
this.delegate = opt.delegate;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
get modelOptions() {
|
|
15
|
+
return this.delegate.modelOptions;
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
is(OperationClass) {
|
|
15
19
|
return super.is(OperationClass) || this.delegate.is(OperationClass);
|
|
16
20
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
4
|
+
|
|
5
|
+
const AggregateSelector =
|
|
6
|
+
/^(count|countDistinct|min|max|sum|sumDistinct|avg|avgDistinct|groupBy\w*)$/;
|
|
7
|
+
|
|
8
|
+
class NoneOperation extends QueryBuilderOperation {
|
|
9
|
+
onBefore1(builder, result) {
|
|
10
|
+
// Resolve the query without executing it, like `cancelQuery()` does.
|
|
11
|
+
if (builder.isFind()) {
|
|
12
|
+
// Aggregate queries are executed to get the result the database
|
|
13
|
+
// returns for no rows, e.g. `[{ count: 0 }]`.
|
|
14
|
+
if (!builder.has(AggregateSelector)) {
|
|
15
|
+
builder.resolve([]);
|
|
16
|
+
}
|
|
17
|
+
} else if (builder.isUpdate() || builder.isDelete()) {
|
|
18
|
+
builder.resolve(builder.has(/returning/) ? [] : 0);
|
|
19
|
+
} else {
|
|
20
|
+
throw new Error('none() can only be used with find, update and delete queries');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Called by `buildKnexQuery()` after all other operations have been
|
|
27
|
+
// built, so that the query matches no rows when it does get executed,
|
|
28
|
+
// for example as a subquery or in `resultSize()`. The existing where
|
|
29
|
+
// clauses are removed so that `orWhere()` calls can't match any rows.
|
|
30
|
+
onAfterBuildKnex(knexBuilder) {
|
|
31
|
+
return knexBuilder.clearWhere().where(false);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
NoneOperation,
|
|
37
|
+
};
|
|
@@ -106,6 +106,11 @@ function convertArray(arr, builder, i, opName, skipUndefined) {
|
|
|
106
106
|
`undefined passed as an item in argument #${i} for '${opName}' operation. Call skipUndefined() method to ignore the undefined values.`,
|
|
107
107
|
);
|
|
108
108
|
}
|
|
109
|
+
} else if (opName === 'orderBy' && isObject(item)) {
|
|
110
|
+
// knex's `orderBy()` only reads `{ column, order, nulls }` objects from arrays,
|
|
111
|
+
// so wrap refs, raws and query builders in one and convert its properties.
|
|
112
|
+
const obj = isPlainObject(item) ? item : { column: item };
|
|
113
|
+
return convertPlainObject(obj, builder, i, opName, skipUndefined);
|
|
109
114
|
} else if (hasToKnexRawMethod(item)) {
|
|
110
115
|
return convertToKnexRaw(item, builder);
|
|
111
116
|
} else if (isObjectionQueryBuilderBase(item)) {
|
|
@@ -14,6 +14,7 @@ class UpdateAndFetchOperation extends DelegateOperation {
|
|
|
14
14
|
|
|
15
15
|
this.id = null;
|
|
16
16
|
this.skipIdWhere = false;
|
|
17
|
+
this.selectOperations = null;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
get model() {
|
|
@@ -25,6 +26,19 @@ class UpdateAndFetchOperation extends DelegateOperation {
|
|
|
25
26
|
return this.delegate.onAdd(builder, args.slice(1));
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
onBefore3(builder, result) {
|
|
30
|
+
// Selects have no effect on the update query, so remember them here to
|
|
31
|
+
// apply them to the fetch query. Collect them before the query is built,
|
|
32
|
+
// so that selects added by `onBuild` hooks in the shared context aren't
|
|
33
|
+
// copied, as those hooks run again for the fetch query.
|
|
34
|
+
this.selectOperations = new Set();
|
|
35
|
+
builder.forEachOperation(builder.constructor.SelectSelector, (op) => {
|
|
36
|
+
this.selectOperations.add(op);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return super.onBefore3(builder, result);
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
onBuild(builder) {
|
|
29
43
|
if (!this.skipIdWhere) {
|
|
30
44
|
builder.findById(this.id);
|
|
@@ -43,6 +57,7 @@ class UpdateAndFetchOperation extends DelegateOperation {
|
|
|
43
57
|
const fetched = await builder
|
|
44
58
|
.emptyInstance()
|
|
45
59
|
.childQueryOf(builder)
|
|
60
|
+
.copyFrom(builder, (op) => !!this.selectOperations?.has(op))
|
|
46
61
|
.modify((builder) => {
|
|
47
62
|
if (!this.skipIdWhere) {
|
|
48
63
|
builder.findById(this.id);
|
|
@@ -52,6 +67,11 @@ class UpdateAndFetchOperation extends DelegateOperation {
|
|
|
52
67
|
|
|
53
68
|
if (fetched) {
|
|
54
69
|
this.model.$set(fetched);
|
|
70
|
+
// A custom `select` may not include the id. The row was fetched by
|
|
71
|
+
// id, so make sure the result still has it.
|
|
72
|
+
if (!this.model.$hasId()) {
|
|
73
|
+
this.model.$id(this.id);
|
|
74
|
+
}
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
await super.onAfter2(builder, numUpdated);
|
|
@@ -8,14 +8,13 @@ class EagerOperation extends QueryBuilderOperation {
|
|
|
8
8
|
super(name, opt);
|
|
9
9
|
|
|
10
10
|
this.expression = RelationExpression.create();
|
|
11
|
-
this.modifiersAtPath = [];
|
|
12
11
|
this.graphOptions = this.opt.defaultGraphOptions;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
buildFinalExpression() {
|
|
14
|
+
buildFinalExpression(builder) {
|
|
16
15
|
const expression = this.expression.clone();
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
builder.graphModifiersAtPath().forEach((modifier, i) => {
|
|
19
18
|
const modifierName = getModifierName(i);
|
|
20
19
|
|
|
21
20
|
expression.expressionsAtPath(modifier.path).forEach((expr) => {
|
|
@@ -30,7 +29,7 @@ class EagerOperation extends QueryBuilderOperation {
|
|
|
30
29
|
// `modifiers()` returns a clone so we can modify it.
|
|
31
30
|
const modifiers = builder.modifiers();
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
builder.graphModifiersAtPath().forEach((modifier, i) => {
|
|
34
33
|
const modifierName = getModifierName(i);
|
|
35
34
|
|
|
36
35
|
modifiers[modifierName] = modifier.modifier;
|
|
@@ -41,7 +40,6 @@ class EagerOperation extends QueryBuilderOperation {
|
|
|
41
40
|
|
|
42
41
|
cloneFrom(eagerOp) {
|
|
43
42
|
this.expression = eagerOp.expression.clone();
|
|
44
|
-
this.modifiersAtPath = eagerOp.modifiersAtPath.slice();
|
|
45
43
|
this.graphOptions = Object.assign({}, eagerOp.graphOptions);
|
|
46
44
|
}
|
|
47
45
|
|
|
@@ -21,7 +21,7 @@ class JoinEagerOperation extends EagerOperation {
|
|
|
21
21
|
|
|
22
22
|
onBefore3(builder) {
|
|
23
23
|
return this.joiner
|
|
24
|
-
.setExpression(this.buildFinalExpression())
|
|
24
|
+
.setExpression(this.buildFinalExpression(builder))
|
|
25
25
|
.setModifiers(this.buildFinalModifiers(builder))
|
|
26
26
|
.setOptions(this.graphOptions)
|
|
27
27
|
.fetchColumnInfo(builder);
|
|
@@ -29,12 +29,18 @@ class JoinEagerOperation extends EagerOperation {
|
|
|
29
29
|
|
|
30
30
|
onBuild(builder) {
|
|
31
31
|
this.joiner
|
|
32
|
-
.setExpression(this.buildFinalExpression())
|
|
32
|
+
.setExpression(this.buildFinalExpression(builder))
|
|
33
33
|
.setModifiers(this.buildFinalModifiers(builder))
|
|
34
34
|
.setOptions(this.graphOptions)
|
|
35
35
|
.build(builder);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Stops the joiner from omitting an internally selected column of the root
|
|
39
|
+
// table from the result. Returns true if such a selection existed.
|
|
40
|
+
releaseInternalRootSelection(column) {
|
|
41
|
+
return !!this.joiner && this.joiner.releaseInternalRootSelection(column);
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
onRawResult(builder, rows) {
|
|
39
45
|
return this.joiner.parseResult(builder, rows);
|
|
40
46
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const promiseUtils = require('../../../utils/promiseUtils');
|
|
4
4
|
|
|
5
5
|
const { EagerOperation } = require('./EagerOperation');
|
|
6
|
+
const { JoinEagerOperation } = require('./JoinEagerOperation');
|
|
6
7
|
const { isMsSql, isOracle, isSqlite } = require('../../../utils/knexUtils');
|
|
7
8
|
const { isObject, asArray, flatten, chunk } = require('../../../utils/objectUtils');
|
|
8
9
|
const { ValidationErrorType } = require('../../../model/ValidationError');
|
|
@@ -41,7 +42,7 @@ class WhereInEagerOperation extends EagerOperation {
|
|
|
41
42
|
onBuild(builder) {
|
|
42
43
|
const relationsToFetch = findRelationsToFetch(
|
|
43
44
|
builder.modelClass(),
|
|
44
|
-
this.buildFinalExpression(),
|
|
45
|
+
this.buildFinalExpression(builder),
|
|
45
46
|
);
|
|
46
47
|
|
|
47
48
|
const { selectionsToAdd, selectedProps } = findRelationPropsToSelect(builder, relationsToFetch);
|
|
@@ -237,6 +238,11 @@ function findRelationPropsToSelect(builder, relationsToFetch) {
|
|
|
237
238
|
const selectionsToAdd = [];
|
|
238
239
|
const selectedProps = [];
|
|
239
240
|
|
|
241
|
+
// When mixed with `withGraphJoined`, the join operation may have selected
|
|
242
|
+
// some of the needed columns internally, in which case it would omit them
|
|
243
|
+
// from the result models before we get to use them.
|
|
244
|
+
const joinOp = builder.findOperation(JoinEagerOperation);
|
|
245
|
+
|
|
240
246
|
// Collect columns that need to be selected for the eager fetch
|
|
241
247
|
// to work that are not currently selected.
|
|
242
248
|
for (const { relation } of relationsToFetch) {
|
|
@@ -247,9 +253,15 @@ function findRelationPropsToSelect(builder, relationsToFetch) {
|
|
|
247
253
|
const prop = ownerProp.props[c];
|
|
248
254
|
const col = ownerProp.cols[c];
|
|
249
255
|
|
|
250
|
-
if (!builder.hasSelectionAs(fullCol, col)
|
|
256
|
+
if (!builder.hasSelectionAs(fullCol, col)) {
|
|
257
|
+
if (selectionsToAdd.indexOf(fullCol) === -1) {
|
|
258
|
+
selectedProps.push(prop);
|
|
259
|
+
selectionsToAdd.push(fullCol);
|
|
260
|
+
}
|
|
261
|
+
} else if (joinOp && joinOp.releaseInternalRootSelection(col)) {
|
|
262
|
+
// The join operation no longer omits the column, so we need to
|
|
263
|
+
// omit the property once the relations have been fetched.
|
|
251
264
|
selectedProps.push(prop);
|
|
252
|
-
selectionsToAdd.push(fullCol);
|
|
253
265
|
}
|
|
254
266
|
}
|
|
255
267
|
}
|
|
@@ -44,10 +44,16 @@ const { asArray, isObject, isString } = require('../../../utils/objectUtils');
|
|
|
44
44
|
|
|
45
45
|
function parseFieldExpression(expression, extractAsText) {
|
|
46
46
|
let parsed = parser.parseFieldExpression(expression);
|
|
47
|
+
let middleQuotedColumnName = parsed.columnName.split('.').join('"."');
|
|
48
|
+
let column = `"${middleQuotedColumnName}"`;
|
|
49
|
+
// Reference the bare column when there is no json path, so that Postgres can
|
|
50
|
+
// use indexes on it. `#>> '{}'` is still needed to extract the value as text.
|
|
51
|
+
if (parsed.access.length === 0 && !extractAsText) {
|
|
52
|
+
return column;
|
|
53
|
+
}
|
|
47
54
|
let jsonRefs = parsed.access.map((it) => it.ref).join(',');
|
|
48
55
|
let extractor = extractAsText ? '#>>' : '#>';
|
|
49
|
-
|
|
50
|
-
return `"${middleQuotedColumnName}"${extractor}'{${jsonRefs}}'`;
|
|
56
|
+
return `${column}${extractor}'{${jsonRefs}}'`;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
function whereJsonbRefOnLeftJsonbValOrRefOnRight(
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { QueryTransformation } = require('./QueryTransformation');
|
|
4
4
|
const { isMySql } = require('../../utils/knexUtils');
|
|
5
|
+
const { once } = require('../../utils/objectUtils');
|
|
6
|
+
const getJoinBuilder = once(() => require('../JoinBuilder').JoinBuilder);
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Mysql doesn't allow queries like this:
|
|
@@ -30,6 +32,13 @@ class WrapMysqlModifySubqueryTransformation extends QueryTransformation {
|
|
|
30
32
|
return query;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
// This transformation should not apply to join builder, otherwise it causes:
|
|
36
|
+
// "TypeError: parentQuery.isUpdate is not a function"
|
|
37
|
+
const JoinBuilder = getJoinBuilder();
|
|
38
|
+
if (parentQuery instanceof JoinBuilder) {
|
|
39
|
+
return query;
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
// This transformation only applies to update and delete queries.
|
|
34
43
|
if (!parentQuery.isUpdate() && !parentQuery.isDelete()) {
|
|
35
44
|
return query;
|
|
@@ -184,14 +184,18 @@ function createRefs(refs) {
|
|
|
184
184
|
|
|
185
185
|
function createPaths(refs, modelClassResolver) {
|
|
186
186
|
return refs.map((ref) => {
|
|
187
|
-
|
|
187
|
+
// References created with `SomeModel.ref(column)` have no explicit table
|
|
188
|
+
// name, so fall back to the table name of their model class.
|
|
189
|
+
const tableName = ref.tableName || (ref.modelClass && ref.modelClass.getTableName());
|
|
190
|
+
|
|
191
|
+
if (!tableName) {
|
|
188
192
|
throw new InvalidReferenceError();
|
|
189
193
|
}
|
|
190
194
|
|
|
191
|
-
const modelClass = modelClassResolver(
|
|
195
|
+
const modelClass = modelClassResolver(tableName);
|
|
192
196
|
|
|
193
197
|
if (!modelClass) {
|
|
194
|
-
throw new ModelNotFoundError(
|
|
198
|
+
throw new ModelNotFoundError(tableName);
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
const prop = modelClass.columnNameToPropertyName(ref.column);
|
|
@@ -35,6 +35,11 @@ class BelongsToOneRelateOperation extends RelateOperation {
|
|
|
35
35
|
.query()
|
|
36
36
|
.childQueryOf(builder)
|
|
37
37
|
.patch(patch)
|
|
38
|
+
.modify((query) => {
|
|
39
|
+
if (!this.opt.dontCopyReturning) {
|
|
40
|
+
query.copyFrom(builder, /returning/);
|
|
41
|
+
}
|
|
42
|
+
})
|
|
38
43
|
.whereInComposite(ownerIdProp.refs(builder), ownerIds);
|
|
39
44
|
}
|
|
40
45
|
}
|
|
@@ -28,6 +28,7 @@ class HasManyRelateOperation extends RelateOperation {
|
|
|
28
28
|
.query()
|
|
29
29
|
.childQueryOf(builder)
|
|
30
30
|
.patch(patch)
|
|
31
|
+
.copyFrom(builder, /returning/)
|
|
31
32
|
.copyFrom(builder, builder.constructor.JoinSelector)
|
|
32
33
|
.copyFrom(builder, builder.constructor.WhereSelector)
|
|
33
34
|
.findByIds(this.ids)
|
|
@@ -17,6 +17,7 @@ class ManyToManyRelateOperation extends RelateOperation {
|
|
|
17
17
|
const joinModelClass = this.relation.getJoinModelClass(builder.knex());
|
|
18
18
|
const ownerValues = this.owner.getSplitProps(builder, this.relation);
|
|
19
19
|
const joinModels = [];
|
|
20
|
+
const isReturning = !this.opt.dontCopyReturning && builder.has(/returning/);
|
|
20
21
|
|
|
21
22
|
for (const ownerValue of ownerValues) {
|
|
22
23
|
for (const relatedValue of this.ids) {
|
|
@@ -43,6 +44,11 @@ class ManyToManyRelateOperation extends RelateOperation {
|
|
|
43
44
|
})
|
|
44
45
|
.insert(joinModels)
|
|
45
46
|
.runAfter((models) => {
|
|
47
|
+
if (isReturning) {
|
|
48
|
+
// Return the inserted join rows if `returning` was used.
|
|
49
|
+
return models;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
return Array.isArray(models) ? models.length : 1;
|
|
47
53
|
});
|
|
48
54
|
}
|
|
@@ -27,6 +27,7 @@ function snakeCase(
|
|
|
27
27
|
upperCase = false,
|
|
28
28
|
underscoreBeforeDigits = false,
|
|
29
29
|
underscoreBetweenUppercaseLetters = false,
|
|
30
|
+
noDoubleUnderscores = false,
|
|
30
31
|
} = {},
|
|
31
32
|
) {
|
|
32
33
|
if (str.length === 0) {
|
|
@@ -48,11 +49,16 @@ function snakeCase(
|
|
|
48
49
|
const lowerChar = lower[i];
|
|
49
50
|
const prevLowerChar = lower[i - 1];
|
|
50
51
|
|
|
52
|
+
// If noDoubleUnderscores is true, never insert an underscore directly
|
|
53
|
+
// after an existing one (e.g. "foo_1" stays "foo_1" instead of becoming
|
|
54
|
+
// "foo__1" when underscoreBeforeDigits is true).
|
|
55
|
+
const canInsertUnderscore = !noDoubleUnderscores || prevChar !== '_';
|
|
56
|
+
|
|
51
57
|
// If underScoreBeforeDigits is true then, well, insert an underscore
|
|
52
58
|
// before digits :). Only the first digit gets an underscore if
|
|
53
59
|
// there are multiple.
|
|
54
60
|
if (underscoreBeforeDigits && isDigit(char) && !isDigit(prevChar)) {
|
|
55
|
-
out += '_' + char;
|
|
61
|
+
out += canInsertUnderscore ? '_' + char : char;
|
|
56
62
|
continue;
|
|
57
63
|
}
|
|
58
64
|
|
|
@@ -65,7 +71,7 @@ function snakeCase(
|
|
|
65
71
|
// If underscoreBetweenUppercaseLetters is true, we always place an underscore
|
|
66
72
|
// before consecutive uppercase letters (e.g. "fooBAR" becomes "foo_b_a_r").
|
|
67
73
|
// Otherwise, we don't (e.g. "fooBAR" becomes "foo_bar").
|
|
68
|
-
if (underscoreBetweenUppercaseLetters || !prevCharacterIsUppercase) {
|
|
74
|
+
if (canInsertUnderscore && (underscoreBetweenUppercaseLetters || !prevCharacterIsUppercase)) {
|
|
69
75
|
out += '_' + lowerChar;
|
|
70
76
|
} else {
|
|
71
77
|
out += lowerChar;
|
package/lib/utils/objectUtils.js
CHANGED
|
@@ -123,13 +123,17 @@ function union(arr1, arr2) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
function unionSmall(arr1, arr2) {
|
|
126
|
-
const all =
|
|
126
|
+
const all = [];
|
|
127
127
|
|
|
128
|
-
for (let i = 0, l =
|
|
129
|
-
|
|
128
|
+
for (let i = 0, l = arr1.length; i < l; ++i) {
|
|
129
|
+
if (all.indexOf(arr1[i]) === -1) {
|
|
130
|
+
all.push(arr1[i]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
130
133
|
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
for (let i = 0, l = arr2.length; i < l; ++i) {
|
|
135
|
+
if (all.indexOf(arr2[i]) === -1) {
|
|
136
|
+
all.push(arr2[i]);
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
|