objection 3.6.0 → 3.8.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/lib/model/AjvValidator.js +132 -52
- package/lib/model/modelClone.js +0 -2
- package/lib/queryBuilder/QueryBuilder.js +128 -24
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
- package/lib/queryBuilder/RelationExpression.js +6 -4
- package/lib/queryBuilder/StaticHookArguments.js +4 -1
- package/lib/queryBuilder/graph/GraphOptions.js +36 -79
- package/lib/queryBuilder/graph/GraphUpsert.js +35 -7
- package/lib/queryBuilder/graph/insert/GraphInsert.js +2 -0
- package/lib/queryBuilder/join/RelationJoiner.js +104 -12
- package/lib/queryBuilder/join/utils.js +2 -2
- package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
- package/lib/queryBuilder/operations/FirstOperation.js +5 -3
- package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
- package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +8 -2
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +145 -0
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
- package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
- package/lib/queryBuilder/operations/UpdateOperation.js +40 -3
- package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +2 -2
- package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
- package/lib/queryBuilder/operations/select/Selection.js +21 -20
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +3 -1
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +4 -9
- package/lib/queryBuilder/parsers/relationExpressionParser.js +6 -4
- package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +7 -3
- package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +4 -7
- package/lib/queryBuilder/whereGroups.js +35 -40
- package/lib/queryBuilder/withoutOrder.js +33 -0
- package/lib/relations/Relation.js +4 -0
- package/lib/relations/RelationFindOperation.js +30 -43
- package/lib/relations/belongsToOne/BelongsToOneDeleteOperation.js +4 -2
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +65 -19
- package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +31 -42
- package/lib/relations/manyToMany/ManyToManyRelation.js +15 -16
- package/lib/relations/manyToMany/ManyToManySqliteModifyMixin.js +0 -13
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +9 -5
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +1 -5
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperation.js +1 -5
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +9 -0
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateSqliteOperation.js +1 -5
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +16 -1
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +92 -16
- package/lib/utils/identifierMapping.js +10 -19
- package/lib/utils/knexUtils.js +14 -0
- package/lib/utils/objectUtils.js +38 -6
- package/lib/utils/parseFieldExpression.js +22 -3
- package/lib/utils/warnings.js +26 -0
- package/lib/utils/wrapDbError.js +34 -20
- package/package.json +5 -4
- package/typings/objection/index.d.ts +54 -26
- package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +0 -27
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +0 -16
- package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +0 -12
- package/lib/utils/deprecate.js +0 -21
|
@@ -26,9 +26,12 @@ class StaticHookArguments {
|
|
|
26
26
|
return () => {
|
|
27
27
|
// The `runAfter()` callbacks of the query being executed (including the
|
|
28
28
|
// ones installed by `throwIfNotFound()` and `traverse()`) are meant for
|
|
29
|
-
// its result, not for the result of this find query.
|
|
29
|
+
// its result, not for the result of this find query. The same goes for
|
|
30
|
+
// the value it was resolved with, e.g. the 0 of an update that only
|
|
31
|
+
// changes the join rows of a many-to-many relation.
|
|
30
32
|
return this[BUILDER_SYMBOL].toFindQuery()
|
|
31
33
|
.clear('runAfter')
|
|
34
|
+
.clearResolve()
|
|
32
35
|
.clearWithGraphFetched()
|
|
33
36
|
.runAfter(toArray);
|
|
34
37
|
};
|
|
@@ -15,20 +15,7 @@ const UNRELATE = 'unrelate';
|
|
|
15
15
|
const INSERT_MISSING = 'insertMissing';
|
|
16
16
|
const FETCH_STRATEGY = 'fetchStrategy';
|
|
17
17
|
const ALLOW_REFS = 'allowRefs';
|
|
18
|
-
|
|
19
|
-
const OPTION_NAMES = [
|
|
20
|
-
NO_RELATE,
|
|
21
|
-
NO_UNRELATE,
|
|
22
|
-
NO_INSERT,
|
|
23
|
-
NO_UPDATE,
|
|
24
|
-
NO_DELETE,
|
|
25
|
-
UPDATE,
|
|
26
|
-
RELATE,
|
|
27
|
-
UNRELATE,
|
|
28
|
-
INSERT_MISSING,
|
|
29
|
-
FETCH_STRATEGY,
|
|
30
|
-
ALLOW_REFS,
|
|
31
|
-
];
|
|
18
|
+
const ALLOW_MARKERS = 'allowMarkers';
|
|
32
19
|
|
|
33
20
|
// The options that are matched against the relation path of each node, see
|
|
34
21
|
// `_hasOption()`.
|
|
@@ -44,6 +31,8 @@ const NODE_OPTION_NAMES = [
|
|
|
44
31
|
INSERT_MISSING,
|
|
45
32
|
];
|
|
46
33
|
|
|
34
|
+
const OPTION_NAMES = [...NODE_OPTION_NAMES, FETCH_STRATEGY, ALLOW_REFS, ALLOW_MARKERS];
|
|
35
|
+
|
|
47
36
|
const FetchStrategy = {
|
|
48
37
|
OnlyIdentifiers: 'OnlyIdentifiers',
|
|
49
38
|
Everything: 'Everything',
|
|
@@ -52,19 +41,19 @@ const FetchStrategy = {
|
|
|
52
41
|
|
|
53
42
|
class GraphOptions {
|
|
54
43
|
// `rootRelationPath` is the relation path of the root node of a recursive
|
|
55
|
-
// upsert in the original graph, against which relation
|
|
56
|
-
|
|
44
|
+
// upsert in the original graph, against which the relation paths and
|
|
45
|
+
// expressions of the options match. `insertOnly` is only set by
|
|
46
|
+
// `insertGraph`, which never fetches, updates or removes existing rows.
|
|
47
|
+
constructor(options, rootRelationPath = [], insertOnly = false) {
|
|
57
48
|
if (options instanceof GraphOptions) {
|
|
58
49
|
this.options = options.options;
|
|
59
50
|
this.rootRelationPath = options.rootRelationPath;
|
|
51
|
+
this.insertOnly = options.insertOnly;
|
|
60
52
|
} else {
|
|
61
53
|
this.options = parseRelationExpressions(options);
|
|
62
54
|
this.rootRelationPath = rootRelationPath;
|
|
55
|
+
this.insertOnly = insertOnly;
|
|
63
56
|
}
|
|
64
|
-
|
|
65
|
-
// Caches the result of matching relation expressions against relation
|
|
66
|
-
// paths, by option name and relation path key.
|
|
67
|
-
this.expressionMatches = new Map();
|
|
68
57
|
}
|
|
69
58
|
|
|
70
59
|
isFetchStrategy(strategy) {
|
|
@@ -80,17 +69,12 @@ class GraphOptions {
|
|
|
80
69
|
}
|
|
81
70
|
|
|
82
71
|
isInsertOnly() {
|
|
83
|
-
|
|
84
|
-
// relate things that can be related using inserts.
|
|
85
|
-
// TODO: Use a special key for this.
|
|
86
|
-
return [NO_DELETE, NO_UPDATE, NO_UNRELATE, INSERT_MISSING].every((opt) => {
|
|
87
|
-
return this.options[opt] === true;
|
|
88
|
-
});
|
|
72
|
+
return this.insertOnly;
|
|
89
73
|
}
|
|
90
74
|
|
|
91
75
|
// Like `shouldRelate` but ignores settings that explicitly disable relate operations.
|
|
92
76
|
shouldRelateIgnoreDisable(node, graphData) {
|
|
93
|
-
if (node.
|
|
77
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
94
78
|
return false;
|
|
95
79
|
}
|
|
96
80
|
|
|
@@ -114,7 +98,7 @@ class GraphOptions {
|
|
|
114
98
|
|
|
115
99
|
// Like `shouldInsert` but ignores settings that explicitly disable insert operations.
|
|
116
100
|
shouldInsertIgnoreDisable(node, graphData) {
|
|
117
|
-
if (node.
|
|
101
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
118
102
|
return false;
|
|
119
103
|
}
|
|
120
104
|
|
|
@@ -136,7 +120,7 @@ class GraphOptions {
|
|
|
136
120
|
// Like `shouldPatch() || shouldUpdate()` but ignores settings that explicitly disable
|
|
137
121
|
// update or patch operations.
|
|
138
122
|
shouldPatchOrUpdateIgnoreDisable(node, graphData) {
|
|
139
|
-
if (node.
|
|
123
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
140
124
|
return false;
|
|
141
125
|
}
|
|
142
126
|
|
|
@@ -206,33 +190,23 @@ class GraphOptions {
|
|
|
206
190
|
return !!this.options[ALLOW_REFS];
|
|
207
191
|
}
|
|
208
192
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for (const name of Object.keys(this.options)) {
|
|
214
|
-
const value = this.options[name];
|
|
215
|
-
|
|
216
|
-
if (Array.isArray(value)) {
|
|
217
|
-
newOpt[name] = value
|
|
218
|
-
.filter((it) => it === newRootRelationPath || it.startsWith(`${newRootRelationPath}.`))
|
|
219
|
-
.map((it) => it.slice(newRootRelationPath.length + 1))
|
|
220
|
-
.filter((it) => !!it);
|
|
221
|
-
} else {
|
|
222
|
-
newOpt[name] = value;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
193
|
+
allowMarkers() {
|
|
194
|
+
return !!this.options[ALLOW_MARKERS];
|
|
195
|
+
}
|
|
225
196
|
|
|
226
|
-
|
|
197
|
+
// The options of recursive upserts keep matching against the relation paths
|
|
198
|
+
// of the original graph, which the relation paths of the new root lead to.
|
|
199
|
+
rebasedOptions(newRoot) {
|
|
200
|
+
return new GraphOptions(this.options, [...this.rootRelationPath, ...newRoot.relationPath]);
|
|
227
201
|
}
|
|
228
202
|
|
|
229
203
|
_hasOption(node, optionName) {
|
|
230
204
|
const option = this.options[optionName];
|
|
231
205
|
|
|
232
206
|
if (isRelationExpression(option)) {
|
|
233
|
-
return this.
|
|
207
|
+
return matchesRelationExpression(option, this._relationPath(node));
|
|
234
208
|
} else if (Array.isArray(option)) {
|
|
235
|
-
return option.
|
|
209
|
+
return option.includes(this._relationPath(node).join('.'));
|
|
236
210
|
} else if (typeof option === 'boolean') {
|
|
237
211
|
return option;
|
|
238
212
|
} else if (option === undefined) {
|
|
@@ -244,36 +218,9 @@ class GraphOptions {
|
|
|
244
218
|
}
|
|
245
219
|
}
|
|
246
220
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// `'*'` matches all relations below it and recursive expressions like
|
|
251
|
-
// `'parent.^'` match every level of the recursion. The root has no relation
|
|
252
|
-
// path and never matches.
|
|
253
|
-
_matchesRelationExpression(node, optionName, expression) {
|
|
254
|
-
const relationPath = [...this.rootRelationPath, ...node.relationPath];
|
|
255
|
-
|
|
256
|
-
if (relationPath.length === 0) {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const relationPathKey = relationPath.join('.');
|
|
261
|
-
|
|
262
|
-
let matches = this.expressionMatches.get(optionName);
|
|
263
|
-
|
|
264
|
-
if (!matches) {
|
|
265
|
-
matches = new Map();
|
|
266
|
-
this.expressionMatches.set(optionName, matches);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
let isMatch = matches.get(relationPathKey);
|
|
270
|
-
|
|
271
|
-
if (isMatch === undefined) {
|
|
272
|
-
isMatch = matchesRelationPath(expression, relationPath);
|
|
273
|
-
matches.set(relationPathKey, isMatch);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return isMatch;
|
|
221
|
+
// The relation path of the node in the original graph, see `rebasedOptions()`.
|
|
222
|
+
_relationPath(node) {
|
|
223
|
+
return [...this.rootRelationPath, ...node.relationPath];
|
|
277
224
|
}
|
|
278
225
|
}
|
|
279
226
|
|
|
@@ -344,7 +291,17 @@ function findAlias(node) {
|
|
|
344
291
|
return null;
|
|
345
292
|
}
|
|
346
293
|
|
|
347
|
-
|
|
294
|
+
// A relation path matches a relation expression when it leads to a leaf of
|
|
295
|
+
// the expression, like it would match the same relation paths in an array:
|
|
296
|
+
// `'movies.reviews'` matches `movies.reviews` but not `movies`. `'*'` matches
|
|
297
|
+
// all relations below it and recursive expressions like `'parent.^'` match
|
|
298
|
+
// every level of the recursion. The root has no relation path and never
|
|
299
|
+
// matches.
|
|
300
|
+
function matchesRelationExpression(expression, relationPath) {
|
|
301
|
+
if (relationPath.length === 0) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
348
305
|
for (const relationName of relationPath) {
|
|
349
306
|
expression = expression.childExpression(relationName);
|
|
350
307
|
|
|
@@ -14,16 +14,17 @@ 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 {
|
|
17
|
+
const { warnOnce } = require('../../utils/warnings');
|
|
18
18
|
|
|
19
19
|
class GraphUpsert {
|
|
20
|
-
constructor({ rootModelClass, objects, upsertOptions }) {
|
|
20
|
+
constructor({ rootModelClass, objects, upsertOptions, insertOnly = false }) {
|
|
21
21
|
checkCanBeConvertedToModels(rootModelClass, objects);
|
|
22
22
|
warnOnUnknownOptions(upsertOptions);
|
|
23
23
|
|
|
24
24
|
this.objects = rootModelClass.ensureModelArray(objects, GraphUpsert.modelOptions);
|
|
25
25
|
this.isArray = Array.isArray(objects);
|
|
26
26
|
this.upsertOpt = upsertOptions;
|
|
27
|
+
this.insertOnly = insertOnly;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
static get modelOptions() {
|
|
@@ -39,7 +40,7 @@ class GraphUpsert {
|
|
|
39
40
|
// TODO: Throw an error instead in objection 4.0.
|
|
40
41
|
static warnOnConflict(builder, method) {
|
|
41
42
|
if (builder.has(isOnConflictOperation)) {
|
|
42
|
-
|
|
43
|
+
warnOnce(
|
|
43
44
|
`onConflict(), ignore() and merge() are not supported by ${method}(). ` +
|
|
44
45
|
'Insert the conflicting rows with a separate insert() query instead. ' +
|
|
45
46
|
'This will throw in objection 4.0.',
|
|
@@ -49,7 +50,7 @@ class GraphUpsert {
|
|
|
49
50
|
|
|
50
51
|
run(builder) {
|
|
51
52
|
const modelClass = builder.modelClass();
|
|
52
|
-
const graphOptions = new GraphOptions(this.upsertOpt);
|
|
53
|
+
const graphOptions = new GraphOptions(this.upsertOpt, [], this.insertOnly);
|
|
53
54
|
|
|
54
55
|
const graph = ModelGraph.create(modelClass, this.objects);
|
|
55
56
|
assignDbRefsAsRelateProps(graph);
|
|
@@ -74,7 +75,7 @@ function warnOnUnknownOptions(upsertOptions) {
|
|
|
74
75
|
|
|
75
76
|
for (const name of Object.keys(upsertOptions)) {
|
|
76
77
|
if (!OPTION_NAMES.includes(name)) {
|
|
77
|
-
|
|
78
|
+
warnOnce(
|
|
78
79
|
`Unknown graph option "${name}" is ignored. ` +
|
|
79
80
|
`Valid options are: ${OPTION_NAMES.join(', ')}. This will throw in objection 4.0.`,
|
|
80
81
|
);
|
|
@@ -245,7 +246,10 @@ function checkForErrors(builder) {
|
|
|
245
246
|
checkForNotFoundErrors(graphData, builder);
|
|
246
247
|
checkForUnallowedRelationErrors(graphData, builder);
|
|
247
248
|
checkForUnallowedReferenceErrors(graphData, builder);
|
|
249
|
+
// Root models can't be marked even with `allowMarkers`, so report that
|
|
250
|
+
// first instead of pointing to the option.
|
|
248
251
|
checkForRootUnrelateOrDeleteErrors(graphData, builder);
|
|
252
|
+
checkForUnallowedMarkerErrors(graphData, builder);
|
|
249
253
|
|
|
250
254
|
if (graphData.graphOptions.isInsertOnly()) {
|
|
251
255
|
checkForHasManyRelateErrors(graphData);
|
|
@@ -290,8 +294,7 @@ function checkForNotFoundErrors(graphData, builder) {
|
|
|
290
294
|
for (const node of graph.nodes) {
|
|
291
295
|
if (
|
|
292
296
|
node.obj.$hasId() &&
|
|
293
|
-
!node.
|
|
294
|
-
!node.toBeDeleted &&
|
|
297
|
+
!node.toBeUnrelatedOrDeleted &&
|
|
295
298
|
!graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
|
|
296
299
|
!graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
|
|
297
300
|
!currentGraph.nodeForNode(node)
|
|
@@ -344,6 +347,31 @@ function checkForUnallowedReferenceErrors(graphData, builder) {
|
|
|
344
347
|
}
|
|
345
348
|
}
|
|
346
349
|
|
|
350
|
+
// `#unrelate` and `#delete` markers come from the graph data, which may be
|
|
351
|
+
// untrusted input. Like `#ref`, they have to be allowed explicitly (#2410).
|
|
352
|
+
// An insert has nothing to unrelate or delete, so `insertGraph` rejects them.
|
|
353
|
+
function checkForUnallowedMarkerErrors(graphData, builder) {
|
|
354
|
+
const { graph, graphOptions } = graphData;
|
|
355
|
+
const isInsertOnly = graphOptions.isInsertOnly();
|
|
356
|
+
|
|
357
|
+
if (graphOptions.allowMarkers() && !isInsertOnly) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const node = graph.nodes.find((node) => node.toBeUnrelatedOrDeleted);
|
|
362
|
+
|
|
363
|
+
if (node) {
|
|
364
|
+
const { graphUnrelateProp, graphDeleteProp } = node.modelClass;
|
|
365
|
+
|
|
366
|
+
throw builder.modelClass().createValidationError({
|
|
367
|
+
type: ValidationErrorType.InvalidGraph,
|
|
368
|
+
message: isInsertOnly
|
|
369
|
+
? `${graphUnrelateProp} and ${graphDeleteProp} can only be used in upsertGraph, not in insertGraph`
|
|
370
|
+
: `${graphUnrelateProp} and ${graphDeleteProp} markers are not allowed in a graph by default. see the allowMarkers upsertGraph option`,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
347
375
|
function checkForRootUnrelateOrDeleteErrors(graphData, builder) {
|
|
348
376
|
for (const node of graphData.graph.nodes) {
|
|
349
377
|
if (!node.parentEdge && node.toBeUnrelatedOrDeleted) {
|
|
@@ -140,6 +140,8 @@ class GraphInsert extends GraphOperation {
|
|
|
140
140
|
edge.relation.isObjectionBelongsToOneRelation &&
|
|
141
141
|
edge.isOwnerNode(node) &&
|
|
142
142
|
edge.relatedNode.isReference &&
|
|
143
|
+
// The owner is patched by its id, which models without one don't have (#2305).
|
|
144
|
+
node.modelClass.getIdColumnArray().length > 0 &&
|
|
143
145
|
// Both ends need to be inserted for the foreign key to be patched in later.
|
|
144
146
|
this.graphOptions.shouldInsert(node, this.graphData) &&
|
|
145
147
|
this.graphOptions.shouldInsert(edge.relatedNode.referencedNode, this.graphData) &&
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { uniqBy } = require('../../utils/objectUtils');
|
|
4
|
-
const { Selection } = require('../operations/select/Selection');
|
|
4
|
+
const { Selection, mapIdentifier } = require('../operations/select/Selection');
|
|
5
5
|
const { SelectOperation } = require('../operations/select/SelectOperation');
|
|
6
6
|
const { createModifier } = require('../../utils/createModifier');
|
|
7
7
|
const { map: mapPromise } = require('../../utils/promiseUtils');
|
|
@@ -89,6 +89,13 @@ class RelationJoiner {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Whether the joins can return more than one row per root model.
|
|
93
|
+
hasToManyJoins(builder) {
|
|
94
|
+
return this._getTableTree(builder).nodes.some(
|
|
95
|
+
(tableNode) => tableNode.relation && !tableNode.relation.isOneToOne(),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
parseResult(builder, flatRows) {
|
|
93
100
|
const parser = JoinResultParser.create({
|
|
94
101
|
tableTree: this._getTableTree(builder),
|
|
@@ -203,7 +210,60 @@ class RelationJoiner {
|
|
|
203
210
|
}
|
|
204
211
|
}
|
|
205
212
|
|
|
206
|
-
|
|
213
|
+
// Each name is selected only once, as SQL Server returns duplicate names as
|
|
214
|
+
// arrays of values, and the other databases silently return one of them.
|
|
215
|
+
removeRootSelectAll(builder);
|
|
216
|
+
const tableTree = this._getTableTree(builder);
|
|
217
|
+
const userSelectionsByName = new Map();
|
|
218
|
+
|
|
219
|
+
for (const selection of findJoinSelections(builder)) {
|
|
220
|
+
// Only the columns of the joined tables are known for `table.*`:
|
|
221
|
+
const node = isSelectAll(selection) ? tableTree.nodesByAlias.get(selection.table) : null;
|
|
222
|
+
const names = node
|
|
223
|
+
? getAllColumnSelectionsForNode({ builder, tableNode: node }).map((it) => it.column)
|
|
224
|
+
: [selection.name];
|
|
225
|
+
|
|
226
|
+
for (const name of names) {
|
|
227
|
+
userSelectionsByName.set(mapIdentifier(builder, name), { selection, name });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const idAliases = new Set(
|
|
232
|
+
tableTree.nodes.flatMap((node) =>
|
|
233
|
+
getIdSelectionsForNode({ tableNode: node }).map((it) => it.alias),
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
builder.select(
|
|
238
|
+
selectionsToStrings(
|
|
239
|
+
selections.filter(({ table, column, alias }) => {
|
|
240
|
+
const { selection, name } = userSelectionsByName.get(mapIdentifier(builder, alias)) || {};
|
|
241
|
+
|
|
242
|
+
if (!selection) {
|
|
243
|
+
return true;
|
|
244
|
+
} else if (selection.column) {
|
|
245
|
+
if (!Selection.doesSelect(builder, selection, `${table}.${column}`)) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`withGraphJoined: the selection '${selectionToString(selection)}' selects '${name}', ` +
|
|
248
|
+
`the name under which the column '${table}.${column}' is selected. ` +
|
|
249
|
+
'Select it under a different name.',
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
} else if (idAliases.has(alias)) {
|
|
253
|
+
// Raws, subqueries and aggregates select values that we can't see
|
|
254
|
+
// into. They replace our columns, but not the ids that we need to
|
|
255
|
+
// parse the result.
|
|
256
|
+
throw new Error(
|
|
257
|
+
`withGraphJoined: a raw, subquery or aggregate selection selects '${name}', ` +
|
|
258
|
+
`the name under which the id column '${table}.${column}' is selected. ` +
|
|
259
|
+
'Select it under a different name, or select the id column itself.',
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return false;
|
|
264
|
+
}),
|
|
265
|
+
),
|
|
266
|
+
);
|
|
207
267
|
}
|
|
208
268
|
}
|
|
209
269
|
|
|
@@ -268,8 +328,9 @@ function ensureIdAndRelationPropsAreSelected({ builder, tableNode }) {
|
|
|
268
328
|
),
|
|
269
329
|
];
|
|
270
330
|
|
|
331
|
+
const hasSelectionAs = createHasSelectionAs(builder);
|
|
271
332
|
const selectStrings = uniqBy(cols)
|
|
272
|
-
.filter((col) => !hasSelectionAs(
|
|
333
|
+
.filter((col) => !hasSelectionAs(col))
|
|
273
334
|
.map((col) => `${tableRef}.${col}`);
|
|
274
335
|
|
|
275
336
|
return builder.select(selectStrings);
|
|
@@ -297,9 +358,10 @@ function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
|
|
|
297
358
|
selections = allColumnSelections.concat(selections);
|
|
298
359
|
} else {
|
|
299
360
|
const idSelections = getIdSelectionsForNode({ tableNode });
|
|
361
|
+
const hasUserSelectionAs = createHasUserSelectionAs(userSelectQuery, tableNode);
|
|
300
362
|
|
|
301
363
|
for (const idSelection of idSelections) {
|
|
302
|
-
if (!hasUserSelectionAs(
|
|
364
|
+
if (!hasUserSelectionAs(idSelection.column)) {
|
|
303
365
|
selections.push(idSelection);
|
|
304
366
|
internalSelections.push(idSelection);
|
|
305
367
|
}
|
|
@@ -334,14 +396,16 @@ function findJoinSelections(builder) {
|
|
|
334
396
|
return selections;
|
|
335
397
|
}
|
|
336
398
|
|
|
337
|
-
//
|
|
338
|
-
// passed to aggregates, as they don't select them.
|
|
339
|
-
|
|
399
|
+
// Create a `(column) => builder.hasSelectionAs(column, column)` function that
|
|
400
|
+
// ignores the columns passed to aggregates, as they don't select them. Clone
|
|
401
|
+
// the builder once, not once per column.
|
|
402
|
+
function createHasSelectionAs(builder) {
|
|
340
403
|
if (builder.has(isAggregateOperation)) {
|
|
404
|
+
const selectionQuery = builder.clone().clear(isAggregateOperation);
|
|
341
405
|
// Explicit, as a query with only aggregates doesn't select all columns.
|
|
342
|
-
return
|
|
406
|
+
return (column) => selectionQuery.hasSelectionAs(column, column, true);
|
|
343
407
|
} else {
|
|
344
|
-
return builder.hasSelectionAs(column, column);
|
|
408
|
+
return (column) => builder.hasSelectionAs(column, column);
|
|
345
409
|
}
|
|
346
410
|
}
|
|
347
411
|
|
|
@@ -355,11 +419,12 @@ function findUserSelections(builder, tableNode) {
|
|
|
355
419
|
}
|
|
356
420
|
}
|
|
357
421
|
|
|
358
|
-
function
|
|
422
|
+
function createHasUserSelectionAs(builder, tableNode) {
|
|
359
423
|
if (tableNode.parentNode) {
|
|
360
|
-
return
|
|
424
|
+
return createHasSelectionAs(builder);
|
|
361
425
|
} else {
|
|
362
|
-
|
|
426
|
+
// Qualified, as the root query can also select the columns of the joined tables.
|
|
427
|
+
return (column) => builder.hasSelectionAs(`${tableNode.alias}.${column}`, column);
|
|
363
428
|
}
|
|
364
429
|
}
|
|
365
430
|
|
|
@@ -416,6 +481,28 @@ function getIdSelectionsForNode({ tableNode }) {
|
|
|
416
481
|
});
|
|
417
482
|
}
|
|
418
483
|
|
|
484
|
+
// The selections of all columns of the root table replace the user's `*`, which
|
|
485
|
+
// would select them a second time, and a bare `*` also the columns of the
|
|
486
|
+
// joined tables under the same names.
|
|
487
|
+
function removeRootSelectAll(builder) {
|
|
488
|
+
const tableRef = builder.tableRef();
|
|
489
|
+
const isRootSelectAll = (selection) =>
|
|
490
|
+
selection.column === '*' && (!selection.table || selection.table === tableRef);
|
|
491
|
+
const isSelect = (op) =>
|
|
492
|
+
op.constructor === SelectOperation && !isAggregateOperation(op) && !op.isDistinctOn;
|
|
493
|
+
|
|
494
|
+
builder.forEachOperation(isSelect, (op) => {
|
|
495
|
+
op.args = op.args.filter((arg) => {
|
|
496
|
+
const selection = Selection.create(arg);
|
|
497
|
+
return !selection || !isRootSelectAll(selection);
|
|
498
|
+
});
|
|
499
|
+
op.selections = op.selections.filter((selection) => !isRootSelectAll(selection));
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// An empty `distinct()` still makes the whole select distinct.
|
|
503
|
+
builder.clear((op) => isSelect(op) && op.name !== 'distinct' && op.args.length === 0);
|
|
504
|
+
}
|
|
505
|
+
|
|
419
506
|
function selectionsToStrings(selections) {
|
|
420
507
|
return selections.map((selection) => {
|
|
421
508
|
const selectStr = `${selection.table}.${selection.column}`;
|
|
@@ -423,6 +510,11 @@ function selectionsToStrings(selections) {
|
|
|
423
510
|
});
|
|
424
511
|
}
|
|
425
512
|
|
|
513
|
+
function selectionToString({ table, column, alias }) {
|
|
514
|
+
const selection = table ? `${table}.${column}` : column;
|
|
515
|
+
return alias ? `${selection} as ${alias}` : selection;
|
|
516
|
+
}
|
|
517
|
+
|
|
426
518
|
function isSelectAll(selection) {
|
|
427
519
|
return selection.column === '*';
|
|
428
520
|
}
|
|
@@ -6,8 +6,8 @@ const { isPostgres, isMySql, isMsSql, isSqlite } = require('../../utils/knexUtil
|
|
|
6
6
|
const RELATION_RECURSION_LIMIT = 64;
|
|
7
7
|
|
|
8
8
|
// The maximum length of the column aliases of the joined relations, after
|
|
9
|
-
// which the database would truncate them.
|
|
10
|
-
//
|
|
9
|
+
// which the database would truncate them. Other databases keep the limit of
|
|
10
|
+
// postgres, which isn't the strictest one: Oracle before 12.2 only allows 30.
|
|
11
11
|
function getIdentifierLengthLimit(knex) {
|
|
12
12
|
if (isPostgres(knex)) {
|
|
13
13
|
return 63;
|
|
@@ -55,6 +55,17 @@ class DelegateOperation extends QueryBuilderOperation {
|
|
|
55
55
|
return this.onBuild !== DelegateOperation.prototype.onBuild || this.delegate.hasOnBuild();
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
onAfterBuild(builder) {
|
|
59
|
+
return this.delegate.onAfterBuild(builder);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
hasOnAfterBuild() {
|
|
63
|
+
return (
|
|
64
|
+
this.onAfterBuild !== DelegateOperation.prototype.onAfterBuild ||
|
|
65
|
+
this.delegate.hasOnAfterBuild()
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
onBuildKnex(knexBuilder, builder) {
|
|
59
70
|
return this.delegate.onBuildKnex(knexBuilder, builder);
|
|
60
71
|
}
|
|
@@ -110,6 +121,17 @@ class DelegateOperation extends QueryBuilderOperation {
|
|
|
110
121
|
);
|
|
111
122
|
}
|
|
112
123
|
|
|
124
|
+
queryBatches(builder) {
|
|
125
|
+
return this.delegate.queryBatches(builder);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
hasQueryBatches() {
|
|
129
|
+
return (
|
|
130
|
+
this.queryBatches !== DelegateOperation.prototype.queryBatches ||
|
|
131
|
+
this.delegate.hasQueryBatches()
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
113
135
|
onError(builder, error) {
|
|
114
136
|
return this.delegate.onError(builder, error);
|
|
115
137
|
}
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
4
4
|
|
|
5
5
|
class FirstOperation extends QueryBuilderOperation {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
shouldLimit(builder) {
|
|
7
|
+
return builder.isFind() && builder.modelClass().useLimitInFirst;
|
|
8
|
+
}
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
onBuildKnex(knexBuilder, builder) {
|
|
11
|
+
if (this.shouldLimit(builder)) {
|
|
10
12
|
knexBuilder = knexBuilder.limit(1);
|
|
11
13
|
}
|
|
12
14
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { DeleteOperation } = require('./DeleteOperation');
|
|
4
4
|
const { InstanceFindOperation } = require('./InstanceFindOperation');
|
|
5
5
|
const { assertHasId } = require('../../utils/assert');
|
|
6
|
+
const { addWhereGroup } = require('../whereGroups');
|
|
6
7
|
|
|
7
8
|
class InstanceDeleteOperation extends DeleteOperation {
|
|
8
9
|
constructor(name, opt) {
|
|
@@ -20,7 +21,7 @@ class InstanceDeleteOperation extends DeleteOperation {
|
|
|
20
21
|
super.onBuild(builder);
|
|
21
22
|
|
|
22
23
|
assertHasId(this.instance);
|
|
23
|
-
builder.findById(this.instance.$id());
|
|
24
|
+
addWhereGroup(builder, () => builder.findById(this.instance.$id()));
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
async onAfter2(builder, result) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { FindOperation } = require('./FindOperation');
|
|
4
4
|
const { assertHasId } = require('../../utils/assert');
|
|
5
|
+
const { addWhereGroup } = require('../whereGroups');
|
|
5
6
|
|
|
6
7
|
class InstanceFindOperation extends FindOperation {
|
|
7
8
|
constructor(name, opt) {
|
|
@@ -11,7 +12,7 @@ class InstanceFindOperation extends FindOperation {
|
|
|
11
12
|
|
|
12
13
|
onBuild(builder) {
|
|
13
14
|
assertHasId(this.instance);
|
|
14
|
-
builder.findById(this.instance.$id());
|
|
15
|
+
addWhereGroup(builder, () => builder.findById(this.instance.$id()));
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -4,6 +4,7 @@ const { UpdateOperation } = require('./UpdateOperation');
|
|
|
4
4
|
const { InstanceFindOperation } = require('./InstanceFindOperation');
|
|
5
5
|
const { assertHasId } = require('../../utils/assert');
|
|
6
6
|
const { isObject } = require('../../utils/objectUtils');
|
|
7
|
+
const { addWhereGroup } = require('../whereGroups');
|
|
7
8
|
|
|
8
9
|
class InstanceUpdateOperation extends UpdateOperation {
|
|
9
10
|
constructor(name, opt) {
|
|
@@ -27,7 +28,7 @@ class InstanceUpdateOperation extends UpdateOperation {
|
|
|
27
28
|
super.onBuild(builder);
|
|
28
29
|
|
|
29
30
|
assertHasId(this.instance);
|
|
30
|
-
builder.findById(this.instance.$id());
|
|
31
|
+
addWhereGroup(builder, () => builder.findById(this.instance.$id()));
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
async onAfter2(builder, result) {
|
|
@@ -37,7 +38,12 @@ class InstanceUpdateOperation extends UpdateOperation {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
result = await super.onAfter2(builder, result);
|
|
40
|
-
|
|
41
|
+
|
|
42
|
+
// A query resolved without updating anything, e.g. with `none()`, leaves
|
|
43
|
+
// the instance untouched.
|
|
44
|
+
if (!this.isResolvedWithoutUpdate(builder)) {
|
|
45
|
+
this.instance.$set(this.model);
|
|
46
|
+
}
|
|
41
47
|
|
|
42
48
|
if (isObject(result)) {
|
|
43
49
|
this.instance.$set(result);
|