objection 3.3.0 → 3.4.1
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 +87 -10
- package/lib/model/Model.js +3 -1
- package/lib/model/modelId.js +10 -2
- package/lib/model/modelQueryProps.js +7 -0
- package/lib/queryBuilder/QueryBuilder.js +5 -3
- package/lib/queryBuilder/QueryBuilderBase.js +44 -9
- package/lib/queryBuilder/ReferenceBuilder.js +8 -1
- package/lib/queryBuilder/graph/GraphFetcher.js +1 -3
- package/lib/queryBuilder/graph/GraphOptions.js +30 -1
- package/lib/queryBuilder/graph/GraphUpsert.js +16 -1
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +8 -2
- package/lib/queryBuilder/join/RelationJoiner.js +62 -9
- package/lib/queryBuilder/join/utils.js +20 -2
- package/lib/queryBuilder/operations/InsertOperation.js +11 -4
- package/lib/queryBuilder/operations/UpdateOperation.js +2 -0
- package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +1 -0
- package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -0
- package/lib/queryBuilder/operations/jsonApi/WhereJsonPostgresOperation.js +33 -0
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +16 -1
- package/lib/queryBuilder/operations/select/SelectOperation.js +39 -1
- package/lib/relations/RelationFindOperation.js +66 -2
- package/lib/relations/RelationProperty.js +3 -2
- package/lib/relations/manyToMany/ManyToManyRelation.js +34 -3
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +34 -4
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +4 -0
- package/lib/utils/assert.js +9 -0
- package/lib/utils/wrapDbError.js +36 -0
- package/package.json +2 -5
- package/typings/objection/index.d.ts +1 -1
|
@@ -6,6 +6,7 @@ const { Validator } = require('./Validator');
|
|
|
6
6
|
const { ValidationErrorType } = require('../model/ValidationError');
|
|
7
7
|
const { isObject, once, cloneDeep: lodashCloneDeep, omit } = require('../utils/objectUtils');
|
|
8
8
|
const { parseFieldExpression } = require('../utils/parseFieldExpression');
|
|
9
|
+
const { getQueryPropKeys } = require('./modelQueryProps');
|
|
9
10
|
|
|
10
11
|
class AjvValidator extends Validator {
|
|
11
12
|
static init(self, conf) {
|
|
@@ -51,6 +52,10 @@ class AjvValidator extends Validator {
|
|
|
51
52
|
|
|
52
53
|
setupAjv(self.ajv);
|
|
53
54
|
setupAjv(self.ajvNoDefaults);
|
|
55
|
+
|
|
56
|
+
// Created when needed by `compileQueryPropValidator()`.
|
|
57
|
+
self.ajvQueryProps = null;
|
|
58
|
+
self.setupAjv = setupAjv;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
beforeValidate({ json, model, options, ctx }) {
|
|
@@ -75,7 +80,12 @@ class AjvValidator extends Validator {
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
const modelClass = model.constructor;
|
|
78
|
-
const validator = this.getValidator(
|
|
83
|
+
const validator = this.getValidator(
|
|
84
|
+
modelClass,
|
|
85
|
+
ctx.jsonSchema,
|
|
86
|
+
!!options.patch,
|
|
87
|
+
options.patch ? null : getMissingQueryPropKeys(model, json, ctx.jsonSchema),
|
|
88
|
+
);
|
|
79
89
|
|
|
80
90
|
// We need to clone the input json if we are about to set default values.
|
|
81
91
|
if (!options.mutable && !options.patch && setsDefaultValues(ctx.jsonSchema)) {
|
|
@@ -103,7 +113,7 @@ class AjvValidator extends Validator {
|
|
|
103
113
|
return json;
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
getValidator(modelClass, jsonSchema, isPatchObject) {
|
|
116
|
+
getValidator(modelClass, jsonSchema, isPatchObject, queryPropKeys = null) {
|
|
107
117
|
// Use the Ajv custom serializer if provided.
|
|
108
118
|
const createCacheKey = this.ajvOptions.serialize || JSON.stringify;
|
|
109
119
|
|
|
@@ -136,6 +146,10 @@ class AjvValidator extends Validator {
|
|
|
136
146
|
|
|
137
147
|
// Validator created for the unmodified schema.
|
|
138
148
|
normalValidator: null,
|
|
149
|
+
|
|
150
|
+
// Validators created for the schema object without the `required`
|
|
151
|
+
// properties that are given as query properties, keyed by their names.
|
|
152
|
+
queryPropValidators: null,
|
|
139
153
|
};
|
|
140
154
|
|
|
141
155
|
this.cache.set(cacheKey, validators);
|
|
@@ -148,6 +162,17 @@ class AjvValidator extends Validator {
|
|
|
148
162
|
validator = this.compilePatchValidator(jsonSchema);
|
|
149
163
|
validators.patchValidator = validator;
|
|
150
164
|
}
|
|
165
|
+
} else if (queryPropKeys) {
|
|
166
|
+
const key = JSON.stringify(queryPropKeys);
|
|
167
|
+
validators.queryPropValidators = validators.queryPropValidators || new Map();
|
|
168
|
+
validator = validators.queryPropValidators.get(key);
|
|
169
|
+
|
|
170
|
+
if (!validator) {
|
|
171
|
+
validator = this.compileQueryPropValidator(
|
|
172
|
+
jsonSchemaWithoutRequiredKeys(jsonSchema, queryPropKeys),
|
|
173
|
+
);
|
|
174
|
+
validators.queryPropValidators.set(key, validator);
|
|
175
|
+
}
|
|
151
176
|
} else {
|
|
152
177
|
validator = validators.normalValidator;
|
|
153
178
|
|
|
@@ -169,6 +194,29 @@ class AjvValidator extends Validator {
|
|
|
169
194
|
compileNormalValidator(jsonSchema) {
|
|
170
195
|
return this.ajv.compile(jsonSchema);
|
|
171
196
|
}
|
|
197
|
+
|
|
198
|
+
compileQueryPropValidator(jsonSchema) {
|
|
199
|
+
// The schema is a modified copy that keeps the `$id`s of the original, so
|
|
200
|
+
// it's compiled on an instance that doesn't add compiled schemas by their
|
|
201
|
+
// `$id`. Otherwise ajv would throw because the ids already exist.
|
|
202
|
+
if (!this.ajvQueryProps) {
|
|
203
|
+
this.ajvQueryProps = new Ajv(
|
|
204
|
+
Object.assign(
|
|
205
|
+
{
|
|
206
|
+
useDefaults: true,
|
|
207
|
+
},
|
|
208
|
+
this.ajvOptions,
|
|
209
|
+
{
|
|
210
|
+
addUsedSchema: false,
|
|
211
|
+
},
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
this.setupAjv(this.ajvQueryProps);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return this.ajvQueryProps.compile(jsonSchema);
|
|
219
|
+
}
|
|
172
220
|
}
|
|
173
221
|
|
|
174
222
|
function parseValidationError(errors, modelClass, options, ajvOptions) {
|
|
@@ -397,6 +445,30 @@ function objectHasDefaults(obj) {
|
|
|
397
445
|
return false;
|
|
398
446
|
}
|
|
399
447
|
|
|
448
|
+
// Returns the keys of the query properties of `model` (`raw()`, `val()`,
|
|
449
|
+
// `ref()`, subqueries...) that are required by the schema but missing from
|
|
450
|
+
// `json`, sorted, or `null` if there are none. Query properties are split from
|
|
451
|
+
// the json before validation, but they do provide a value for the property.
|
|
452
|
+
function getMissingQueryPropKeys(model, json, jsonSchema) {
|
|
453
|
+
const required = jsonSchema.required;
|
|
454
|
+
|
|
455
|
+
if (!Array.isArray(required) || required.length === 0) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const keys = getQueryPropKeys(model).filter(
|
|
460
|
+
(key) => required.includes(key) && !Object.prototype.hasOwnProperty.call(json, key),
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
return keys.length > 0 ? keys.sort() : null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
|
|
467
|
+
return Object.assign({}, jsonSchema, {
|
|
468
|
+
required: jsonSchema.required.filter((key) => !keys.includes(key)),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
400
472
|
function jsonSchemaWithoutRequired(jsonSchema) {
|
|
401
473
|
// `not` is intentionally left untouched: Stripping `required` inside it
|
|
402
474
|
// would turn it into a schema that rejects everything (e.g.
|
|
@@ -436,11 +508,20 @@ function definitionsWithoutRequired(jsonSchema, prop) {
|
|
|
436
508
|
function subSchemaWithoutRequired(jsonSchema, prop) {
|
|
437
509
|
if (jsonSchema[prop]) {
|
|
438
510
|
if (Array.isArray(jsonSchema[prop])) {
|
|
439
|
-
const schemaArray =
|
|
440
|
-
|
|
441
|
-
|
|
511
|
+
const schemaArray = jsonSchema[prop].map(jsonSchemaWithoutRequired);
|
|
512
|
+
const nonEmptySchemaArray = schemaArray.filter(isNotEmptyObject);
|
|
513
|
+
|
|
514
|
+
// An `anyOf` or `oneOf` option that only consisted of `required` is
|
|
515
|
+
// empty once `required` is stripped, and an empty schema matches any
|
|
516
|
+
// patch, just like the stripped `required` keywords elsewhere. Drop the
|
|
517
|
+
// whole keyword then: requiring the patch to match one of the remaining
|
|
518
|
+
// options would reject valid patches, and with `oneOf` keeping the empty
|
|
519
|
+
// option would fail every patch that also matches another option.
|
|
520
|
+
if (prop !== 'allOf' && nonEmptySchemaArray.length !== schemaArray.length) {
|
|
521
|
+
return {};
|
|
522
|
+
} else if (nonEmptySchemaArray.length !== 0) {
|
|
442
523
|
return {
|
|
443
|
-
[prop]:
|
|
524
|
+
[prop]: nonEmptySchemaArray,
|
|
444
525
|
};
|
|
445
526
|
} else {
|
|
446
527
|
return {};
|
|
@@ -467,10 +548,6 @@ function subSchemaWithoutRequired(jsonSchema, prop) {
|
|
|
467
548
|
}
|
|
468
549
|
}
|
|
469
550
|
|
|
470
|
-
function jsonSchemaArrayWithoutRequired(jsonSchemaArray) {
|
|
471
|
-
return jsonSchemaArray.map(jsonSchemaWithoutRequired).filter(isNotEmptyObject);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
551
|
function isNotEmptyObject(obj) {
|
|
475
552
|
return Object.keys(obj).length !== 0;
|
|
476
553
|
}
|
package/lib/model/Model.js
CHANGED
|
@@ -839,11 +839,13 @@ function getColumnNameMappers(modelClass) {
|
|
|
839
839
|
}
|
|
840
840
|
|
|
841
841
|
function getIdRelationProperty(modelClass) {
|
|
842
|
-
|
|
842
|
+
// `idColumn = null` means that the model has no primary key.
|
|
843
|
+
const idColumn = modelClass.getIdColumn() === null ? [] : asArray(modelClass.getIdColumn());
|
|
843
844
|
|
|
844
845
|
return new RelationProperty(
|
|
845
846
|
idColumn.map((idCol) => `${modelClass.getTableName()}.${idCol}`),
|
|
846
847
|
() => modelClass,
|
|
848
|
+
{ allowEmpty: true },
|
|
847
849
|
);
|
|
848
850
|
}
|
|
849
851
|
|
package/lib/model/modelId.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { assertHasIdColumn } = require('../utils/assert');
|
|
4
|
+
|
|
3
5
|
function getSetId(model, maybeId) {
|
|
4
6
|
if (maybeId !== undefined) {
|
|
5
7
|
return setId(model, maybeId);
|
|
@@ -9,10 +11,13 @@ function getSetId(model, maybeId) {
|
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
function hasId(model) {
|
|
12
|
-
|
|
14
|
+
const idProps = model.constructor.getIdPropertyArray();
|
|
15
|
+
// Models without an `idColumn` never have an id.
|
|
16
|
+
return idProps.length > 0 && model.$hasProps(idProps);
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
function setId(model, id) {
|
|
20
|
+
assertHasIdColumn(model.constructor);
|
|
16
21
|
const idProp = model.constructor.getIdProperty();
|
|
17
22
|
const isCompositeId = Array.isArray(idProp);
|
|
18
23
|
|
|
@@ -49,7 +54,10 @@ function getId(model) {
|
|
|
49
54
|
const idProp = model.constructor.getIdProperty();
|
|
50
55
|
const isCompositeId = Array.isArray(idProp);
|
|
51
56
|
|
|
52
|
-
if (isCompositeId) {
|
|
57
|
+
if (isCompositeId && idProp.length === 0) {
|
|
58
|
+
// Models without an `idColumn` have no id.
|
|
59
|
+
return undefined;
|
|
60
|
+
} else if (isCompositeId) {
|
|
53
61
|
return model.$values(idProp);
|
|
54
62
|
} else {
|
|
55
63
|
return model[idProp];
|
|
@@ -108,6 +108,12 @@ function convertAndMergeHiddenQueryProps(model, json, omitProps, builder) {
|
|
|
108
108
|
return json;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// Returns the keys of the query properties that were split from `model`.
|
|
112
|
+
function getQueryPropKeys(model) {
|
|
113
|
+
const queryProps = model[QUERY_PROPS_PROPERTY];
|
|
114
|
+
return queryProps ? Object.keys(queryProps) : [];
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
// Converts a query property into a knex `raw` instance.
|
|
112
118
|
function queryPropToKnexRaw(queryProp, builder) {
|
|
113
119
|
if (!queryProp) {
|
|
@@ -144,4 +150,5 @@ function isKnexRawConvertable(queryProp) {
|
|
|
144
150
|
module.exports = {
|
|
145
151
|
splitQueryProps,
|
|
146
152
|
mergeQueryProps,
|
|
153
|
+
getQueryPropKeys,
|
|
147
154
|
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { wrapDbError } = require('../utils/wrapDbError');
|
|
4
4
|
|
|
5
5
|
const { raw } = require('./RawBuilder');
|
|
6
6
|
const { createModifier } = require('../utils/createModifier');
|
|
7
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
|
-
const { assertIdNotUndefined } = require('../utils/assert');
|
|
10
|
+
const { assertIdNotUndefined, assertHasIdColumn } = require('../utils/assert');
|
|
11
11
|
|
|
12
12
|
const { Selection } = require('./operations/select/Selection');
|
|
13
13
|
|
|
@@ -125,6 +125,8 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
fullIdColumnFor(modelClass) {
|
|
128
|
+
assertHasIdColumn(modelClass);
|
|
129
|
+
|
|
128
130
|
const tableName = this.tableRefFor(modelClass);
|
|
129
131
|
const idColumn = modelClass.getIdColumn();
|
|
130
132
|
|
|
@@ -1408,7 +1410,7 @@ function handleExecuteError(builder, err) {
|
|
|
1408
1410
|
return Promise.resolve(err.value);
|
|
1409
1411
|
}
|
|
1410
1412
|
|
|
1411
|
-
let promise = Promise.reject(
|
|
1413
|
+
let promise = Promise.reject(wrapDbError(err));
|
|
1412
1414
|
|
|
1413
1415
|
builder.forEachOperation(true, (op) => {
|
|
1414
1416
|
if (op.hasOnError()) {
|
|
@@ -616,9 +616,14 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
616
616
|
// whereJson(Not)SupersetOf / whereJson(Not)SubsetOf are now supported by knex >= 1.0, but for now
|
|
617
617
|
// objection handles them differently and only for postgres.
|
|
618
618
|
// Changing them to utilize knex methods directly may require a major version bump and upgrade guide.
|
|
619
|
+
// On MySQL, the simple `(column, object | array)` form is passed on to knex (ditojs#113).
|
|
619
620
|
whereJsonSupersetOf(...args) {
|
|
620
621
|
return this.addOperation(
|
|
621
|
-
new WhereJsonPostgresOperation('whereJsonSupersetOf', {
|
|
622
|
+
new WhereJsonPostgresOperation('whereJsonSupersetOf', {
|
|
623
|
+
operator: '@>',
|
|
624
|
+
bool: 'and',
|
|
625
|
+
knexMethod: 'whereJsonSupersetOf',
|
|
626
|
+
}),
|
|
622
627
|
args,
|
|
623
628
|
);
|
|
624
629
|
}
|
|
@@ -629,7 +634,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
629
634
|
|
|
630
635
|
orWhereJsonSupersetOf(...args) {
|
|
631
636
|
return this.addOperation(
|
|
632
|
-
new WhereJsonPostgresOperation('orWhereJsonSupersetOf', {
|
|
637
|
+
new WhereJsonPostgresOperation('orWhereJsonSupersetOf', {
|
|
638
|
+
operator: '@>',
|
|
639
|
+
bool: 'or',
|
|
640
|
+
knexMethod: 'whereJsonSupersetOf',
|
|
641
|
+
}),
|
|
633
642
|
args,
|
|
634
643
|
);
|
|
635
644
|
}
|
|
@@ -640,13 +649,14 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
640
649
|
operator: '@>',
|
|
641
650
|
bool: 'and',
|
|
642
651
|
prefix: 'not',
|
|
652
|
+
knexMethod: 'whereJsonNotSupersetOf',
|
|
643
653
|
}),
|
|
644
654
|
args,
|
|
645
655
|
);
|
|
646
656
|
}
|
|
647
657
|
|
|
648
658
|
andWhereJsonNotSupersetOf(...args) {
|
|
649
|
-
return this.
|
|
659
|
+
return this.whereJsonNotSupersetOf(...args);
|
|
650
660
|
}
|
|
651
661
|
|
|
652
662
|
orWhereJsonNotSupersetOf(...args) {
|
|
@@ -655,6 +665,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
655
665
|
operator: '@>',
|
|
656
666
|
bool: 'or',
|
|
657
667
|
prefix: 'not',
|
|
668
|
+
knexMethod: 'whereJsonNotSupersetOf',
|
|
658
669
|
}),
|
|
659
670
|
args,
|
|
660
671
|
);
|
|
@@ -662,7 +673,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
662
673
|
|
|
663
674
|
whereJsonSubsetOf(...args) {
|
|
664
675
|
return this.addOperation(
|
|
665
|
-
new WhereJsonPostgresOperation('whereJsonSubsetOf', {
|
|
676
|
+
new WhereJsonPostgresOperation('whereJsonSubsetOf', {
|
|
677
|
+
operator: '<@',
|
|
678
|
+
bool: 'and',
|
|
679
|
+
knexMethod: 'whereJsonSubsetOf',
|
|
680
|
+
}),
|
|
666
681
|
args,
|
|
667
682
|
);
|
|
668
683
|
}
|
|
@@ -673,7 +688,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
673
688
|
|
|
674
689
|
orWhereJsonSubsetOf(...args) {
|
|
675
690
|
return this.addOperation(
|
|
676
|
-
new WhereJsonPostgresOperation('orWhereJsonSubsetOf', {
|
|
691
|
+
new WhereJsonPostgresOperation('orWhereJsonSubsetOf', {
|
|
692
|
+
operator: '<@',
|
|
693
|
+
bool: 'or',
|
|
694
|
+
knexMethod: 'whereJsonSubsetOf',
|
|
695
|
+
}),
|
|
677
696
|
args,
|
|
678
697
|
);
|
|
679
698
|
}
|
|
@@ -684,6 +703,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
684
703
|
operator: '<@',
|
|
685
704
|
bool: 'and',
|
|
686
705
|
prefix: 'not',
|
|
706
|
+
knexMethod: 'whereJsonNotSubsetOf',
|
|
687
707
|
}),
|
|
688
708
|
args,
|
|
689
709
|
);
|
|
@@ -699,6 +719,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
699
719
|
operator: '<@',
|
|
700
720
|
bool: 'or',
|
|
701
721
|
prefix: 'not',
|
|
722
|
+
knexMethod: 'whereJsonNotSubsetOf',
|
|
702
723
|
}),
|
|
703
724
|
args,
|
|
704
725
|
);
|
|
@@ -772,20 +793,34 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
772
793
|
);
|
|
773
794
|
}
|
|
774
795
|
|
|
796
|
+
// Not passed on to knex like `whereJsonSupersetOf()`, as MySQL's `json_contains()`
|
|
797
|
+
// also matches `{}` in arrays of objects.
|
|
775
798
|
whereJsonIsArray(fieldExpression) {
|
|
776
|
-
return this.
|
|
799
|
+
return this.addOperation(
|
|
800
|
+
new WhereJsonPostgresOperation('whereJsonSupersetOf', { operator: '@>', bool: 'and' }),
|
|
801
|
+
[fieldExpression, []],
|
|
802
|
+
);
|
|
777
803
|
}
|
|
778
804
|
|
|
779
805
|
orWhereJsonIsArray(fieldExpression) {
|
|
780
|
-
return this.
|
|
806
|
+
return this.addOperation(
|
|
807
|
+
new WhereJsonPostgresOperation('orWhereJsonSupersetOf', { operator: '@>', bool: 'or' }),
|
|
808
|
+
[fieldExpression, []],
|
|
809
|
+
);
|
|
781
810
|
}
|
|
782
811
|
|
|
783
812
|
whereJsonIsObject(fieldExpression) {
|
|
784
|
-
return this.
|
|
813
|
+
return this.addOperation(
|
|
814
|
+
new WhereJsonPostgresOperation('whereJsonSupersetOf', { operator: '@>', bool: 'and' }),
|
|
815
|
+
[fieldExpression, {}],
|
|
816
|
+
);
|
|
785
817
|
}
|
|
786
818
|
|
|
787
819
|
orWhereJsonIsObject(fieldExpression) {
|
|
788
|
-
return this.
|
|
820
|
+
return this.addOperation(
|
|
821
|
+
new WhereJsonPostgresOperation('orWhereJsonSupersetOf', { operator: '@>', bool: 'or' }),
|
|
822
|
+
[fieldExpression, {}],
|
|
823
|
+
);
|
|
789
824
|
}
|
|
790
825
|
|
|
791
826
|
whereColumn(...args) {
|
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
toPostgresJsonPathLiteral,
|
|
6
6
|
} = require('../utils/parseFieldExpression');
|
|
7
7
|
const { isObject } = require('../utils/objectUtils');
|
|
8
|
+
const { warnIfNotPostgres } = require('./operations/jsonApi/postgresJsonApi');
|
|
8
9
|
|
|
9
10
|
class ReferenceBuilder {
|
|
10
11
|
constructor(expr) {
|
|
@@ -163,7 +164,13 @@ class ReferenceBuilder {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
toKnexRaw(builder) {
|
|
166
|
-
|
|
167
|
+
const knex = builder.knex();
|
|
168
|
+
|
|
169
|
+
if (this._parsedExpr.access.length > 0) {
|
|
170
|
+
warnIfNotPostgres(knex);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return knex.raw(...this._createRawArgs(builder));
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
_parseExpression(expr) {
|
|
@@ -153,9 +153,7 @@ function createManyToManyExtraSelectionIfNeeded(builder, column, node) {
|
|
|
153
153
|
const extra = relation.joinTableExtras.find((extra) => extra.aliasCol === column);
|
|
154
154
|
|
|
155
155
|
if (extra) {
|
|
156
|
-
return `${
|
|
157
|
-
extra.aliasCol
|
|
158
|
-
}`;
|
|
156
|
+
return `${relation.joinTableAliasFor(builder)}.${extra.joinTableCol} as ${extra.aliasCol}`;
|
|
159
157
|
}
|
|
160
158
|
}
|
|
161
159
|
|
|
@@ -13,6 +13,20 @@ const INSERT_MISSING = 'insertMissing';
|
|
|
13
13
|
const FETCH_STRATEGY = 'fetchStrategy';
|
|
14
14
|
const ALLOW_REFS = 'allowRefs';
|
|
15
15
|
|
|
16
|
+
const OPTION_NAMES = [
|
|
17
|
+
NO_RELATE,
|
|
18
|
+
NO_UNRELATE,
|
|
19
|
+
NO_INSERT,
|
|
20
|
+
NO_UPDATE,
|
|
21
|
+
NO_DELETE,
|
|
22
|
+
UPDATE,
|
|
23
|
+
RELATE,
|
|
24
|
+
UNRELATE,
|
|
25
|
+
INSERT_MISSING,
|
|
26
|
+
FETCH_STRATEGY,
|
|
27
|
+
ALLOW_REFS,
|
|
28
|
+
];
|
|
29
|
+
|
|
16
30
|
const FetchStrategy = {
|
|
17
31
|
OnlyIdentifiers: 'OnlyIdentifiers',
|
|
18
32
|
Everything: 'Everything',
|
|
@@ -64,7 +78,7 @@ class GraphOptions {
|
|
|
64
78
|
!getCurrentNode(node, graphData) &&
|
|
65
79
|
!!node.parentEdge &&
|
|
66
80
|
!!node.parentEdge.relation &&
|
|
67
|
-
|
|
81
|
+
hasRelateProp(node) &&
|
|
68
82
|
graphData.nodeDbExistence.doesNodeExistInDb(node)
|
|
69
83
|
);
|
|
70
84
|
}
|
|
@@ -212,6 +226,20 @@ function getCurrentNode(node, graphData) {
|
|
|
212
226
|
return graphData.currentGraph.nodeForNode(node);
|
|
213
227
|
}
|
|
214
228
|
|
|
229
|
+
// For `HasMany` and `HasOne` relations, the relate prop is the id of the
|
|
230
|
+
// related model. Use the id state of the input graph, since the parent's
|
|
231
|
+
// insert copies the foreign key into the related model, which can complete
|
|
232
|
+
// a composite id that contains it and would turn an insert into a relate (#2544).
|
|
233
|
+
function hasRelateProp(node) {
|
|
234
|
+
const relation = node.parentEdge.relation;
|
|
235
|
+
|
|
236
|
+
if (relation.isObjectionHasManyRelation) {
|
|
237
|
+
return node.hasId;
|
|
238
|
+
} else {
|
|
239
|
+
return relation.hasRelateProp(node.obj);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
215
243
|
function getNode(currentNode, graph) {
|
|
216
244
|
if (!graph || !currentNode) {
|
|
217
245
|
return null;
|
|
@@ -222,5 +250,6 @@ function getNode(currentNode, graph) {
|
|
|
222
250
|
|
|
223
251
|
module.exports = {
|
|
224
252
|
GraphOptions,
|
|
253
|
+
OPTION_NAMES,
|
|
225
254
|
FetchStrategy,
|
|
226
255
|
};
|
|
@@ -8,7 +8,7 @@ const { GraphInsert } = require('./insert/GraphInsert');
|
|
|
8
8
|
const { GraphPatch } = require('./patch/GraphPatch');
|
|
9
9
|
const { GraphDelete } = require('./delete/GraphDelete');
|
|
10
10
|
const { GraphRecursiveUpsert } = require('./recursiveUpsert/GraphRecursiveUpsert');
|
|
11
|
-
const { GraphOptions } = require('./GraphOptions');
|
|
11
|
+
const { GraphOptions, OPTION_NAMES } = require('./GraphOptions');
|
|
12
12
|
const { ValidationErrorType } = require('../../model/ValidationError');
|
|
13
13
|
const { RelationExpression } = require('../RelationExpression');
|
|
14
14
|
const { GraphNodeDbExistence } = require('./GraphNodeDbExistence');
|
|
@@ -19,6 +19,7 @@ const { deprecate } = require('../../utils/deprecate');
|
|
|
19
19
|
class GraphUpsert {
|
|
20
20
|
constructor({ rootModelClass, objects, upsertOptions }) {
|
|
21
21
|
checkCanBeConvertedToModels(rootModelClass, objects);
|
|
22
|
+
warnOnUnknownOptions(upsertOptions);
|
|
22
23
|
|
|
23
24
|
this.objects = rootModelClass.ensureModelArray(objects, GraphUpsert.modelOptions);
|
|
24
25
|
this.isArray = Array.isArray(objects);
|
|
@@ -61,6 +62,20 @@ class GraphUpsert {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
// Misspelled options are silently ignored, so the operation runs with the
|
|
66
|
+
// default behaviour instead (#84). A warning is logged once per option.
|
|
67
|
+
// TODO: Throw an error instead in objection 4.0.
|
|
68
|
+
function warnOnUnknownOptions(upsertOptions) {
|
|
69
|
+
for (const name of Object.keys(upsertOptions)) {
|
|
70
|
+
if (!OPTION_NAMES.includes(name)) {
|
|
71
|
+
deprecate(
|
|
72
|
+
`Unknown graph option "${name}" is ignored. ` +
|
|
73
|
+
`Valid options are: ${OPTION_NAMES.join(', ')}. This will throw in objection 4.0.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
function checkCanBeConvertedToModels(modelClass, objects) {
|
|
65
80
|
asArray(objects).forEach((obj) => {
|
|
66
81
|
if (!isObject(obj)) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { getModel } = require('../../../model/getModel');
|
|
4
4
|
const { GraphAction } = require('../GraphAction');
|
|
5
5
|
const { isInternalProp } = require('../../../utils/internalPropUtils');
|
|
6
|
-
const { difference, isObject, jsonEquals } = require('../../../utils/objectUtils');
|
|
6
|
+
const { difference, isObject, isPlainObject, jsonEquals } = require('../../../utils/objectUtils');
|
|
7
7
|
const promiseUtils = require('../../../utils/promiseUtils');
|
|
8
8
|
|
|
9
9
|
class GraphPatchAction extends GraphAction {
|
|
@@ -251,13 +251,19 @@ function childQueryOptions() {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
function nonStrictEquals(val1, val2) {
|
|
254
|
-
|
|
254
|
+
// Only use loose equality for non-JSON values, so that for example `false`
|
|
255
|
+
// and `[]` are not considered equal. See issue #1874.
|
|
256
|
+
if (!isJsonValue(val1) && !isJsonValue(val2) && val1 == val2) {
|
|
255
257
|
return true;
|
|
256
258
|
}
|
|
257
259
|
|
|
258
260
|
return jsonEquals(val1, val2);
|
|
259
261
|
}
|
|
260
262
|
|
|
263
|
+
function isJsonValue(value) {
|
|
264
|
+
return Array.isArray(value) || isPlainObject(value);
|
|
265
|
+
}
|
|
266
|
+
|
|
261
267
|
function isToBeUnrelatedOrDeleted(node) {
|
|
262
268
|
return !!node && node.toBeUnrelatedOrDeleted;
|
|
263
269
|
}
|