objection 3.3.0 → 3.4.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 +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/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 +9 -5
- package/lib/queryBuilder/join/utils.js +20 -2
- package/lib/queryBuilder/operations/InsertOperation.js +11 -4
- package/lib/relations/RelationFindOperation.js +46 -2
- package/lib/relations/RelationProperty.js +3 -2
- package/lib/relations/manyToMany/ManyToManyRelation.js +34 -3
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +4 -2
- 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()) {
|
|
@@ -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
|
}
|
|
@@ -8,7 +8,7 @@ const { ValidationErrorType } = require('../../model/ValidationError');
|
|
|
8
8
|
|
|
9
9
|
const { TableTree } = require('./TableTree');
|
|
10
10
|
const { JoinResultParser } = require('./JoinResultParser');
|
|
11
|
-
const {
|
|
11
|
+
const { getIdentifierLengthLimit } = require('./utils');
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Given a relation expression, builds a query using joins to fetch it.
|
|
@@ -179,8 +179,10 @@ class RelationJoiner {
|
|
|
179
179
|
userSelectQueries,
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
+
const lengthLimit = getIdentifierLengthLimit(builder.knex());
|
|
183
|
+
|
|
182
184
|
for (const selection of selections) {
|
|
183
|
-
checkAliasLength(tableNode.modelClass, selection.name);
|
|
185
|
+
checkAliasLength(tableNode.modelClass, selection.name, lengthLimit);
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
// Save the selections that were added internally (not by the user)
|
|
@@ -393,11 +395,13 @@ function selectsOnlyAliasesKeepingSelectAll(selections) {
|
|
|
393
395
|
return selections.length > 0 && selections.every((it) => it.keepsSelectAll);
|
|
394
396
|
}
|
|
395
397
|
|
|
396
|
-
function checkAliasLength(modelClass, alias) {
|
|
397
|
-
if (alias.length >
|
|
398
|
+
function checkAliasLength(modelClass, alias, lengthLimit) {
|
|
399
|
+
if (alias.length > lengthLimit) {
|
|
398
400
|
throw modelClass.createValidationError({
|
|
399
401
|
type: ValidationErrorType.RelationExpression,
|
|
400
|
-
message:
|
|
402
|
+
message:
|
|
403
|
+
`identifier ${alias} is over ${lengthLimit} characters long and would be truncated by the database engine. ` +
|
|
404
|
+
'Use the `minimize` option of withGraphJoined() to shorten the aliases.',
|
|
401
405
|
});
|
|
402
406
|
}
|
|
403
407
|
}
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { ValidationErrorType } = require('../../model/ValidationError');
|
|
4
|
+
const { isPostgres, isMySql, isMsSql, isSqlite } = require('../../utils/knexUtils');
|
|
4
5
|
|
|
5
|
-
const ID_LENGTH_LIMIT = 63;
|
|
6
6
|
const RELATION_RECURSION_LIMIT = 64;
|
|
7
7
|
|
|
8
|
+
// The maximum length of the column aliases of the joined relations, after
|
|
9
|
+
// which the database would truncate them. Unknown databases use the strictest
|
|
10
|
+
// of the known limits, the one of postgres.
|
|
11
|
+
function getIdentifierLengthLimit(knex) {
|
|
12
|
+
if (isPostgres(knex)) {
|
|
13
|
+
return 63;
|
|
14
|
+
} else if (isMySql(knex)) {
|
|
15
|
+
return 256;
|
|
16
|
+
} else if (isMsSql(knex)) {
|
|
17
|
+
return 128;
|
|
18
|
+
} else if (isSqlite(knex)) {
|
|
19
|
+
return Infinity;
|
|
20
|
+
} else {
|
|
21
|
+
return 63;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
// Given a relation expression, goes through all first level children.
|
|
9
26
|
function forEachChildExpression(expr, modelClass, callback) {
|
|
10
27
|
if (expr.node.$allRecursive || expr.maxRecursionDepth > RELATION_RECURSION_LIMIT) {
|
|
@@ -18,8 +35,9 @@ function forEachChildExpression(expr, modelClass, callback) {
|
|
|
18
35
|
}
|
|
19
36
|
|
|
20
37
|
module.exports = {
|
|
21
|
-
ID_LENGTH_LIMIT,
|
|
22
38
|
RELATION_RECURSION_LIMIT,
|
|
23
39
|
|
|
40
|
+
getIdentifierLengthLimit,
|
|
41
|
+
|
|
24
42
|
forEachChildExpression,
|
|
25
43
|
};
|
|
@@ -38,18 +38,20 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
38
38
|
onBuildKnex(knexBuilder, builder) {
|
|
39
39
|
const knex = builder.knex();
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
const defaultReturning = getDefaultReturning(builder);
|
|
42
|
+
|
|
43
|
+
if (!builder.has(/returning/) && defaultReturning !== null) {
|
|
42
44
|
if (!isSqlite(knex) && !isMySql(knex)) {
|
|
43
45
|
// If the user hasn't specified a `returning` clause, we make sure
|
|
44
46
|
// that at least the identifier is returned. With `onConflict()` the
|
|
45
47
|
// conflict columns are returned too, so that the returned rows can
|
|
46
48
|
// be matched to the models when some of the rows are ignored.
|
|
47
|
-
knexBuilder = knexBuilder.returning(
|
|
49
|
+
knexBuilder = knexBuilder.returning(defaultReturning);
|
|
48
50
|
} else if (isSqlite(knex) && builder.has('onConflict')) {
|
|
49
51
|
// Without a `returning` clause, SQLite returns the id of the last
|
|
50
52
|
// inserted row, which belongs to a previous insert if the row was
|
|
51
53
|
// ignored. Use `returning` to find out what was actually inserted.
|
|
52
|
-
knexBuilder = knexBuilder.returning(
|
|
54
|
+
knexBuilder = knexBuilder.returning(defaultReturning);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -77,7 +79,7 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
77
79
|
this.models[i].$setDatabaseJson(ret[i]);
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
|
-
} else {
|
|
82
|
+
} else if (builder.modelClass().getIdColumnArray().length > 0) {
|
|
81
83
|
// If the return value is not an array of objects, we assume it is an array of identifiers.
|
|
82
84
|
for (let i = 0, l = this.models.length; i < l; ++i) {
|
|
83
85
|
const model = this.models[i];
|
|
@@ -113,10 +115,15 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
// Returns `null` if there's nothing to return, e.g. for models without an `idColumn`.
|
|
116
119
|
function getDefaultReturning(builder) {
|
|
117
120
|
const idColumns = builder.modelClass().getIdColumnArray();
|
|
118
121
|
const columns = Array.from(new Set(idColumns.concat(getConflictColumns(builder))));
|
|
119
122
|
|
|
123
|
+
if (columns.length === 0) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
return columns.length === 1 ? columns[0] : columns;
|
|
121
128
|
}
|
|
122
129
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { FindOperation } = require('../queryBuilder/operations/FindOperation');
|
|
4
|
+
const { deprecate } = require('../utils/deprecate');
|
|
4
5
|
|
|
5
6
|
class RelationFindOperation extends FindOperation {
|
|
6
7
|
constructor(name, opt) {
|
|
@@ -19,8 +20,12 @@ class RelationFindOperation extends FindOperation {
|
|
|
19
20
|
this.maybeApplyAlias(builder);
|
|
20
21
|
this.relation.findQuery(builder, this.owner);
|
|
21
22
|
|
|
22
|
-
if (this.
|
|
23
|
-
this.
|
|
23
|
+
if (this.owner.isModels) {
|
|
24
|
+
this.warnOnMissingOwnerProps();
|
|
25
|
+
|
|
26
|
+
if (this.assignResultToOwner) {
|
|
27
|
+
this.selectMissingJoinColumns(builder);
|
|
28
|
+
}
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -44,6 +49,8 @@ class RelationFindOperation extends FindOperation {
|
|
|
44
49
|
arr.push(rel);
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
this.warnOnMissingRelatedProps(related);
|
|
53
|
+
|
|
47
54
|
for (let i = 0, l = owners.length; i < l; ++i) {
|
|
48
55
|
const own = owners[i];
|
|
49
56
|
const key = this.relation.ownerProp.propKey(own);
|
|
@@ -75,6 +82,43 @@ class RelationFindOperation extends FindOperation {
|
|
|
75
82
|
return super.onAfter3(builder, related);
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
// Owner models without the join columns, e.g. loaded with a partial
|
|
86
|
+
// `select()`, silently get no related models (#1832). Only `undefined`
|
|
87
|
+
// is reported, as `null` is a legit value for a missing relation.
|
|
88
|
+
// TODO: Consider throwing an error instead in objection 4.0.
|
|
89
|
+
warnOnMissingOwnerProps() {
|
|
90
|
+
const { ownerProp } = this.relation;
|
|
91
|
+
|
|
92
|
+
for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
|
|
93
|
+
if (this.owner.modelArray.some((owner) => ownerProp.getProp(owner, c) === undefined)) {
|
|
94
|
+
deprecate(
|
|
95
|
+
`Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
|
|
96
|
+
`some owner models are missing the join property "${ownerProp.propDescription(c)}", ` +
|
|
97
|
+
'so no related models are found for them. Select the column in the owner query.',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Related models without the join properties can't be assigned to their
|
|
104
|
+
// owners and are silently dropped (#2258). This happens for example when the
|
|
105
|
+
// relation mapping uses column names but the rows come back with different
|
|
106
|
+
// property names, e.g. snake_case mappings with `knexSnakeCaseMappers`.
|
|
107
|
+
warnOnMissingRelatedProps(related) {
|
|
108
|
+
const { relatedProp } = this.relation;
|
|
109
|
+
|
|
110
|
+
for (let c = 0, lc = relatedProp.size; c < lc; ++c) {
|
|
111
|
+
if (related.some((rel) => relatedProp.getProp(rel, c) === undefined)) {
|
|
112
|
+
deprecate(
|
|
113
|
+
`Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
|
|
114
|
+
`some related models are missing the join property "${relatedProp.propDescription(c)}", ` +
|
|
115
|
+
'so they are not assigned to any owner. Check that the relation mapping uses the ' +
|
|
116
|
+
'property names of the models, e.g. camelCase names when using knexSnakeCaseMappers.',
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
78
122
|
selectMissingJoinColumns(builder) {
|
|
79
123
|
const relatedProp = this.relation.relatedProp;
|
|
80
124
|
const addedSelects = [];
|
|
@@ -31,10 +31,11 @@ class RelationProperty {
|
|
|
31
31
|
//
|
|
32
32
|
// modelClassResolver must be a function that takes a table name
|
|
33
33
|
// and returns a model class.
|
|
34
|
-
constructor(references, modelClassResolver) {
|
|
34
|
+
constructor(references, modelClassResolver, { allowEmpty = false } = {}) {
|
|
35
35
|
const refs = createRefs(asArray(references));
|
|
36
36
|
const paths = createPaths(refs, modelClassResolver);
|
|
37
|
-
const modelClass =
|
|
37
|
+
const modelClass =
|
|
38
|
+
allowEmpty && refs.length === 0 ? modelClassResolver(null) : resolveModelClass(paths);
|
|
38
39
|
|
|
39
40
|
this._refs = refs.map((ref) => ref.model(modelClass));
|
|
40
41
|
this._modelClass = modelClass;
|
|
@@ -75,10 +75,39 @@ class ManyToManyRelation extends Relation {
|
|
|
75
75
|
return [];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
isJoinTableRelatedTable(builder) {
|
|
79
|
+
return builder.tableNameFor(this.joinTable) === builder.tableNameFor(this.relatedModelClass);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Returns the alias of the join table in `findQuery`. If the join table is
|
|
83
|
+
// also the related table, it needs an alias of its own (#1803).
|
|
84
|
+
joinTableAliasFor(builder) {
|
|
85
|
+
const joinTableRef = builder.tableRefFor(this.joinTable);
|
|
86
|
+
|
|
87
|
+
if (this.isJoinTableRelatedTable(builder)) {
|
|
88
|
+
return this.ownerModelClass.joinTableAlias(joinTableRef);
|
|
89
|
+
} else {
|
|
90
|
+
return joinTableRef;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The modify operations can't tell the join rows apart from the related rows
|
|
95
|
+
// if the join table is also the related table, so they would modify the
|
|
96
|
+
// wrong rows (#1803).
|
|
97
|
+
assertCanModifyJoinTable(builder, operationName) {
|
|
98
|
+
if (this.isJoinTableRelatedTable(builder)) {
|
|
99
|
+
throw this.createError(
|
|
100
|
+
`${operationName} is not supported when the join table is also the related table`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
findQuery(builder, owner) {
|
|
79
|
-
const joinTableOwnerRefs = this.joinTableOwnerProp.refs(builder);
|
|
80
106
|
const joinTable = builder.tableNameFor(this.joinTable);
|
|
81
|
-
const joinTableAlias =
|
|
107
|
+
const joinTableAlias = this.joinTableAliasFor(builder);
|
|
108
|
+
const joinTableOwnerRefs = this.joinTableOwnerProp
|
|
109
|
+
.refs(builder)
|
|
110
|
+
.map((ref) => ref.table(joinTableAlias));
|
|
82
111
|
|
|
83
112
|
let joinTableSelect = this.joinTableModelClass
|
|
84
113
|
.query()
|
|
@@ -92,7 +121,7 @@ class ManyToManyRelation extends Relation {
|
|
|
92
121
|
builder.join(joinTableSelect, (join) => {
|
|
93
122
|
this.relatedProp.forEach((i) => {
|
|
94
123
|
const relatedRef = this.relatedProp.ref(builder, i);
|
|
95
|
-
const joinTableRelatedRef = this.joinTableRelatedProp.ref(builder, i);
|
|
124
|
+
const joinTableRelatedRef = this.joinTableRelatedProp.ref(builder, i).table(joinTableAlias);
|
|
96
125
|
|
|
97
126
|
join.on(relatedRef, joinTableRelatedRef);
|
|
98
127
|
});
|
|
@@ -196,6 +225,8 @@ class ManyToManyRelation extends Relation {
|
|
|
196
225
|
}
|
|
197
226
|
|
|
198
227
|
unrelate(builder, owner) {
|
|
228
|
+
this.assertCanModifyJoinTable(builder, 'unrelate');
|
|
229
|
+
|
|
199
230
|
return new (getUnrelateOperationClass(builder))('unrelate', {
|
|
200
231
|
relation: this,
|
|
201
232
|
owner,
|
|
@@ -29,7 +29,7 @@ class ManyToManyFindOperation extends RelationFindOperation {
|
|
|
29
29
|
|
|
30
30
|
// Also select all extra columns.
|
|
31
31
|
for (const extra of this.relation.joinTableExtras) {
|
|
32
|
-
const joinTable =
|
|
32
|
+
const joinTable = this.relation.joinTableAliasFor(builder);
|
|
33
33
|
builder.select(`${joinTable}.${extra.joinTableCol} as ${extra.aliasCol}`);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -88,7 +88,9 @@ class ManyToManyFindOperation extends RelationFindOperation {
|
|
|
88
88
|
// We must select the owner join columns so that we know for which owner model the related
|
|
89
89
|
// models belong to after the requests.
|
|
90
90
|
joinTableOwnerProp.forEach((i) => {
|
|
91
|
-
const joinTableOwnerRef = joinTableOwnerProp
|
|
91
|
+
const joinTableOwnerRef = joinTableOwnerProp
|
|
92
|
+
.ref(builder, i)
|
|
93
|
+
.table(this.relation.joinTableAliasFor(builder));
|
|
92
94
|
const propName = relatedModelClass.columnNameToPropertyName(this.ownerJoinColumnAlias[i]);
|
|
93
95
|
|
|
94
96
|
builder.select(joinTableOwnerRef.as(this.ownerJoinColumnAlias[i]));
|
|
@@ -26,6 +26,10 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
if (this.hasExtraProps) {
|
|
30
|
+
this.relation.assertCanModifyJoinTable(builder, `${this.name} with extra properties`);
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
const res = super.onAdd(builder, args);
|
|
30
34
|
|
|
31
35
|
if (this.hasExtraProps) {
|
package/lib/utils/assert.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
function assertHasIdColumn(modelClass) {
|
|
4
|
+
if (modelClass.getIdColumnArray().length === 0) {
|
|
5
|
+
throw new Error(`model ${modelClass.name} has no idColumn`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
function assertHasId(model) {
|
|
10
|
+
assertHasIdColumn(model.constructor);
|
|
11
|
+
|
|
4
12
|
if (!model.$hasId()) {
|
|
5
13
|
const modelClass = model.constructor;
|
|
6
14
|
const ids = modelClass.getIdColumnArray().join(', ');
|
|
@@ -21,5 +29,6 @@ function assertIdNotUndefined(id, message) {
|
|
|
21
29
|
|
|
22
30
|
module.exports = {
|
|
23
31
|
assertHasId,
|
|
32
|
+
assertHasIdColumn,
|
|
24
33
|
assertIdNotUndefined,
|
|
25
34
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { wrapError, DBError, UniqueViolationError } = require('db-errors');
|
|
4
|
+
|
|
5
|
+
// db-errors only recognizes mssql unique key violations, but the same error
|
|
6
|
+
// number (2627) is also used for primary key violations (#2688).
|
|
7
|
+
const MSSQL_PRIMARY_KEY_REGEX =
|
|
8
|
+
/Violation of PRIMARY KEY constraint '(.+)'\. Cannot insert duplicate key in object '(.+)\.(.+)'\. The duplicate key value is \((.+)\)/;
|
|
9
|
+
|
|
10
|
+
function wrapDbError(err) {
|
|
11
|
+
const wrapped = wrapError(err);
|
|
12
|
+
|
|
13
|
+
if (wrapped.constructor === DBError && wrapped.client === 'mssql') {
|
|
14
|
+
const { nativeError } = wrapped;
|
|
15
|
+
const match =
|
|
16
|
+
nativeError.info &&
|
|
17
|
+
nativeError.info.number === 2627 &&
|
|
18
|
+
MSSQL_PRIMARY_KEY_REGEX.exec(nativeError.message);
|
|
19
|
+
|
|
20
|
+
if (match) {
|
|
21
|
+
return new UniqueViolationError({
|
|
22
|
+
nativeError,
|
|
23
|
+
client: 'mssql',
|
|
24
|
+
table: match[3],
|
|
25
|
+
schema: match[2],
|
|
26
|
+
constraint: match[1],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return wrapped;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
wrapDbError,
|
|
36
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "objection",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "An SQL-friendly ORM for Node.js",
|
|
5
5
|
"main": "lib/objection.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,11 +63,10 @@
|
|
|
63
63
|
"db-errors": "^0.2.3"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"knex": ">=
|
|
66
|
+
"knex": ">=2.3.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@types/node": "^22.15.3",
|
|
70
|
-
"bluebird": "^3.7.2",
|
|
71
70
|
"chai": "^4.3.10",
|
|
72
71
|
"chai-subset": "^1.6.0",
|
|
73
72
|
"eslint": "^8.54.0",
|
|
@@ -75,14 +74,12 @@
|
|
|
75
74
|
"eslint-plugin-prettier": "^5.3.1",
|
|
76
75
|
"expect.js": "^0.3.1",
|
|
77
76
|
"knex": "^3.3.0",
|
|
78
|
-
"lodash": "^4.18.1",
|
|
79
77
|
"mocha": "^11.2.2",
|
|
80
78
|
"mysql": "^2.18.1",
|
|
81
79
|
"pg": "^8.15.6",
|
|
82
80
|
"prettier": "3.9.9",
|
|
83
81
|
"sqlite3": "^5.1.7",
|
|
84
82
|
"typescript": ">=5.8.3",
|
|
85
|
-
"uuid": "^9.0.1",
|
|
86
83
|
"vitepress": "1.6.4"
|
|
87
84
|
}
|
|
88
85
|
}
|
|
@@ -590,7 +590,7 @@ declare namespace Objection {
|
|
|
590
590
|
}
|
|
591
591
|
|
|
592
592
|
interface FromMethod<QB extends AnyQueryBuilder> {
|
|
593
|
-
(table: TableRef<QB
|
|
593
|
+
(table: TableRef<QB>, options?: { only?: boolean }): QB;
|
|
594
594
|
}
|
|
595
595
|
|
|
596
596
|
interface FromRawMethod<QB extends AnyQueryBuilder> extends RawInterface<QB> {}
|