objection 3.7.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 +101 -44
- package/lib/queryBuilder/QueryBuilder.js +91 -33
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
- package/lib/queryBuilder/StaticHookArguments.js +4 -1
- package/lib/queryBuilder/graph/GraphOptions.js +11 -26
- package/lib/queryBuilder/graph/GraphUpsert.js +16 -10
- 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 +2 -1
- 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 +29 -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/parsers/relationExpressionParser.js +5 -3
- package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +6 -2
- package/lib/queryBuilder/whereGroups.js +35 -40
- package/lib/queryBuilder/withoutOrder.js +33 -0
- package/lib/relations/Relation.js +3 -0
- package/lib/relations/RelationFindOperation.js +9 -4
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +31 -9
- package/lib/relations/manyToMany/ManyToManyRelation.js +7 -0
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +7 -2
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +16 -1
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +87 -17
- package/lib/utils/identifierMapping.js +2 -2
- package/lib/utils/knexUtils.js +14 -0
- package/lib/utils/parseFieldExpression.js +12 -0
- 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 +10 -2
- package/lib/utils/deprecate.js +0 -21
|
@@ -5,7 +5,7 @@ const addFormats = require('ajv-formats');
|
|
|
5
5
|
const { Validator } = require('./Validator');
|
|
6
6
|
const { ValidationErrorType } = require('../model/ValidationError');
|
|
7
7
|
const { isObject, once, cloneDeep: lodashCloneDeep, omit } = require('../utils/objectUtils');
|
|
8
|
-
const { parseFieldExpression } = require('../utils/parseFieldExpression');
|
|
8
|
+
const { parseFieldExpression, isFieldExpressionKey } = require('../utils/parseFieldExpression');
|
|
9
9
|
const { getQueryPropKeys } = require('./modelQueryProps');
|
|
10
10
|
|
|
11
11
|
class AjvValidator extends Validator {
|
|
@@ -79,27 +79,60 @@ class AjvValidator extends Validator {
|
|
|
79
79
|
return json;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const modelClass = model.constructor;
|
|
83
|
-
const validator = this.getValidator(
|
|
84
|
-
modelClass,
|
|
85
|
-
ctx.jsonSchema,
|
|
86
|
-
!!options.patch,
|
|
87
|
-
options.patch ? null : getMissingQueryPropKeys(model, json, ctx.jsonSchema),
|
|
88
|
-
);
|
|
89
|
-
|
|
90
82
|
// We need to clone the input json if we are about to set default values.
|
|
91
83
|
if (!options.mutable && !options.patch && setsDefaultValues(ctx.jsonSchema)) {
|
|
92
84
|
json = cloneDeep(json);
|
|
93
85
|
}
|
|
94
86
|
|
|
95
|
-
// Patch objects can contain field expression keys like `'meta:b'`
|
|
96
|
-
// a single property inside a json column. Validate them against
|
|
97
|
-
// schema by converting them into nested objects like
|
|
98
|
-
|
|
99
|
-
const
|
|
87
|
+
// Patch and update objects can contain field expression keys like `'meta:b'`
|
|
88
|
+
// that update a single property inside a json column. Validate them against
|
|
89
|
+
// the nested schema by converting them into nested objects like
|
|
90
|
+
// `{ meta: { b: value } }`.
|
|
91
|
+
const queryPropKeys = getQueryPropKeys(model);
|
|
92
|
+
const fieldExpressions = nestFieldExpressions(json, queryPropKeys, ctx.jsonSchema);
|
|
93
|
+
let validationJson = fieldExpressions ? fieldExpressions.json : json;
|
|
94
|
+
|
|
95
|
+
const modelClass = model.constructor;
|
|
96
|
+
const validate = (json, isPatchObject, givenKeys = []) => {
|
|
97
|
+
const validator = this.getValidator(
|
|
98
|
+
modelClass,
|
|
99
|
+
ctx.jsonSchema,
|
|
100
|
+
isPatchObject,
|
|
101
|
+
isPatchObject ? null : getRequiredKeysGivenOutside(json, ctx.jsonSchema, givenKeys),
|
|
102
|
+
);
|
|
103
|
+
validator.call(model, json);
|
|
104
|
+
return validator.errors || [];
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
let errors;
|
|
108
|
+
|
|
109
|
+
if (options.patch) {
|
|
110
|
+
errors = validate(validationJson, true);
|
|
111
|
+
} else if (fieldExpressions && fieldExpressions.columns.length > 0) {
|
|
112
|
+
// Field expressions only write parts of their json columns. Validate these
|
|
113
|
+
// parts like patches, and the rest normally, with the columns counted as
|
|
114
|
+
// present like the query properties.
|
|
115
|
+
const { columns } = fieldExpressions;
|
|
116
|
+
const columnsJson = Object.fromEntries(
|
|
117
|
+
columns
|
|
118
|
+
.filter((column) => hasOwn(validationJson, column))
|
|
119
|
+
.map((column) => [column, validationJson[column]]),
|
|
120
|
+
);
|
|
121
|
+
const restJson = omit(validationJson, columns);
|
|
100
122
|
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
errors = [
|
|
124
|
+
...validate(columnsJson, true),
|
|
125
|
+
...validate(restJson, false, [...queryPropKeys, ...columns]),
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// Combine the parts again, without the defaults that Ajv set for the
|
|
129
|
+
// columns in `restJson`.
|
|
130
|
+
validationJson = Object.assign(restJson, columnsJson);
|
|
131
|
+
} else {
|
|
132
|
+
errors = validate(validationJson, false, queryPropKeys);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const error = parseValidationError(errors, modelClass, options, this.ajvOptions);
|
|
103
136
|
|
|
104
137
|
if (error) {
|
|
105
138
|
throw error;
|
|
@@ -107,7 +140,7 @@ class AjvValidator extends Validator {
|
|
|
107
140
|
|
|
108
141
|
if (fieldExpressions) {
|
|
109
142
|
// Copy back the changes made by Ajv options like `coerceTypes` and `removeAdditional`.
|
|
110
|
-
unnestFieldExpressions(json, fieldExpressions);
|
|
143
|
+
unnestFieldExpressions(json, validationJson, fieldExpressions);
|
|
111
144
|
}
|
|
112
145
|
|
|
113
146
|
return json;
|
|
@@ -148,7 +181,8 @@ class AjvValidator extends Validator {
|
|
|
148
181
|
normalValidator: null,
|
|
149
182
|
|
|
150
183
|
// Validators created for the schema object without the `required`
|
|
151
|
-
// properties that are given
|
|
184
|
+
// properties that are given outside the json, as query properties or
|
|
185
|
+
// json columns written by field expressions, keyed by their names.
|
|
152
186
|
queryPropValidators: null,
|
|
153
187
|
};
|
|
154
188
|
|
|
@@ -298,10 +332,11 @@ function parseValidationError(errors, modelClass, options, ajvOptions) {
|
|
|
298
332
|
});
|
|
299
333
|
}
|
|
300
334
|
|
|
301
|
-
function nestFieldExpressions(json) {
|
|
335
|
+
function nestFieldExpressions(json, queryPropKeys, jsonSchema) {
|
|
336
|
+
const isFieldExpression = (key) => isFieldExpressionKey(key, jsonSchema.properties);
|
|
302
337
|
const keys = Object.keys(json);
|
|
303
338
|
|
|
304
|
-
if (!keys.some(isFieldExpression)) {
|
|
339
|
+
if (!keys.some(isFieldExpression) && !queryPropKeys.some(isFieldExpression)) {
|
|
305
340
|
return null;
|
|
306
341
|
}
|
|
307
342
|
|
|
@@ -310,6 +345,15 @@ function nestFieldExpressions(json) {
|
|
|
310
345
|
// The objects created here for the nested paths. Other objects are values
|
|
311
346
|
// of the input json and must not be modified.
|
|
312
347
|
const createdObjects = new Set();
|
|
348
|
+
// The json columns that are only written partially, by field expressions in
|
|
349
|
+
// the json or the query properties.
|
|
350
|
+
const columns = new Set();
|
|
351
|
+
|
|
352
|
+
const addColumn = (expression) => {
|
|
353
|
+
if (expression && !hasOwn(json, expression.column)) {
|
|
354
|
+
columns.add(expression.column);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
313
357
|
|
|
314
358
|
for (const key of keys) {
|
|
315
359
|
if (!isFieldExpression(key)) {
|
|
@@ -320,7 +364,10 @@ function nestFieldExpressions(json) {
|
|
|
320
364
|
|
|
321
365
|
for (const key of keys) {
|
|
322
366
|
if (isFieldExpression(key)) {
|
|
323
|
-
const
|
|
367
|
+
const expression = parseFieldExpressionKey(key);
|
|
368
|
+
// Not a valid field expression, validate it as a top-level property:
|
|
369
|
+
const path = expression ? expression.path : [key];
|
|
370
|
+
addColumn(expression);
|
|
324
371
|
|
|
325
372
|
// Field expressions with array access, and those that conflict with other
|
|
326
373
|
// keys of the patch, can't be validated without the current value of the
|
|
@@ -331,10 +378,16 @@ function nestFieldExpressions(json) {
|
|
|
331
378
|
}
|
|
332
379
|
}
|
|
333
380
|
|
|
334
|
-
|
|
381
|
+
for (const key of queryPropKeys) {
|
|
382
|
+
if (isFieldExpression(key)) {
|
|
383
|
+
addColumn(parseFieldExpressionKey(key));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { json: nested, paths, columns: [...columns] };
|
|
335
388
|
}
|
|
336
389
|
|
|
337
|
-
function unnestFieldExpressions(json,
|
|
390
|
+
function unnestFieldExpressions(json, nested, { paths, columns }) {
|
|
338
391
|
for (const [key, path] of paths) {
|
|
339
392
|
let value = nested;
|
|
340
393
|
let found = true;
|
|
@@ -356,29 +409,35 @@ function unnestFieldExpressions(json, { json: nested, paths }) {
|
|
|
356
409
|
json[key] = value;
|
|
357
410
|
}
|
|
358
411
|
}
|
|
359
|
-
}
|
|
360
412
|
|
|
361
|
-
|
|
362
|
-
|
|
413
|
+
// Copy over the default values that Ajv set for missing properties, but not
|
|
414
|
+
// the json columns written by the field expressions.
|
|
415
|
+
for (const key of Object.keys(nested)) {
|
|
416
|
+
if (!paths.has(key) && !columns.includes(key)) {
|
|
417
|
+
json[key] = nested[key];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
363
420
|
}
|
|
364
421
|
|
|
365
|
-
|
|
422
|
+
// Parse a field expression key into its json column and nested path, with the
|
|
423
|
+
// path set to `null` if the key accesses array elements.
|
|
424
|
+
function parseFieldExpressionKey(key) {
|
|
366
425
|
let parsed;
|
|
367
426
|
|
|
368
427
|
try {
|
|
369
428
|
parsed = parseFieldExpression(key);
|
|
370
429
|
} catch (err) {
|
|
371
|
-
|
|
372
|
-
return [key];
|
|
430
|
+
return null;
|
|
373
431
|
}
|
|
374
432
|
|
|
433
|
+
const { column, access } = parsed;
|
|
375
434
|
// Numeric keys like the `0` in `tags:0` also access array elements when the
|
|
376
435
|
// json value is an array, so treat them like `[0]`.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
436
|
+
const isArrayAccess = access.some(({ type, ref }) => type !== 'object' || /^-?\d+$/.test(ref));
|
|
437
|
+
return {
|
|
438
|
+
column,
|
|
439
|
+
path: isArrayAccess ? null : [column, ...access.map(({ ref }) => String(ref))],
|
|
440
|
+
};
|
|
382
441
|
}
|
|
383
442
|
|
|
384
443
|
function setPath(obj, path, value, createdObjects) {
|
|
@@ -462,22 +521,20 @@ function objectHasDefaults(obj) {
|
|
|
462
521
|
return false;
|
|
463
522
|
}
|
|
464
523
|
|
|
465
|
-
// Returns the keys
|
|
466
|
-
// `
|
|
467
|
-
// `
|
|
468
|
-
//
|
|
469
|
-
|
|
524
|
+
// Returns the `keys` that are required by the schema but given outside of
|
|
525
|
+
// `json`, sorted, or `null` if there are none. Query properties (`raw()`,
|
|
526
|
+
// `val()`, `ref()`, subqueries...) are split from the json before validation,
|
|
527
|
+
// but they do provide a value for the property, and so do field expressions
|
|
528
|
+
// for their json columns.
|
|
529
|
+
function getRequiredKeysGivenOutside(json, jsonSchema, keys) {
|
|
470
530
|
const required = jsonSchema.required;
|
|
471
531
|
|
|
472
532
|
if (!Array.isArray(required) || required.length === 0) {
|
|
473
533
|
return null;
|
|
474
534
|
}
|
|
475
535
|
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
return keys.length > 0 ? keys.sort() : null;
|
|
536
|
+
const givenKeys = required.filter((key) => keys.includes(key) && !hasOwn(json, key));
|
|
537
|
+
return givenKeys.length > 0 ? givenKeys.sort() : null;
|
|
481
538
|
}
|
|
482
539
|
|
|
483
540
|
function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
|
|
@@ -8,9 +8,10 @@ const { ValidationErrorType } = require('../model/ValidationError');
|
|
|
8
8
|
const { isObject, isString, isFunction, last, flatten } = require('../utils/objectUtils');
|
|
9
9
|
const { RelationExpression, DuplicateRelationError } = require('./RelationExpression');
|
|
10
10
|
const { assertIdNotUndefined, assertHasIdColumn } = require('../utils/assert');
|
|
11
|
-
const {
|
|
11
|
+
const { warnOnce } = require('../utils/warnings');
|
|
12
|
+
const { isMsSql } = require('../utils/knexUtils');
|
|
12
13
|
|
|
13
|
-
const { Selection } = require('./operations/select/Selection');
|
|
14
|
+
const { Selection, isSameIdentifier } = require('./operations/select/Selection');
|
|
14
15
|
|
|
15
16
|
const { QueryBuilderContext } = require('./QueryBuilderContext');
|
|
16
17
|
const { QueryBuilderBase } = require('./QueryBuilderBase');
|
|
@@ -45,9 +46,10 @@ const { EagerOperation } = require('./operations/eager/EagerOperation');
|
|
|
45
46
|
const { RangeOperation } = require('./operations/RangeOperation');
|
|
46
47
|
const { FirstOperation } = require('./operations/FirstOperation');
|
|
47
48
|
const { NoneOperation } = require('./operations/NoneOperation');
|
|
49
|
+
const { LimitPerParentOperation } = require('./operations/LimitPerParentOperation');
|
|
48
50
|
const { FromOperation } = require('./operations/FromOperation');
|
|
49
51
|
const { KnexOperation } = require('./operations/KnexOperation');
|
|
50
|
-
const {
|
|
52
|
+
const { clearUnsupportedOrder } = require('./withoutOrder');
|
|
51
53
|
|
|
52
54
|
class QueryBuilder extends QueryBuilderBase {
|
|
53
55
|
static init(self, modelClass) {
|
|
@@ -56,6 +58,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
56
58
|
self._resultModelClass = null;
|
|
57
59
|
self._explicitRejectValue = null;
|
|
58
60
|
self._explicitResolveValue = null;
|
|
61
|
+
self._queryBatch = null;
|
|
59
62
|
self._modifiers = {};
|
|
60
63
|
|
|
61
64
|
self._allowedGraphExpression = null;
|
|
@@ -257,21 +260,29 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
257
260
|
// The `joinOperation` option only applies to the relations of this call,
|
|
258
261
|
// see `RelationJoiner`. All other options are shared by all calls.
|
|
259
262
|
const { joinOperation, ...graphOptions } = options;
|
|
263
|
+
const isJoined = eagerOp instanceof JoinEagerOperation;
|
|
260
264
|
|
|
261
265
|
eagerOp.expression = eagerOp.expression.merge(parsedExp);
|
|
262
266
|
eagerOp.graphOptions = { ...eagerOp.graphOptions, ...graphOptions };
|
|
263
267
|
|
|
264
268
|
if (joinOperation) {
|
|
265
|
-
if (
|
|
269
|
+
if (isJoined) {
|
|
266
270
|
eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
|
|
267
271
|
} else {
|
|
268
|
-
|
|
272
|
+
warnOnce(
|
|
269
273
|
'The joinOperation option only applies to withGraphJoined() and is ignored for the ' +
|
|
270
274
|
`fetched relations "${parsedExp.toString()}".`,
|
|
271
275
|
);
|
|
272
276
|
}
|
|
273
277
|
}
|
|
274
278
|
|
|
279
|
+
if (graphOptions.limitPerParent && isJoined) {
|
|
280
|
+
warnOnce(
|
|
281
|
+
'The limitPerParent option only applies to withGraphFetched() and is ignored for the ' +
|
|
282
|
+
`joined relations "${parsedExp.toString()}".`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
275
286
|
checkMixedEager(this);
|
|
276
287
|
checkEager(this);
|
|
277
288
|
return this;
|
|
@@ -387,7 +398,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
387
398
|
return this.everyOperation((op) => {
|
|
388
399
|
if (op.constructor === SelectOperation) {
|
|
389
400
|
// SelectOperations with zero selections are the ones that only have
|
|
390
|
-
// raw items or other non-trivial selections
|
|
401
|
+
// raw items or other non-trivial selections, or `distinctOn()`.
|
|
391
402
|
return (
|
|
392
403
|
op.selections.length > 0 &&
|
|
393
404
|
op.selections.every((select) => {
|
|
@@ -506,34 +517,38 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
506
517
|
|
|
507
518
|
if (joinEagerOp) {
|
|
508
519
|
// `withGraphJoined` needs the table metadata to build its selects.
|
|
509
|
-
await joinEagerOp.
|
|
520
|
+
await joinEagerOp.fetchTableMetadata(builder);
|
|
521
|
+
|
|
522
|
+
// To-many joins return one row per joined child, so the root models are
|
|
523
|
+
// counted by their distinct ids, see below.
|
|
524
|
+
if (joinEagerOp.joiner.hasToManyJoins(builder)) {
|
|
525
|
+
assertHasIdColumn(builder.modelClass());
|
|
526
|
+
}
|
|
510
527
|
}
|
|
511
528
|
|
|
529
|
+
const idColumns = builder.modelClass().getIdColumnArray();
|
|
530
|
+
|
|
512
531
|
// Also clear the order on the built knex query, since `orderBy` calls
|
|
513
532
|
// can be added at build time, e.g. by a relation's `modify`.
|
|
514
533
|
const buildQuery = (knexBuilder) => builder.toKnexQuery(knexBuilder).clearOrder();
|
|
515
534
|
|
|
516
535
|
const countQuery = knex.count('* as count').from((knexBuilder) => {
|
|
517
|
-
if (!joinEagerOp) {
|
|
536
|
+
if (!joinEagerOp || idColumns.length === 0) {
|
|
537
|
+
// Without ids, the joins are all to-one and return one row per root
|
|
538
|
+
// model.
|
|
518
539
|
buildQuery(knexBuilder);
|
|
540
|
+
} else if (refersToSelections(builder)) {
|
|
541
|
+
// `having`, and `groupBy` with an alias, may refer to aliased
|
|
542
|
+
// selections, so keep them and select the distinct ids from the
|
|
543
|
+
// result instead.
|
|
544
|
+
knexBuilder.distinct(idColumns).from((knexBuilder) => {
|
|
545
|
+
buildQuery(knexBuilder).as('inner');
|
|
546
|
+
});
|
|
519
547
|
} else {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (refersToSelections(builder)) {
|
|
525
|
-
// `having`, and `groupBy` with an alias, may refer to aliased
|
|
526
|
-
// selections, so keep them and select the distinct ids from the
|
|
527
|
-
// result instead.
|
|
528
|
-
knexBuilder.distinct(idColumns).from((knexBuilder) => {
|
|
529
|
-
buildQuery(knexBuilder).as('inner');
|
|
530
|
-
});
|
|
531
|
-
} else {
|
|
532
|
-
const tableRef = builder.tableRef();
|
|
533
|
-
buildQuery(knexBuilder)
|
|
534
|
-
.clearSelect()
|
|
535
|
-
.distinct(idColumns.map((col) => `${tableRef}.${col}`));
|
|
536
|
-
}
|
|
548
|
+
const tableRef = builder.tableRef();
|
|
549
|
+
buildQuery(knexBuilder)
|
|
550
|
+
.clearSelect()
|
|
551
|
+
.distinct(idColumns.map((col) => `${tableRef}.${col}`));
|
|
537
552
|
}
|
|
538
553
|
|
|
539
554
|
knexBuilder.as('temp');
|
|
@@ -585,7 +600,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
585
600
|
let selectionInstance = null;
|
|
586
601
|
|
|
587
602
|
this.forEachOperation(true, (op) => {
|
|
588
|
-
if (op.constructor === SelectOperation) {
|
|
603
|
+
if (op.constructor === SelectOperation && !op.isDistinctOn) {
|
|
589
604
|
selectionInstance = op.findSelection(this, selection);
|
|
590
605
|
noSelectStatements = false;
|
|
591
606
|
|
|
@@ -637,9 +652,9 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
637
652
|
} else {
|
|
638
653
|
if (foundSelection.column === '*') {
|
|
639
654
|
// * selects the columns with their column names as aliases.
|
|
640
|
-
return selection.column
|
|
655
|
+
return isSameIdentifier(this, selection.column, alias);
|
|
641
656
|
} else {
|
|
642
|
-
return foundSelection.name
|
|
657
|
+
return isSameIdentifier(this, foundSelection.name, alias);
|
|
643
658
|
}
|
|
644
659
|
}
|
|
645
660
|
}
|
|
@@ -1325,14 +1340,14 @@ function checkMixedEager(builder) {
|
|
|
1325
1340
|
|
|
1326
1341
|
if (joinNode.$allRecursive || fetchNode.$allRecursive) {
|
|
1327
1342
|
throw new Error(
|
|
1328
|
-
'relation expression
|
|
1343
|
+
'relation expression "*" cannot be used when mixing withGraphJoined and withGraphFetched',
|
|
1329
1344
|
);
|
|
1330
1345
|
}
|
|
1331
1346
|
|
|
1332
1347
|
for (const childName of joinNode.$childNames) {
|
|
1333
1348
|
if (fetchNode.$childNames.includes(childName)) {
|
|
1334
1349
|
throw new Error(
|
|
1335
|
-
`relation
|
|
1350
|
+
`relation "${childName}" cannot be loaded with both withGraphJoined and withGraphFetched`,
|
|
1336
1351
|
);
|
|
1337
1352
|
}
|
|
1338
1353
|
}
|
|
@@ -1382,10 +1397,13 @@ function beforeExecute(builder) {
|
|
|
1382
1397
|
return promise;
|
|
1383
1398
|
}
|
|
1384
1399
|
|
|
1385
|
-
function doExecute(builder) {
|
|
1400
|
+
async function doExecute(builder) {
|
|
1386
1401
|
let promise = Promise.resolve();
|
|
1387
1402
|
|
|
1388
1403
|
builder = callOnBuildHooks(builder);
|
|
1404
|
+
// Unlike `onBuild`, `onAfterBuild` can be async, as it only runs when the
|
|
1405
|
+
// query is executed, not when it's only built, e.g. with `toKnexQuery()`:
|
|
1406
|
+
await chainOperationHooks(promise, builder, 'onAfterBuild');
|
|
1389
1407
|
const queryExecutorOperation = findQueryExecutorOperation(builder);
|
|
1390
1408
|
|
|
1391
1409
|
const explicitRejectValue = builder._explicitRejectValue;
|
|
@@ -1398,7 +1416,7 @@ function doExecute(builder) {
|
|
|
1398
1416
|
} else if (queryExecutorOperation !== null) {
|
|
1399
1417
|
promise = Promise.resolve(queryExecutorOperation.queryExecutor(builder));
|
|
1400
1418
|
} else {
|
|
1401
|
-
promise =
|
|
1419
|
+
promise = executeKnexQuery(builder);
|
|
1402
1420
|
|
|
1403
1421
|
promise = chainOperationHooks(promise, builder, 'onRawResult');
|
|
1404
1422
|
promise = promise.then((result) => createModels(result, builder));
|
|
@@ -1407,6 +1425,30 @@ function doExecute(builder) {
|
|
|
1407
1425
|
return promise;
|
|
1408
1426
|
}
|
|
1409
1427
|
|
|
1428
|
+
// An operation can split the query into batches, e.g. to stay below the number
|
|
1429
|
+
// of values a database accepts in one statement. Build and execute the knex
|
|
1430
|
+
// query once per batch then, with the operation reading the current batch from
|
|
1431
|
+
// `builder._queryBatch`, and merge the counts or rows of all batches.
|
|
1432
|
+
async function executeKnexQuery(builder) {
|
|
1433
|
+
const operation = builder.findOperation((op) => op.hasQueryBatches());
|
|
1434
|
+
const batches = operation?.queryBatches(builder);
|
|
1435
|
+
|
|
1436
|
+
if (batches) {
|
|
1437
|
+
const results = [];
|
|
1438
|
+
|
|
1439
|
+
for (const batch of batches) {
|
|
1440
|
+
builder._queryBatch = batch;
|
|
1441
|
+
results.push(await buildKnexQuery(builder));
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
return results.reduce((merged, result) =>
|
|
1445
|
+
Array.isArray(merged) ? merged.concat(result) : merged + result,
|
|
1446
|
+
);
|
|
1447
|
+
} else {
|
|
1448
|
+
return buildKnexQuery(builder);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1410
1452
|
function afterExecute(builder, result) {
|
|
1411
1453
|
let promise = Promise.resolve(result);
|
|
1412
1454
|
|
|
@@ -1560,7 +1602,13 @@ function callOnBuildHooks(builder) {
|
|
|
1560
1602
|
callOnBuildFuncs(builder, builder.internalContext().onBuild);
|
|
1561
1603
|
|
|
1562
1604
|
builder.executeOnBuild();
|
|
1563
|
-
|
|
1605
|
+
|
|
1606
|
+
// Clear the order after all hooks, as any of them may add one.
|
|
1607
|
+
if (builder.isUpdate()) {
|
|
1608
|
+
clearUnsupportedOrder(builder);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
return builder;
|
|
1564
1612
|
}
|
|
1565
1613
|
|
|
1566
1614
|
function callOnBuildFuncs(builder, func) {
|
|
@@ -1595,6 +1643,12 @@ function buildKnexQuery(builder, knexBuilder = builder.knex().queryBuilder()) {
|
|
|
1595
1643
|
}
|
|
1596
1644
|
}
|
|
1597
1645
|
|
|
1646
|
+
const limitPerParentOperation = builder.findOperation(LimitPerParentOperation);
|
|
1647
|
+
|
|
1648
|
+
if (limitPerParentOperation) {
|
|
1649
|
+
knexBuilder = limitPerParentOperation.onAfterBuildKnex(knexBuilder, builder);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1598
1652
|
return knexBuilder;
|
|
1599
1653
|
}
|
|
1600
1654
|
|
|
@@ -1604,6 +1658,10 @@ function setDefaultTable(builder, knexBuilder) {
|
|
|
1604
1658
|
|
|
1605
1659
|
if (table === tableRef) {
|
|
1606
1660
|
return knexBuilder.table(table);
|
|
1661
|
+
} else if (isMsSql(builder.knex()) && (builder.isUpdate() || builder.isDelete())) {
|
|
1662
|
+
// SQL Server can't alias the table of updates and deletes, but it can modify
|
|
1663
|
+
// it through a common table expression named like the alias.
|
|
1664
|
+
return knexBuilder.with(tableRef, (query) => query.select('*').from(table)).table(tableRef);
|
|
1607
1665
|
} else {
|
|
1608
1666
|
return knexBuilder.table(`${table} as ${tableRef}`);
|
|
1609
1667
|
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
const { isString, isFunction, isRegExp, mergeMaps, last } = require('../utils/objectUtils');
|
|
4
4
|
const { QueryBuilderContextBase } = require('./QueryBuilderContextBase');
|
|
5
5
|
const { QueryBuilderUserContext } = require('./QueryBuilderUserContext');
|
|
6
|
-
const { deprecate } = require('../utils/
|
|
6
|
+
const { deprecate } = require('../utils/warnings');
|
|
7
|
+
const { getWhereGroupOperations } = require('./whereGroups');
|
|
7
8
|
|
|
8
9
|
const AllSelector = () => true;
|
|
9
10
|
const SelectSelector =
|
|
@@ -489,8 +490,19 @@ class QueryBuilderOperationSupport {
|
|
|
489
490
|
}
|
|
490
491
|
|
|
491
492
|
executeOnBuildKnex(knexBuilder) {
|
|
492
|
-
|
|
493
|
-
|
|
493
|
+
const whereGroupOps = getWhereGroupOperations(this);
|
|
494
|
+
|
|
495
|
+
// Visit the operations while building, so that operations added by
|
|
496
|
+
// `onBuildKnex` hooks are built too. Build each where group in place of its
|
|
497
|
+
// first operation, and skip the operations that the groups build.
|
|
498
|
+
this.forEachOperation(true, (it) => {
|
|
499
|
+
const op = whereGroupOps.has(it)
|
|
500
|
+
? whereGroupOps.get(it)
|
|
501
|
+
: it.isAncestorInSet(whereGroupOps)
|
|
502
|
+
? null
|
|
503
|
+
: it;
|
|
504
|
+
|
|
505
|
+
if (op?.hasOnBuildKnex()) {
|
|
494
506
|
const newKnexBuilder = this.callOperationMethod(op, 'onBuildKnex', [knexBuilder, this]);
|
|
495
507
|
// Default to the input knex builder for backwards compatibility
|
|
496
508
|
// with QueryBuilder.onBuildKnex hooks.
|
|
@@ -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
|
};
|
|
@@ -17,21 +17,6 @@ const FETCH_STRATEGY = 'fetchStrategy';
|
|
|
17
17
|
const ALLOW_REFS = 'allowRefs';
|
|
18
18
|
const ALLOW_MARKERS = 'allowMarkers';
|
|
19
19
|
|
|
20
|
-
const OPTION_NAMES = [
|
|
21
|
-
NO_RELATE,
|
|
22
|
-
NO_UNRELATE,
|
|
23
|
-
NO_INSERT,
|
|
24
|
-
NO_UPDATE,
|
|
25
|
-
NO_DELETE,
|
|
26
|
-
UPDATE,
|
|
27
|
-
RELATE,
|
|
28
|
-
UNRELATE,
|
|
29
|
-
INSERT_MISSING,
|
|
30
|
-
FETCH_STRATEGY,
|
|
31
|
-
ALLOW_REFS,
|
|
32
|
-
ALLOW_MARKERS,
|
|
33
|
-
];
|
|
34
|
-
|
|
35
20
|
// The options that are matched against the relation path of each node, see
|
|
36
21
|
// `_hasOption()`.
|
|
37
22
|
const NODE_OPTION_NAMES = [
|
|
@@ -46,6 +31,8 @@ const NODE_OPTION_NAMES = [
|
|
|
46
31
|
INSERT_MISSING,
|
|
47
32
|
];
|
|
48
33
|
|
|
34
|
+
const OPTION_NAMES = [...NODE_OPTION_NAMES, FETCH_STRATEGY, ALLOW_REFS, ALLOW_MARKERS];
|
|
35
|
+
|
|
49
36
|
const FetchStrategy = {
|
|
50
37
|
OnlyIdentifiers: 'OnlyIdentifiers',
|
|
51
38
|
Everything: 'Everything',
|
|
@@ -55,14 +42,17 @@ const FetchStrategy = {
|
|
|
55
42
|
class GraphOptions {
|
|
56
43
|
// `rootRelationPath` is the relation path of the root node of a recursive
|
|
57
44
|
// upsert in the original graph, against which the relation paths and
|
|
58
|
-
// expressions of the options match.
|
|
59
|
-
|
|
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) {
|
|
60
48
|
if (options instanceof GraphOptions) {
|
|
61
49
|
this.options = options.options;
|
|
62
50
|
this.rootRelationPath = options.rootRelationPath;
|
|
51
|
+
this.insertOnly = options.insertOnly;
|
|
63
52
|
} else {
|
|
64
53
|
this.options = parseRelationExpressions(options);
|
|
65
54
|
this.rootRelationPath = rootRelationPath;
|
|
55
|
+
this.insertOnly = insertOnly;
|
|
66
56
|
}
|
|
67
57
|
}
|
|
68
58
|
|
|
@@ -79,17 +69,12 @@ class GraphOptions {
|
|
|
79
69
|
}
|
|
80
70
|
|
|
81
71
|
isInsertOnly() {
|
|
82
|
-
|
|
83
|
-
// relate things that can be related using inserts.
|
|
84
|
-
// TODO: Use a special key for this.
|
|
85
|
-
return [NO_DELETE, NO_UPDATE, NO_UNRELATE, INSERT_MISSING].every((opt) => {
|
|
86
|
-
return this.options[opt] === true;
|
|
87
|
-
});
|
|
72
|
+
return this.insertOnly;
|
|
88
73
|
}
|
|
89
74
|
|
|
90
75
|
// Like `shouldRelate` but ignores settings that explicitly disable relate operations.
|
|
91
76
|
shouldRelateIgnoreDisable(node, graphData) {
|
|
92
|
-
if (node.
|
|
77
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
93
78
|
return false;
|
|
94
79
|
}
|
|
95
80
|
|
|
@@ -113,7 +98,7 @@ class GraphOptions {
|
|
|
113
98
|
|
|
114
99
|
// Like `shouldInsert` but ignores settings that explicitly disable insert operations.
|
|
115
100
|
shouldInsertIgnoreDisable(node, graphData) {
|
|
116
|
-
if (node.
|
|
101
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
117
102
|
return false;
|
|
118
103
|
}
|
|
119
104
|
|
|
@@ -135,7 +120,7 @@ class GraphOptions {
|
|
|
135
120
|
// Like `shouldPatch() || shouldUpdate()` but ignores settings that explicitly disable
|
|
136
121
|
// update or patch operations.
|
|
137
122
|
shouldPatchOrUpdateIgnoreDisable(node, graphData) {
|
|
138
|
-
if (node.
|
|
123
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
139
124
|
return false;
|
|
140
125
|
}
|
|
141
126
|
|