objection 3.5.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/lib/model/AjvValidator.js +33 -10
- package/lib/model/modelClone.js +0 -2
- package/lib/queryBuilder/InternalOptions.js +2 -0
- package/lib/queryBuilder/QueryBuilder.js +56 -10
- package/lib/queryBuilder/RelationExpression.js +6 -4
- package/lib/queryBuilder/graph/GraphOptions.js +141 -22
- package/lib/queryBuilder/graph/GraphUpsert.js +57 -0
- package/lib/queryBuilder/graph/insert/GraphInsert.js +2 -0
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +3 -0
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +6 -1
- package/lib/queryBuilder/operations/UpdateOperation.js +11 -0
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +3 -1
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +4 -9
- package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +434 -364
- package/lib/queryBuilder/parsers/relationExpressionParser.js +513 -548
- package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +1 -1
- package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +4 -7
- package/lib/relations/Relation.js +7 -0
- package/lib/relations/RelationFindOperation.js +27 -40
- package/lib/relations/belongsToOne/BelongsToOneDeleteOperation.js +4 -2
- package/lib/relations/belongsToOne/BelongsToOneRelation.js +9 -0
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +41 -13
- package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +31 -42
- package/lib/relations/manyToMany/ManyToManyRelation.js +15 -18
- package/lib/relations/manyToMany/ManyToManySqliteModifyMixin.js +0 -13
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +2 -3
- 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/ManyToManyUpdateOperationBase.js +28 -2
- package/lib/utils/identifierMapping.js +8 -17
- package/lib/utils/objectUtils.js +109 -2
- package/lib/utils/parseFieldExpression.js +10 -3
- package/package.json +5 -2
- package/typings/objection/index.d.ts +56 -36
- 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/clone.js +0 -2329
package/README.md
CHANGED
|
@@ -36,9 +36,9 @@ What objection.js **doesn't** give you:
|
|
|
36
36
|
For simple things it is useful that the database schema is automatically generated from the model definitions,
|
|
37
37
|
but usually just gets in your way when doing anything non-trivial. Objection.js leaves the schema related things
|
|
38
38
|
to you. knex has a great [migration tool](https://knexjs.org/guide/migrations.html) that we recommend for this job. Check
|
|
39
|
-
out the [example project](https://github.com/
|
|
39
|
+
out the [example project](https://github.com/ditojs/objection/tree/main/examples/koa-ts).
|
|
40
40
|
|
|
41
|
-
The best way to get started is to clone our [example project](https://github.com/
|
|
41
|
+
The best way to get started is to clone our [example project](https://github.com/ditojs/objection/tree/main/examples/koa) and start playing with it. There's also a [typescript version](https://github.com/ditojs/objection/tree/main/examples/koa-ts) available.
|
|
42
42
|
|
|
43
43
|
Check out [this issue](https://github.com/Vincit/objection.js/issues/1069) to see who is using objection and what they think about it.
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ Shortcuts:
|
|
|
46
46
|
|
|
47
47
|
- [Who uses objection.js](https://github.com/Vincit/objection.js/discussions/2464)
|
|
48
48
|
- [API reference](https://ditojs.github.io/objection/api/query-builder/)
|
|
49
|
-
- [Example projects](https://github.com/
|
|
49
|
+
- [Example projects](https://github.com/ditojs/objection/tree/main/examples)
|
|
50
50
|
- [Changelog](https://ditojs.github.io/objection/release-notes/changelog.html)
|
|
51
51
|
- [v1 -> v2 -> v3 migration guide](https://ditojs.github.io/objection/release-notes/migration.html)
|
|
52
52
|
- [Contribution guide](https://ditojs.github.io/objection/guide/contributing.html)
|
|
@@ -215,7 +215,22 @@ class AjvValidator extends Validator {
|
|
|
215
215
|
this.setupAjv(this.ajvQueryProps);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
for (;;) {
|
|
219
|
+
try {
|
|
220
|
+
return this.ajvQueryProps.compile(jsonSchema);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
// The schemas of other model classes that `$ref`s point to aren't added
|
|
223
|
+
// to this instance. Copy them over from the normal instance when needed.
|
|
224
|
+
const validator =
|
|
225
|
+
err instanceof Ajv.MissingRefError && this.ajv.getSchema(err.missingSchema);
|
|
226
|
+
|
|
227
|
+
if (!validator) {
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.ajvQueryProps.addSchema(validator.schema, err.missingSchema);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
219
234
|
}
|
|
220
235
|
}
|
|
221
236
|
|
|
@@ -357,7 +372,9 @@ function fieldExpressionPath(key) {
|
|
|
357
372
|
return [key];
|
|
358
373
|
}
|
|
359
374
|
|
|
360
|
-
|
|
375
|
+
// Numeric keys like the `0` in `tags:0` also access array elements when the
|
|
376
|
+
// json value is an array, so treat them like `[0]`.
|
|
377
|
+
if (parsed.access.some((access) => access.type !== 'object' || /^-?\d+$/.test(access.ref))) {
|
|
361
378
|
return null;
|
|
362
379
|
}
|
|
363
380
|
|
|
@@ -470,6 +487,11 @@ function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
|
|
|
470
487
|
}
|
|
471
488
|
|
|
472
489
|
function jsonSchemaWithoutRequired(jsonSchema) {
|
|
490
|
+
// Boolean schemas (`true` / `false`) have no `required` to strip.
|
|
491
|
+
if (!isObject(jsonSchema)) {
|
|
492
|
+
return jsonSchema;
|
|
493
|
+
}
|
|
494
|
+
|
|
473
495
|
// `not` is intentionally left untouched: Stripping `required` inside it
|
|
474
496
|
// would turn it into a schema that rejects everything (e.g.
|
|
475
497
|
// `not: { required: ['a'] }` -> `not: {}`). Kept as is, it still holds for
|
|
@@ -506,17 +528,18 @@ function definitionsWithoutRequired(jsonSchema, prop) {
|
|
|
506
528
|
}
|
|
507
529
|
|
|
508
530
|
function subSchemaWithoutRequired(jsonSchema, prop) {
|
|
509
|
-
if (jsonSchema[prop]) {
|
|
531
|
+
if (jsonSchema[prop] !== undefined) {
|
|
510
532
|
if (Array.isArray(jsonSchema[prop])) {
|
|
511
533
|
const schemaArray = jsonSchema[prop].map(jsonSchemaWithoutRequired);
|
|
512
|
-
const nonEmptySchemaArray = schemaArray.filter(
|
|
534
|
+
const nonEmptySchemaArray = schemaArray.filter((schema) => !matchesAnything(schema));
|
|
513
535
|
|
|
514
536
|
// An `anyOf` or `oneOf` option that only consisted of `required` is
|
|
515
537
|
// 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
|
|
538
|
+
// patch, just like the stripped `required` keywords elsewhere and the
|
|
539
|
+
// `true` schema. Drop the whole keyword then: requiring the patch to
|
|
540
|
+
// match one of the remaining options would reject valid patches, and
|
|
541
|
+
// with `oneOf` keeping the empty option would fail every patch that also
|
|
542
|
+
// matches another option.
|
|
520
543
|
if (prop !== 'allOf' && nonEmptySchemaArray.length !== schemaArray.length) {
|
|
521
544
|
return {};
|
|
522
545
|
} else if (nonEmptySchemaArray.length !== 0) {
|
|
@@ -548,8 +571,8 @@ function subSchemaWithoutRequired(jsonSchema, prop) {
|
|
|
548
571
|
}
|
|
549
572
|
}
|
|
550
573
|
|
|
551
|
-
function
|
|
552
|
-
return Object.keys(
|
|
574
|
+
function matchesAnything(schema) {
|
|
575
|
+
return schema === true || (isObject(schema) && Object.keys(schema).length === 0);
|
|
553
576
|
}
|
|
554
577
|
|
|
555
578
|
module.exports = {
|
package/lib/model/modelClone.js
CHANGED
|
@@ -6,6 +6,7 @@ class InternalOptions {
|
|
|
6
6
|
this.keepImplicitJoinProps = false;
|
|
7
7
|
this.returnImmediatelyValue = undefined;
|
|
8
8
|
this.isInternalQuery = false;
|
|
9
|
+
this.skipRelationFilter = false;
|
|
9
10
|
this.debug = false;
|
|
10
11
|
this.schema = undefined;
|
|
11
12
|
}
|
|
@@ -17,6 +18,7 @@ class InternalOptions {
|
|
|
17
18
|
copy.keepImplicitJoinProps = this.keepImplicitJoinProps;
|
|
18
19
|
copy.returnImmediatelyValue = this.returnImmediatelyValue;
|
|
19
20
|
copy.isInternalQuery = this.isInternalQuery;
|
|
21
|
+
copy.skipRelationFilter = this.skipRelationFilter;
|
|
20
22
|
copy.debug = this.debug;
|
|
21
23
|
copy.schema = this.schema;
|
|
22
24
|
|
|
@@ -8,6 +8,7 @@ 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 { deprecate } = require('../utils/deprecate');
|
|
11
12
|
|
|
12
13
|
const { Selection } = require('./operations/select/Selection');
|
|
13
14
|
|
|
@@ -176,7 +177,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
isExplicitlyResolvedOrRejected() {
|
|
179
|
-
return
|
|
180
|
+
return this._explicitRejectValue !== null || this._explicitResolveValue !== null;
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
isExecutable() {
|
|
@@ -261,7 +262,14 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
261
262
|
eagerOp.graphOptions = { ...eagerOp.graphOptions, ...graphOptions };
|
|
262
263
|
|
|
263
264
|
if (joinOperation) {
|
|
264
|
-
eagerOp
|
|
265
|
+
if (eagerOp instanceof JoinEagerOperation) {
|
|
266
|
+
eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
|
|
267
|
+
} else {
|
|
268
|
+
deprecate(
|
|
269
|
+
'The joinOperation option only applies to withGraphJoined() and is ignored for the ' +
|
|
270
|
+
`fetched relations "${parsedExp.toString()}".`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
265
273
|
}
|
|
266
274
|
|
|
267
275
|
checkMixedEager(this);
|
|
@@ -501,17 +509,31 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
501
509
|
await joinEagerOp.onBefore3(builder);
|
|
502
510
|
}
|
|
503
511
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
knexBuilder = builder.toKnexQuery(knexBuilder).clearOrder();
|
|
512
|
+
// Also clear the order on the built knex query, since `orderBy` calls
|
|
513
|
+
// can be added at build time, e.g. by a relation's `modify`.
|
|
514
|
+
const buildQuery = (knexBuilder) => builder.toKnexQuery(knexBuilder).clearOrder();
|
|
508
515
|
|
|
509
|
-
|
|
516
|
+
const countQuery = knex.count('* as count').from((knexBuilder) => {
|
|
517
|
+
if (!joinEagerOp) {
|
|
518
|
+
buildQuery(knexBuilder);
|
|
519
|
+
} else {
|
|
510
520
|
// The joins of `withGraphJoined` return one row per joined child.
|
|
511
521
|
// Count the distinct root models instead.
|
|
512
|
-
const tableRef = builder.tableRef();
|
|
513
522
|
const idColumns = builder.modelClass().getIdColumnArray();
|
|
514
|
-
|
|
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
|
+
}
|
|
515
537
|
}
|
|
516
538
|
|
|
517
539
|
knexBuilder.as('temp');
|
|
@@ -1171,6 +1193,29 @@ function getEagerAlgorithmByName(builder, name) {
|
|
|
1171
1193
|
}
|
|
1172
1194
|
}
|
|
1173
1195
|
|
|
1196
|
+
// Returns true if the `having` or `groupBy` clauses of `builder` may refer to
|
|
1197
|
+
// aliased selections (#2329).
|
|
1198
|
+
function refersToSelections(builder) {
|
|
1199
|
+
if (builder.has(/having/)) {
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const aliases = builder
|
|
1204
|
+
.findAllSelections()
|
|
1205
|
+
.map((selection) => selection.alias)
|
|
1206
|
+
.filter((alias) => !!alias);
|
|
1207
|
+
|
|
1208
|
+
let refersToAlias = false;
|
|
1209
|
+
|
|
1210
|
+
builder.forEachOperation(/^groupBy$/, (op) => {
|
|
1211
|
+
if (flatten(op.args).some((arg) => aliases.includes(arg))) {
|
|
1212
|
+
refersToAlias = true;
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
return refersToAlias;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1174
1219
|
function getEagerAlgorithmOfOperation(builder, eagerOp) {
|
|
1175
1220
|
if (eagerOp instanceof JoinEagerOperation) {
|
|
1176
1221
|
return getJoinEagerAlgorithm(builder);
|
|
@@ -1198,7 +1243,8 @@ function groupByEagerAlgorithm(builder, expression) {
|
|
|
1198
1243
|
|
|
1199
1244
|
for (const childName of node.$childNames) {
|
|
1200
1245
|
const eagerOp = findEagerOperations(builder).find(
|
|
1201
|
-
(op) =>
|
|
1246
|
+
(op) =>
|
|
1247
|
+
op.expression.node.$allRecursive || op.expression.node.$childNames.includes(childName),
|
|
1202
1248
|
);
|
|
1203
1249
|
|
|
1204
1250
|
const algorithm = eagerOp ? getEagerAlgorithmOfOperation(builder, eagerOp) : defaultAlgorithm;
|
|
@@ -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);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { RelationExpression } = require('../RelationExpression');
|
|
4
|
+
const { isString } = require('../../utils/objectUtils');
|
|
5
|
+
|
|
3
6
|
const NO_RELATE = 'noRelate';
|
|
4
7
|
const NO_UNRELATE = 'noUnrelate';
|
|
5
8
|
const NO_INSERT = 'noInsert';
|
|
@@ -12,6 +15,7 @@ const UNRELATE = 'unrelate';
|
|
|
12
15
|
const INSERT_MISSING = 'insertMissing';
|
|
13
16
|
const FETCH_STRATEGY = 'fetchStrategy';
|
|
14
17
|
const ALLOW_REFS = 'allowRefs';
|
|
18
|
+
const ALLOW_MARKERS = 'allowMarkers';
|
|
15
19
|
|
|
16
20
|
const OPTION_NAMES = [
|
|
17
21
|
NO_RELATE,
|
|
@@ -25,6 +29,21 @@ const OPTION_NAMES = [
|
|
|
25
29
|
INSERT_MISSING,
|
|
26
30
|
FETCH_STRATEGY,
|
|
27
31
|
ALLOW_REFS,
|
|
32
|
+
ALLOW_MARKERS,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// The options that are matched against the relation path of each node, see
|
|
36
|
+
// `_hasOption()`.
|
|
37
|
+
const NODE_OPTION_NAMES = [
|
|
38
|
+
NO_RELATE,
|
|
39
|
+
NO_UNRELATE,
|
|
40
|
+
NO_INSERT,
|
|
41
|
+
NO_UPDATE,
|
|
42
|
+
NO_DELETE,
|
|
43
|
+
UPDATE,
|
|
44
|
+
RELATE,
|
|
45
|
+
UNRELATE,
|
|
46
|
+
INSERT_MISSING,
|
|
28
47
|
];
|
|
29
48
|
|
|
30
49
|
const FetchStrategy = {
|
|
@@ -34,11 +53,16 @@ const FetchStrategy = {
|
|
|
34
53
|
};
|
|
35
54
|
|
|
36
55
|
class GraphOptions {
|
|
37
|
-
|
|
56
|
+
// `rootRelationPath` is the relation path of the root node of a recursive
|
|
57
|
+
// upsert in the original graph, against which the relation paths and
|
|
58
|
+
// expressions of the options match.
|
|
59
|
+
constructor(options, rootRelationPath = []) {
|
|
38
60
|
if (options instanceof GraphOptions) {
|
|
39
61
|
this.options = options.options;
|
|
62
|
+
this.rootRelationPath = options.rootRelationPath;
|
|
40
63
|
} else {
|
|
41
|
-
this.options = options;
|
|
64
|
+
this.options = parseRelationExpressions(options);
|
|
65
|
+
this.rootRelationPath = rootRelationPath;
|
|
42
66
|
}
|
|
43
67
|
}
|
|
44
68
|
|
|
@@ -181,41 +205,136 @@ class GraphOptions {
|
|
|
181
205
|
return !!this.options[ALLOW_REFS];
|
|
182
206
|
}
|
|
183
207
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
for (const name of Object.keys(this.options)) {
|
|
189
|
-
const value = this.options[name];
|
|
190
|
-
|
|
191
|
-
if (Array.isArray(value)) {
|
|
192
|
-
newOpt[name] = value
|
|
193
|
-
.filter((it) => it.startsWith(newRootRelationPath))
|
|
194
|
-
.map((it) => it.slice(newRootRelationPath.length + 1))
|
|
195
|
-
.filter((it) => !!it);
|
|
196
|
-
} else {
|
|
197
|
-
newOpt[name] = value;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
208
|
+
allowMarkers() {
|
|
209
|
+
return !!this.options[ALLOW_MARKERS];
|
|
210
|
+
}
|
|
200
211
|
|
|
201
|
-
|
|
212
|
+
// The options of recursive upserts keep matching against the relation paths
|
|
213
|
+
// of the original graph, which the relation paths of the new root lead to.
|
|
214
|
+
rebasedOptions(newRoot) {
|
|
215
|
+
return new GraphOptions(this.options, [...this.rootRelationPath, ...newRoot.relationPath]);
|
|
202
216
|
}
|
|
203
217
|
|
|
204
218
|
_hasOption(node, optionName) {
|
|
205
219
|
const option = this.options[optionName];
|
|
206
220
|
|
|
207
|
-
if (
|
|
208
|
-
return option.
|
|
221
|
+
if (isRelationExpression(option)) {
|
|
222
|
+
return matchesRelationExpression(option, this._relationPath(node));
|
|
223
|
+
} else if (Array.isArray(option)) {
|
|
224
|
+
return option.includes(this._relationPath(node).join('.'));
|
|
209
225
|
} else if (typeof option === 'boolean') {
|
|
210
226
|
return option;
|
|
211
227
|
} else if (option === undefined) {
|
|
212
228
|
return false;
|
|
213
229
|
} else {
|
|
214
230
|
throw new Error(
|
|
215
|
-
`expected ${optionName} option value "${option}" to be an
|
|
231
|
+
`expected ${optionName} option value "${option}" to be a boolean, an array of relation paths or a relation expression`,
|
|
216
232
|
);
|
|
217
233
|
}
|
|
218
234
|
}
|
|
235
|
+
|
|
236
|
+
// The relation path of the node in the original graph, see `rebasedOptions()`.
|
|
237
|
+
_relationPath(node) {
|
|
238
|
+
return [...this.rootRelationPath, ...node.relationPath];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Parses the string values of the node options into relation expressions,
|
|
243
|
+
// once per operation.
|
|
244
|
+
function parseRelationExpressions(options) {
|
|
245
|
+
let parsedOptions = options;
|
|
246
|
+
|
|
247
|
+
for (const optionName of NODE_OPTION_NAMES) {
|
|
248
|
+
const option = options[optionName];
|
|
249
|
+
|
|
250
|
+
if (isString(option)) {
|
|
251
|
+
if (parsedOptions === options) {
|
|
252
|
+
parsedOptions = { ...options };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
parsedOptions[optionName] = parseRelationExpression(optionName, option);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return parsedOptions;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parseRelationExpression(optionName, expression) {
|
|
263
|
+
const createError = (message) => {
|
|
264
|
+
return new Error(
|
|
265
|
+
`invalid relation expression "${expression}" in ${optionName} option: ${message}`,
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
let parsedExpression;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
parsedExpression = RelationExpression.create(expression);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
throw createError(err.message);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (parsedExpression.isEmpty && !parsedExpression.node.$allRecursive) {
|
|
278
|
+
throw createError('the expression is empty');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Relation paths are matched by relation name, so aliases would never match.
|
|
282
|
+
const alias = findAlias(parsedExpression.node);
|
|
283
|
+
|
|
284
|
+
if (alias) {
|
|
285
|
+
throw createError(`aliases like "${alias.$relation} as ${alias.$name}" are not supported`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return parsedExpression;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function findAlias(node) {
|
|
292
|
+
for (const childName of node.$childNames) {
|
|
293
|
+
const child = node[childName];
|
|
294
|
+
|
|
295
|
+
if (child.$name !== child.$relation) {
|
|
296
|
+
return child;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const alias = findAlias(child);
|
|
300
|
+
|
|
301
|
+
if (alias) {
|
|
302
|
+
return alias;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// A relation path matches a relation expression when it leads to a leaf of
|
|
310
|
+
// the expression, like it would match the same relation paths in an array:
|
|
311
|
+
// `'movies.reviews'` matches `movies.reviews` but not `movies`. `'*'` matches
|
|
312
|
+
// all relations below it and recursive expressions like `'parent.^'` match
|
|
313
|
+
// every level of the recursion. The root has no relation path and never
|
|
314
|
+
// matches.
|
|
315
|
+
function matchesRelationExpression(expression, relationPath) {
|
|
316
|
+
if (relationPath.length === 0) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for (const relationName of relationPath) {
|
|
321
|
+
expression = expression.childExpression(relationName);
|
|
322
|
+
|
|
323
|
+
if (!expression) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Below `*`, the expression stays on the same node with increasing depth.
|
|
329
|
+
if (expression.node.$allRecursive) {
|
|
330
|
+
return expression.recursionDepth > 0;
|
|
331
|
+
} else {
|
|
332
|
+
return expression.isEmpty;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isRelationExpression(value) {
|
|
337
|
+
return !!value && value.isObjectionRelationExpression === true;
|
|
219
338
|
}
|
|
220
339
|
|
|
221
340
|
function getCurrentNode(node, graphData) {
|
|
@@ -57,6 +57,7 @@ class GraphUpsert {
|
|
|
57
57
|
return createGraphData(builder, graphOptions, graph)
|
|
58
58
|
.then(checkForErrors(builder))
|
|
59
59
|
.then(pruneGraphs())
|
|
60
|
+
.then(checkForJoinTableRelatedTableErrors(builder))
|
|
60
61
|
.then(executeOperations(builder))
|
|
61
62
|
.then(returnResult(this.objects, this.isArray));
|
|
62
63
|
}
|
|
@@ -66,6 +67,11 @@ class GraphUpsert {
|
|
|
66
67
|
// default behaviour instead (#84). A warning is logged once per option.
|
|
67
68
|
// TODO: Throw an error instead in objection 4.0.
|
|
68
69
|
function warnOnUnknownOptions(upsertOptions) {
|
|
70
|
+
// Recursive upserts pass on the already checked options of their parent.
|
|
71
|
+
if (upsertOptions instanceof GraphOptions) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
69
75
|
for (const name of Object.keys(upsertOptions)) {
|
|
70
76
|
if (!OPTION_NAMES.includes(name)) {
|
|
71
77
|
deprecate(
|
|
@@ -239,6 +245,7 @@ function checkForErrors(builder) {
|
|
|
239
245
|
checkForNotFoundErrors(graphData, builder);
|
|
240
246
|
checkForUnallowedRelationErrors(graphData, builder);
|
|
241
247
|
checkForUnallowedReferenceErrors(graphData, builder);
|
|
248
|
+
checkForUnallowedMarkerErrors(graphData, builder);
|
|
242
249
|
checkForRootUnrelateOrDeleteErrors(graphData, builder);
|
|
243
250
|
|
|
244
251
|
if (graphData.graphOptions.isInsertOnly()) {
|
|
@@ -249,6 +256,35 @@ function checkForErrors(builder) {
|
|
|
249
256
|
};
|
|
250
257
|
}
|
|
251
258
|
|
|
259
|
+
// Inserting or relating a node of a many-to-many relation inserts a join row,
|
|
260
|
+
// which would be a new related row if the join table is also the related
|
|
261
|
+
// table (#133). Check this before this graph is written. It runs after
|
|
262
|
+
// pruning, as the branches below related nodes are upserted and checked by
|
|
263
|
+
// nested `upsertGraph` calls against their own current graphs.
|
|
264
|
+
function checkForJoinTableRelatedTableErrors(builder) {
|
|
265
|
+
return (graphData) => {
|
|
266
|
+
const { graphOptions, currentGraph, graph } = graphData;
|
|
267
|
+
|
|
268
|
+
for (const node of graph.nodes) {
|
|
269
|
+
const relation = node.parentEdge && node.parentEdge.relation;
|
|
270
|
+
|
|
271
|
+
if (
|
|
272
|
+
relation &&
|
|
273
|
+
relation.isObjectionManyToManyRelation &&
|
|
274
|
+
!currentGraph.nodeForNode(node) &&
|
|
275
|
+
graphOptions.shouldInsertOrRelate(node, graphData)
|
|
276
|
+
) {
|
|
277
|
+
relation.assertCanModifyJoinTable(
|
|
278
|
+
builder,
|
|
279
|
+
graphOptions.shouldRelate(node, graphData) ? 'relate' : 'insert',
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return graphData;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
252
288
|
function checkForNotFoundErrors(graphData, builder) {
|
|
253
289
|
const { graphOptions, currentGraph, graph } = graphData;
|
|
254
290
|
|
|
@@ -309,6 +345,27 @@ function checkForUnallowedReferenceErrors(graphData, builder) {
|
|
|
309
345
|
}
|
|
310
346
|
}
|
|
311
347
|
|
|
348
|
+
// `#unrelate` and `#delete` markers come from the graph data, which may be
|
|
349
|
+
// untrusted input. Like `#ref`, they have to be allowed explicitly (#2410).
|
|
350
|
+
function checkForUnallowedMarkerErrors(graphData, builder) {
|
|
351
|
+
const { graph, graphOptions } = graphData;
|
|
352
|
+
|
|
353
|
+
if (graphOptions.allowMarkers()) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const node = graph.nodes.find((node) => node.toBeUnrelatedOrDeleted);
|
|
358
|
+
|
|
359
|
+
if (node) {
|
|
360
|
+
const { graphUnrelateProp, graphDeleteProp } = node.modelClass;
|
|
361
|
+
|
|
362
|
+
throw builder.modelClass().createValidationError({
|
|
363
|
+
type: ValidationErrorType.InvalidGraph,
|
|
364
|
+
message: `${graphUnrelateProp} and ${graphDeleteProp} markers are not allowed in a graph by default. see the allowMarkers insert/upsert graph option`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
312
369
|
function checkForRootUnrelateOrDeleteErrors(graphData, builder) {
|
|
313
370
|
for (const node of graphData.graph.nodes) {
|
|
314
371
|
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) &&
|
|
@@ -85,6 +85,9 @@ class GraphPatchAction extends GraphAction {
|
|
|
85
85
|
|
|
86
86
|
const updateBuilder = this._createBuilder(node)
|
|
87
87
|
.childQueryOf(builder, childQueryOptions())
|
|
88
|
+
// The current graph was fetched through the relations, so the patched
|
|
89
|
+
// models are known to be related to their parents.
|
|
90
|
+
.internalOptions({ skipRelationFilter: true })
|
|
88
91
|
.copyFrom(builder, GraphAction.ReturningAllSelector);
|
|
89
92
|
|
|
90
93
|
if (hasBeforeUpdate) {
|
|
@@ -37,7 +37,12 @@ class InstanceUpdateOperation extends UpdateOperation {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
result = await super.onAfter2(builder, result);
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
// A query resolved without updating anything, e.g. with `none()`, leaves
|
|
42
|
+
// the instance untouched.
|
|
43
|
+
if (!this.isResolvedWithoutUpdate(builder)) {
|
|
44
|
+
this.instance.$set(this.model);
|
|
45
|
+
}
|
|
41
46
|
|
|
42
47
|
if (isObject(result)) {
|
|
43
48
|
this.instance.$set(result);
|
|
@@ -14,6 +14,7 @@ class UpdateOperation extends QueryBuilderOperation {
|
|
|
14
14
|
|
|
15
15
|
this.model = null;
|
|
16
16
|
this.modelOptions = Object.assign({}, this.opt.modelOptions || {});
|
|
17
|
+
this.hasNothingToUpdate = false;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
onAdd(builder, args) {
|
|
@@ -34,10 +35,19 @@ class UpdateOperation extends QueryBuilderOperation {
|
|
|
34
35
|
|
|
35
36
|
if (isEmpty(row)) {
|
|
36
37
|
// Resolve the query if there is nothing to update.
|
|
38
|
+
this.hasNothingToUpdate = !builder.isExplicitlyResolvedOrRejected();
|
|
37
39
|
builder.resolve(0);
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
// Returns true if the query was resolved without updating anything, e.g.
|
|
44
|
+
// with `none()` or `resolve()` in a hook, but not if it was only resolved
|
|
45
|
+
// because there is nothing to update in the table, as the update still
|
|
46
|
+
// applies to everything else (#2184).
|
|
47
|
+
isResolvedWithoutUpdate(builder) {
|
|
48
|
+
return builder.isExplicitlyResolvedOrRejected() && !this.hasNothingToUpdate;
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
onBuildKnex(knexBuilder, builder) {
|
|
42
52
|
const json = this.model.$toDatabaseJson(builder);
|
|
43
53
|
const convertedJson = convertFieldExpressionsToRaw(builder, this.model, json);
|
|
@@ -56,6 +66,7 @@ class UpdateOperation extends QueryBuilderOperation {
|
|
|
56
66
|
clone() {
|
|
57
67
|
const clone = super.clone();
|
|
58
68
|
clone.model = this.model;
|
|
69
|
+
clone.hasNothingToUpdate = this.hasNothingToUpdate;
|
|
59
70
|
return clone;
|
|
60
71
|
}
|
|
61
72
|
}
|
|
@@ -42,7 +42,9 @@ class WhereInCompositeMsSqlOperation extends ObjectionToKnexConvertingOperation
|
|
|
42
42
|
knexBuilder,
|
|
43
43
|
columns,
|
|
44
44
|
helperColumns,
|
|
45
|
-
|
|
45
|
+
// knex only accepts strings as aliases, so the subquery is aliased in
|
|
46
|
+
// a raw query.
|
|
47
|
+
knex.raw(`? AS V(${helperColumns.map((_) => '??')})`, [values, ...helperColumns]),
|
|
46
48
|
);
|
|
47
49
|
}
|
|
48
50
|
}
|