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
|
@@ -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);
|
|
122
|
+
|
|
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
|
+
}
|
|
100
134
|
|
|
101
|
-
|
|
102
|
-
const error = parseValidationError(validator.errors, modelClass, options, this.ajvOptions);
|
|
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
|
|
|
@@ -215,7 +249,22 @@ class AjvValidator extends Validator {
|
|
|
215
249
|
this.setupAjv(this.ajvQueryProps);
|
|
216
250
|
}
|
|
217
251
|
|
|
218
|
-
|
|
252
|
+
for (;;) {
|
|
253
|
+
try {
|
|
254
|
+
return this.ajvQueryProps.compile(jsonSchema);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
// The schemas of other model classes that `$ref`s point to aren't added
|
|
257
|
+
// to this instance. Copy them over from the normal instance when needed.
|
|
258
|
+
const validator =
|
|
259
|
+
err instanceof Ajv.MissingRefError && this.ajv.getSchema(err.missingSchema);
|
|
260
|
+
|
|
261
|
+
if (!validator) {
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.ajvQueryProps.addSchema(validator.schema, err.missingSchema);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
219
268
|
}
|
|
220
269
|
}
|
|
221
270
|
|
|
@@ -283,10 +332,11 @@ function parseValidationError(errors, modelClass, options, ajvOptions) {
|
|
|
283
332
|
});
|
|
284
333
|
}
|
|
285
334
|
|
|
286
|
-
function nestFieldExpressions(json) {
|
|
335
|
+
function nestFieldExpressions(json, queryPropKeys, jsonSchema) {
|
|
336
|
+
const isFieldExpression = (key) => isFieldExpressionKey(key, jsonSchema.properties);
|
|
287
337
|
const keys = Object.keys(json);
|
|
288
338
|
|
|
289
|
-
if (!keys.some(isFieldExpression)) {
|
|
339
|
+
if (!keys.some(isFieldExpression) && !queryPropKeys.some(isFieldExpression)) {
|
|
290
340
|
return null;
|
|
291
341
|
}
|
|
292
342
|
|
|
@@ -295,6 +345,15 @@ function nestFieldExpressions(json) {
|
|
|
295
345
|
// The objects created here for the nested paths. Other objects are values
|
|
296
346
|
// of the input json and must not be modified.
|
|
297
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
|
+
};
|
|
298
357
|
|
|
299
358
|
for (const key of keys) {
|
|
300
359
|
if (!isFieldExpression(key)) {
|
|
@@ -305,7 +364,10 @@ function nestFieldExpressions(json) {
|
|
|
305
364
|
|
|
306
365
|
for (const key of keys) {
|
|
307
366
|
if (isFieldExpression(key)) {
|
|
308
|
-
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);
|
|
309
371
|
|
|
310
372
|
// Field expressions with array access, and those that conflict with other
|
|
311
373
|
// keys of the patch, can't be validated without the current value of the
|
|
@@ -316,10 +378,16 @@ function nestFieldExpressions(json) {
|
|
|
316
378
|
}
|
|
317
379
|
}
|
|
318
380
|
|
|
319
|
-
|
|
381
|
+
for (const key of queryPropKeys) {
|
|
382
|
+
if (isFieldExpression(key)) {
|
|
383
|
+
addColumn(parseFieldExpressionKey(key));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { json: nested, paths, columns: [...columns] };
|
|
320
388
|
}
|
|
321
389
|
|
|
322
|
-
function unnestFieldExpressions(json,
|
|
390
|
+
function unnestFieldExpressions(json, nested, { paths, columns }) {
|
|
323
391
|
for (const [key, path] of paths) {
|
|
324
392
|
let value = nested;
|
|
325
393
|
let found = true;
|
|
@@ -341,27 +409,35 @@ function unnestFieldExpressions(json, { json: nested, paths }) {
|
|
|
341
409
|
json[key] = value;
|
|
342
410
|
}
|
|
343
411
|
}
|
|
344
|
-
}
|
|
345
412
|
|
|
346
|
-
|
|
347
|
-
|
|
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
|
+
}
|
|
348
420
|
}
|
|
349
421
|
|
|
350
|
-
|
|
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) {
|
|
351
425
|
let parsed;
|
|
352
426
|
|
|
353
427
|
try {
|
|
354
428
|
parsed = parseFieldExpression(key);
|
|
355
429
|
} catch (err) {
|
|
356
|
-
// Not a valid field expression, validate it as a top-level property.
|
|
357
|
-
return [key];
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (parsed.access.some((access) => access.type !== 'object')) {
|
|
361
430
|
return null;
|
|
362
431
|
}
|
|
363
432
|
|
|
364
|
-
|
|
433
|
+
const { column, access } = parsed;
|
|
434
|
+
// Numeric keys like the `0` in `tags:0` also access array elements when the
|
|
435
|
+
// json value is an array, so treat them like `[0]`.
|
|
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
|
+
};
|
|
365
441
|
}
|
|
366
442
|
|
|
367
443
|
function setPath(obj, path, value, createdObjects) {
|
|
@@ -445,22 +521,20 @@ function objectHasDefaults(obj) {
|
|
|
445
521
|
return false;
|
|
446
522
|
}
|
|
447
523
|
|
|
448
|
-
// Returns the keys
|
|
449
|
-
// `
|
|
450
|
-
// `
|
|
451
|
-
//
|
|
452
|
-
|
|
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) {
|
|
453
530
|
const required = jsonSchema.required;
|
|
454
531
|
|
|
455
532
|
if (!Array.isArray(required) || required.length === 0) {
|
|
456
533
|
return null;
|
|
457
534
|
}
|
|
458
535
|
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
);
|
|
462
|
-
|
|
463
|
-
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;
|
|
464
538
|
}
|
|
465
539
|
|
|
466
540
|
function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
|
|
@@ -470,6 +544,11 @@ function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
|
|
|
470
544
|
}
|
|
471
545
|
|
|
472
546
|
function jsonSchemaWithoutRequired(jsonSchema) {
|
|
547
|
+
// Boolean schemas (`true` / `false`) have no `required` to strip.
|
|
548
|
+
if (!isObject(jsonSchema)) {
|
|
549
|
+
return jsonSchema;
|
|
550
|
+
}
|
|
551
|
+
|
|
473
552
|
// `not` is intentionally left untouched: Stripping `required` inside it
|
|
474
553
|
// would turn it into a schema that rejects everything (e.g.
|
|
475
554
|
// `not: { required: ['a'] }` -> `not: {}`). Kept as is, it still holds for
|
|
@@ -506,17 +585,18 @@ function definitionsWithoutRequired(jsonSchema, prop) {
|
|
|
506
585
|
}
|
|
507
586
|
|
|
508
587
|
function subSchemaWithoutRequired(jsonSchema, prop) {
|
|
509
|
-
if (jsonSchema[prop]) {
|
|
588
|
+
if (jsonSchema[prop] !== undefined) {
|
|
510
589
|
if (Array.isArray(jsonSchema[prop])) {
|
|
511
590
|
const schemaArray = jsonSchema[prop].map(jsonSchemaWithoutRequired);
|
|
512
|
-
const nonEmptySchemaArray = schemaArray.filter(
|
|
591
|
+
const nonEmptySchemaArray = schemaArray.filter((schema) => !matchesAnything(schema));
|
|
513
592
|
|
|
514
593
|
// An `anyOf` or `oneOf` option that only consisted of `required` is
|
|
515
594
|
// empty once `required` is stripped, and an empty schema matches any
|
|
516
|
-
// patch, just like the stripped `required` keywords elsewhere
|
|
517
|
-
// whole keyword then: requiring the patch to
|
|
518
|
-
// options would reject valid patches, and
|
|
519
|
-
// option would fail every patch that also
|
|
595
|
+
// patch, just like the stripped `required` keywords elsewhere and the
|
|
596
|
+
// `true` schema. Drop the whole keyword then: requiring the patch to
|
|
597
|
+
// match one of the remaining options would reject valid patches, and
|
|
598
|
+
// with `oneOf` keeping the empty option would fail every patch that also
|
|
599
|
+
// matches another option.
|
|
520
600
|
if (prop !== 'allOf' && nonEmptySchemaArray.length !== schemaArray.length) {
|
|
521
601
|
return {};
|
|
522
602
|
} else if (nonEmptySchemaArray.length !== 0) {
|
|
@@ -548,8 +628,8 @@ function subSchemaWithoutRequired(jsonSchema, prop) {
|
|
|
548
628
|
}
|
|
549
629
|
}
|
|
550
630
|
|
|
551
|
-
function
|
|
552
|
-
return Object.keys(
|
|
631
|
+
function matchesAnything(schema) {
|
|
632
|
+
return schema === true || (isObject(schema) && Object.keys(schema).length === 0);
|
|
553
633
|
}
|
|
554
634
|
|
|
555
635
|
module.exports = {
|
package/lib/model/modelClone.js
CHANGED
|
@@ -8,8 +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 { warnOnce } = require('../utils/warnings');
|
|
12
|
+
const { isMsSql } = require('../utils/knexUtils');
|
|
11
13
|
|
|
12
|
-
const { Selection } = require('./operations/select/Selection');
|
|
14
|
+
const { Selection, isSameIdentifier } = require('./operations/select/Selection');
|
|
13
15
|
|
|
14
16
|
const { QueryBuilderContext } = require('./QueryBuilderContext');
|
|
15
17
|
const { QueryBuilderBase } = require('./QueryBuilderBase');
|
|
@@ -44,9 +46,10 @@ const { EagerOperation } = require('./operations/eager/EagerOperation');
|
|
|
44
46
|
const { RangeOperation } = require('./operations/RangeOperation');
|
|
45
47
|
const { FirstOperation } = require('./operations/FirstOperation');
|
|
46
48
|
const { NoneOperation } = require('./operations/NoneOperation');
|
|
49
|
+
const { LimitPerParentOperation } = require('./operations/LimitPerParentOperation');
|
|
47
50
|
const { FromOperation } = require('./operations/FromOperation');
|
|
48
51
|
const { KnexOperation } = require('./operations/KnexOperation');
|
|
49
|
-
const {
|
|
52
|
+
const { clearUnsupportedOrder } = require('./withoutOrder');
|
|
50
53
|
|
|
51
54
|
class QueryBuilder extends QueryBuilderBase {
|
|
52
55
|
static init(self, modelClass) {
|
|
@@ -55,6 +58,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
55
58
|
self._resultModelClass = null;
|
|
56
59
|
self._explicitRejectValue = null;
|
|
57
60
|
self._explicitResolveValue = null;
|
|
61
|
+
self._queryBatch = null;
|
|
58
62
|
self._modifiers = {};
|
|
59
63
|
|
|
60
64
|
self._allowedGraphExpression = null;
|
|
@@ -176,7 +180,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
isExplicitlyResolvedOrRejected() {
|
|
179
|
-
return
|
|
183
|
+
return this._explicitRejectValue !== null || this._explicitResolveValue !== null;
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
isExecutable() {
|
|
@@ -256,12 +260,27 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
256
260
|
// The `joinOperation` option only applies to the relations of this call,
|
|
257
261
|
// see `RelationJoiner`. All other options are shared by all calls.
|
|
258
262
|
const { joinOperation, ...graphOptions } = options;
|
|
263
|
+
const isJoined = eagerOp instanceof JoinEagerOperation;
|
|
259
264
|
|
|
260
265
|
eagerOp.expression = eagerOp.expression.merge(parsedExp);
|
|
261
266
|
eagerOp.graphOptions = { ...eagerOp.graphOptions, ...graphOptions };
|
|
262
267
|
|
|
263
268
|
if (joinOperation) {
|
|
264
|
-
|
|
269
|
+
if (isJoined) {
|
|
270
|
+
eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
|
|
271
|
+
} else {
|
|
272
|
+
warnOnce(
|
|
273
|
+
'The joinOperation option only applies to withGraphJoined() and is ignored for the ' +
|
|
274
|
+
`fetched relations "${parsedExp.toString()}".`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
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
|
+
);
|
|
265
284
|
}
|
|
266
285
|
|
|
267
286
|
checkMixedEager(this);
|
|
@@ -379,7 +398,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
379
398
|
return this.everyOperation((op) => {
|
|
380
399
|
if (op.constructor === SelectOperation) {
|
|
381
400
|
// SelectOperations with zero selections are the ones that only have
|
|
382
|
-
// raw items or other non-trivial selections
|
|
401
|
+
// raw items or other non-trivial selections, or `distinctOn()`.
|
|
383
402
|
return (
|
|
384
403
|
op.selections.length > 0 &&
|
|
385
404
|
op.selections.every((select) => {
|
|
@@ -498,20 +517,38 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
498
517
|
|
|
499
518
|
if (joinEagerOp) {
|
|
500
519
|
// `withGraphJoined` needs the table metadata to build its selects.
|
|
501
|
-
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
|
+
}
|
|
502
527
|
}
|
|
503
528
|
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
529
|
+
const idColumns = builder.modelClass().getIdColumnArray();
|
|
530
|
+
|
|
531
|
+
// Also clear the order on the built knex query, since `orderBy` calls
|
|
532
|
+
// can be added at build time, e.g. by a relation's `modify`.
|
|
533
|
+
const buildQuery = (knexBuilder) => builder.toKnexQuery(knexBuilder).clearOrder();
|
|
508
534
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
//
|
|
535
|
+
const countQuery = knex.count('* as count').from((knexBuilder) => {
|
|
536
|
+
if (!joinEagerOp || idColumns.length === 0) {
|
|
537
|
+
// Without ids, the joins are all to-one and return one row per root
|
|
538
|
+
// model.
|
|
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
|
+
});
|
|
547
|
+
} else {
|
|
512
548
|
const tableRef = builder.tableRef();
|
|
513
|
-
|
|
514
|
-
|
|
549
|
+
buildQuery(knexBuilder)
|
|
550
|
+
.clearSelect()
|
|
551
|
+
.distinct(idColumns.map((col) => `${tableRef}.${col}`));
|
|
515
552
|
}
|
|
516
553
|
|
|
517
554
|
knexBuilder.as('temp');
|
|
@@ -563,7 +600,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
563
600
|
let selectionInstance = null;
|
|
564
601
|
|
|
565
602
|
this.forEachOperation(true, (op) => {
|
|
566
|
-
if (op.constructor === SelectOperation) {
|
|
603
|
+
if (op.constructor === SelectOperation && !op.isDistinctOn) {
|
|
567
604
|
selectionInstance = op.findSelection(this, selection);
|
|
568
605
|
noSelectStatements = false;
|
|
569
606
|
|
|
@@ -615,9 +652,9 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
615
652
|
} else {
|
|
616
653
|
if (foundSelection.column === '*') {
|
|
617
654
|
// * selects the columns with their column names as aliases.
|
|
618
|
-
return selection.column
|
|
655
|
+
return isSameIdentifier(this, selection.column, alias);
|
|
619
656
|
} else {
|
|
620
|
-
return foundSelection.name
|
|
657
|
+
return isSameIdentifier(this, foundSelection.name, alias);
|
|
621
658
|
}
|
|
622
659
|
}
|
|
623
660
|
}
|
|
@@ -1171,6 +1208,29 @@ function getEagerAlgorithmByName(builder, name) {
|
|
|
1171
1208
|
}
|
|
1172
1209
|
}
|
|
1173
1210
|
|
|
1211
|
+
// Returns true if the `having` or `groupBy` clauses of `builder` may refer to
|
|
1212
|
+
// aliased selections (#2329).
|
|
1213
|
+
function refersToSelections(builder) {
|
|
1214
|
+
if (builder.has(/having/)) {
|
|
1215
|
+
return true;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const aliases = builder
|
|
1219
|
+
.findAllSelections()
|
|
1220
|
+
.map((selection) => selection.alias)
|
|
1221
|
+
.filter((alias) => !!alias);
|
|
1222
|
+
|
|
1223
|
+
let refersToAlias = false;
|
|
1224
|
+
|
|
1225
|
+
builder.forEachOperation(/^groupBy$/, (op) => {
|
|
1226
|
+
if (flatten(op.args).some((arg) => aliases.includes(arg))) {
|
|
1227
|
+
refersToAlias = true;
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
return refersToAlias;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1174
1234
|
function getEagerAlgorithmOfOperation(builder, eagerOp) {
|
|
1175
1235
|
if (eagerOp instanceof JoinEagerOperation) {
|
|
1176
1236
|
return getJoinEagerAlgorithm(builder);
|
|
@@ -1198,7 +1258,8 @@ function groupByEagerAlgorithm(builder, expression) {
|
|
|
1198
1258
|
|
|
1199
1259
|
for (const childName of node.$childNames) {
|
|
1200
1260
|
const eagerOp = findEagerOperations(builder).find(
|
|
1201
|
-
(op) =>
|
|
1261
|
+
(op) =>
|
|
1262
|
+
op.expression.node.$allRecursive || op.expression.node.$childNames.includes(childName),
|
|
1202
1263
|
);
|
|
1203
1264
|
|
|
1204
1265
|
const algorithm = eagerOp ? getEagerAlgorithmOfOperation(builder, eagerOp) : defaultAlgorithm;
|
|
@@ -1279,14 +1340,14 @@ function checkMixedEager(builder) {
|
|
|
1279
1340
|
|
|
1280
1341
|
if (joinNode.$allRecursive || fetchNode.$allRecursive) {
|
|
1281
1342
|
throw new Error(
|
|
1282
|
-
'relation expression
|
|
1343
|
+
'relation expression "*" cannot be used when mixing withGraphJoined and withGraphFetched',
|
|
1283
1344
|
);
|
|
1284
1345
|
}
|
|
1285
1346
|
|
|
1286
1347
|
for (const childName of joinNode.$childNames) {
|
|
1287
1348
|
if (fetchNode.$childNames.includes(childName)) {
|
|
1288
1349
|
throw new Error(
|
|
1289
|
-
`relation
|
|
1350
|
+
`relation "${childName}" cannot be loaded with both withGraphJoined and withGraphFetched`,
|
|
1290
1351
|
);
|
|
1291
1352
|
}
|
|
1292
1353
|
}
|
|
@@ -1336,10 +1397,13 @@ function beforeExecute(builder) {
|
|
|
1336
1397
|
return promise;
|
|
1337
1398
|
}
|
|
1338
1399
|
|
|
1339
|
-
function doExecute(builder) {
|
|
1400
|
+
async function doExecute(builder) {
|
|
1340
1401
|
let promise = Promise.resolve();
|
|
1341
1402
|
|
|
1342
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');
|
|
1343
1407
|
const queryExecutorOperation = findQueryExecutorOperation(builder);
|
|
1344
1408
|
|
|
1345
1409
|
const explicitRejectValue = builder._explicitRejectValue;
|
|
@@ -1352,7 +1416,7 @@ function doExecute(builder) {
|
|
|
1352
1416
|
} else if (queryExecutorOperation !== null) {
|
|
1353
1417
|
promise = Promise.resolve(queryExecutorOperation.queryExecutor(builder));
|
|
1354
1418
|
} else {
|
|
1355
|
-
promise =
|
|
1419
|
+
promise = executeKnexQuery(builder);
|
|
1356
1420
|
|
|
1357
1421
|
promise = chainOperationHooks(promise, builder, 'onRawResult');
|
|
1358
1422
|
promise = promise.then((result) => createModels(result, builder));
|
|
@@ -1361,6 +1425,30 @@ function doExecute(builder) {
|
|
|
1361
1425
|
return promise;
|
|
1362
1426
|
}
|
|
1363
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
|
+
|
|
1364
1452
|
function afterExecute(builder, result) {
|
|
1365
1453
|
let promise = Promise.resolve(result);
|
|
1366
1454
|
|
|
@@ -1514,7 +1602,13 @@ function callOnBuildHooks(builder) {
|
|
|
1514
1602
|
callOnBuildFuncs(builder, builder.internalContext().onBuild);
|
|
1515
1603
|
|
|
1516
1604
|
builder.executeOnBuild();
|
|
1517
|
-
|
|
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;
|
|
1518
1612
|
}
|
|
1519
1613
|
|
|
1520
1614
|
function callOnBuildFuncs(builder, func) {
|
|
@@ -1549,6 +1643,12 @@ function buildKnexQuery(builder, knexBuilder = builder.knex().queryBuilder()) {
|
|
|
1549
1643
|
}
|
|
1550
1644
|
}
|
|
1551
1645
|
|
|
1646
|
+
const limitPerParentOperation = builder.findOperation(LimitPerParentOperation);
|
|
1647
|
+
|
|
1648
|
+
if (limitPerParentOperation) {
|
|
1649
|
+
knexBuilder = limitPerParentOperation.onAfterBuildKnex(knexBuilder, builder);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1552
1652
|
return knexBuilder;
|
|
1553
1653
|
}
|
|
1554
1654
|
|
|
@@ -1558,6 +1658,10 @@ function setDefaultTable(builder, knexBuilder) {
|
|
|
1558
1658
|
|
|
1559
1659
|
if (table === tableRef) {
|
|
1560
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);
|
|
1561
1665
|
} else {
|
|
1562
1666
|
return knexBuilder.table(`${table} as ${tableRef}`);
|
|
1563
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.
|
|
@@ -129,7 +129,8 @@ class RelationExpression {
|
|
|
129
129
|
return new RelationExpression(this.node, this.recursionDepth + 1);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
// Don't mistake inherited properties like `constructor` for children.
|
|
133
|
+
const child = this.node.$childNames.includes(childName) ? this.node[childName] : null;
|
|
133
134
|
|
|
134
135
|
if (child) {
|
|
135
136
|
return new RelationExpression(child);
|
|
@@ -292,7 +293,7 @@ function modelToNode(model, node) {
|
|
|
292
293
|
const relName = relationNames[r];
|
|
293
294
|
|
|
294
295
|
if (model[relName] !== undefined) {
|
|
295
|
-
let childNode = node[relName];
|
|
296
|
+
let childNode = node.$childNames.includes(relName) ? node[relName] : null;
|
|
296
297
|
|
|
297
298
|
if (!childNode) {
|
|
298
299
|
childNode = newNode(relName);
|
|
@@ -402,8 +403,9 @@ function mergeNodes(node1, node2) {
|
|
|
402
403
|
node.$childNames = union(node1.$childNames, node2.$childNames);
|
|
403
404
|
|
|
404
405
|
for (const childName of node.$childNames) {
|
|
405
|
-
|
|
406
|
-
const
|
|
406
|
+
// Don't mistake inherited properties like `constructor` for children.
|
|
407
|
+
const child1 = node1.$childNames.includes(childName) ? node1[childName] : null;
|
|
408
|
+
const child2 = node2.$childNames.includes(childName) ? node2[childName] : null;
|
|
407
409
|
|
|
408
410
|
if (child1 && child2) {
|
|
409
411
|
node[childName] = mergeNodes(child1, child2);
|