objection 3.7.0 → 3.9.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 +123 -50
- package/lib/model/Model.js +2 -0
- package/lib/model/graph/ModelGraphNode.js +2 -1
- package/lib/model/modelJsonAttributes.js +20 -11
- package/lib/model/modelQueryProps.js +49 -5
- package/lib/model/modelSet.js +6 -8
- package/lib/model/modelToJson.js +17 -2
- package/lib/model/modelValidate.js +2 -1
- package/lib/model/modelValues.js +43 -50
- package/lib/queryBuilder/QueryBuilder.js +106 -38
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
- package/lib/queryBuilder/StaticHookArguments.js +4 -1
- package/lib/queryBuilder/graph/GraphData.js +39 -0
- package/lib/queryBuilder/graph/GraphFetcher.js +9 -1
- package/lib/queryBuilder/graph/GraphOptions.js +77 -38
- package/lib/queryBuilder/graph/GraphUpsert.js +98 -14
- package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
- package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
- package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -1
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +3 -2
- package/lib/queryBuilder/join/JoinResultParser.js +13 -8
- package/lib/queryBuilder/join/RelationJoiner.js +105 -12
- package/lib/queryBuilder/join/TableNode.js +6 -68
- package/lib/queryBuilder/join/utils.js +2 -2
- package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
- package/lib/queryBuilder/operations/DeleteOperation.js +13 -0
- package/lib/queryBuilder/operations/FirstOperation.js +5 -3
- package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
- package/lib/queryBuilder/operations/InsertOperation.js +2 -2
- package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +4 -1
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +152 -0
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
- package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
- package/lib/queryBuilder/operations/UpdateOperation.js +54 -5
- package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +3 -3
- package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
- package/lib/queryBuilder/operations/select/Selection.js +39 -16
- package/lib/queryBuilder/parsers/relationExpressionParser.js +5 -3
- package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +6 -2
- package/lib/queryBuilder/whereGroups.js +35 -40
- package/lib/queryBuilder/withoutOrder.js +33 -0
- package/lib/relations/Relation.js +3 -0
- package/lib/relations/RelationFindOperation.js +13 -5
- package/lib/relations/RelationOwner.js +2 -5
- package/lib/relations/RelationProperty.js +3 -14
- package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +31 -9
- package/lib/relations/manyToMany/ManyToManyRelation.js +7 -0
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +7 -2
- package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +123 -17
- package/lib/utils/identifierMapping.js +2 -2
- package/lib/utils/knexUtils.js +21 -0
- package/lib/utils/objectUtils.js +5 -0
- package/lib/utils/parseFieldExpression.js +9 -0
- package/lib/utils/warnings.js +26 -0
- package/lib/utils/wrapDbError.js +34 -20
- package/package.json +5 -4
- package/typings/objection/index.d.ts +31 -13
- package/lib/utils/deprecate.js +0 -21
|
@@ -4,8 +4,14 @@ const Ajv = require('ajv');
|
|
|
4
4
|
const addFormats = require('ajv-formats');
|
|
5
5
|
const { Validator } = require('./Validator');
|
|
6
6
|
const { ValidationErrorType } = require('../model/ValidationError');
|
|
7
|
-
const {
|
|
8
|
-
|
|
7
|
+
const {
|
|
8
|
+
isObject,
|
|
9
|
+
hasOwn,
|
|
10
|
+
once,
|
|
11
|
+
cloneDeep: lodashCloneDeep,
|
|
12
|
+
omit,
|
|
13
|
+
} = require('../utils/objectUtils');
|
|
14
|
+
const { parseFieldExpression, isFieldExpressionKey } = require('../utils/parseFieldExpression');
|
|
9
15
|
const { getQueryPropKeys } = require('./modelQueryProps');
|
|
10
16
|
|
|
11
17
|
class AjvValidator extends Validator {
|
|
@@ -28,7 +34,7 @@ class AjvValidator extends Validator {
|
|
|
28
34
|
|
|
29
35
|
// Create an instance that doesn't set default values. We need this one
|
|
30
36
|
// to validate `patch` objects (objects that have a subset of properties).
|
|
31
|
-
self.ajvNoDefaults = new
|
|
37
|
+
self.ajvNoDefaults = new PatchAjv(
|
|
32
38
|
Object.assign({}, self.ajvOptions, {
|
|
33
39
|
useDefaults: false,
|
|
34
40
|
}),
|
|
@@ -79,27 +85,60 @@ class AjvValidator extends Validator {
|
|
|
79
85
|
return json;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
const modelClass = model.constructor;
|
|
83
|
-
const validator = this.getValidator(
|
|
84
|
-
modelClass,
|
|
85
|
-
ctx.jsonSchema,
|
|
86
|
-
!!options.patch,
|
|
87
|
-
options.patch ? null : getMissingQueryPropKeys(model, json, ctx.jsonSchema),
|
|
88
|
-
);
|
|
89
|
-
|
|
90
88
|
// We need to clone the input json if we are about to set default values.
|
|
91
89
|
if (!options.mutable && !options.patch && setsDefaultValues(ctx.jsonSchema)) {
|
|
92
90
|
json = cloneDeep(json);
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
// Patch objects can contain field expression keys like `'meta:b'`
|
|
96
|
-
// a single property inside a json column. Validate them against
|
|
97
|
-
// schema by converting them into nested objects like
|
|
98
|
-
|
|
99
|
-
const
|
|
93
|
+
// Patch and update objects can contain field expression keys like `'meta:b'`
|
|
94
|
+
// that update a single property inside a json column. Validate them against
|
|
95
|
+
// the nested schema by converting them into nested objects like
|
|
96
|
+
// `{ meta: { b: value } }`.
|
|
97
|
+
const queryPropKeys = getQueryPropKeys(model);
|
|
98
|
+
const fieldExpressions = nestFieldExpressions(json, queryPropKeys, ctx.jsonSchema);
|
|
99
|
+
let validationJson = fieldExpressions ? fieldExpressions.json : json;
|
|
100
|
+
|
|
101
|
+
const modelClass = model.constructor;
|
|
102
|
+
const validate = (json, isPatchObject, givenKeys = []) => {
|
|
103
|
+
const validator = this.getValidator(
|
|
104
|
+
modelClass,
|
|
105
|
+
ctx.jsonSchema,
|
|
106
|
+
isPatchObject,
|
|
107
|
+
isPatchObject ? null : getRequiredKeysGivenOutside(json, ctx.jsonSchema, givenKeys),
|
|
108
|
+
);
|
|
109
|
+
validator.call(model, json);
|
|
110
|
+
return validator.errors || [];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let errors;
|
|
114
|
+
|
|
115
|
+
if (options.patch) {
|
|
116
|
+
errors = validate(validationJson, true);
|
|
117
|
+
} else if (fieldExpressions && fieldExpressions.columns.length > 0) {
|
|
118
|
+
// Field expressions only write parts of their json columns. Validate these
|
|
119
|
+
// parts like patches, and the rest normally, with the columns counted as
|
|
120
|
+
// present like the query properties.
|
|
121
|
+
const { columns } = fieldExpressions;
|
|
122
|
+
const columnsJson = Object.fromEntries(
|
|
123
|
+
columns
|
|
124
|
+
.filter((column) => hasOwn(validationJson, column))
|
|
125
|
+
.map((column) => [column, validationJson[column]]),
|
|
126
|
+
);
|
|
127
|
+
const restJson = omit(validationJson, columns);
|
|
100
128
|
|
|
101
|
-
|
|
102
|
-
|
|
129
|
+
errors = [
|
|
130
|
+
...validate(columnsJson, true),
|
|
131
|
+
...validate(restJson, false, [...queryPropKeys, ...columns]),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
// Combine the parts again, without the defaults that Ajv set for the
|
|
135
|
+
// columns in `restJson`.
|
|
136
|
+
validationJson = Object.assign(restJson, columnsJson);
|
|
137
|
+
} else {
|
|
138
|
+
errors = validate(validationJson, false, queryPropKeys);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const error = parseValidationError(errors, modelClass, options, this.ajvOptions);
|
|
103
142
|
|
|
104
143
|
if (error) {
|
|
105
144
|
throw error;
|
|
@@ -107,7 +146,7 @@ class AjvValidator extends Validator {
|
|
|
107
146
|
|
|
108
147
|
if (fieldExpressions) {
|
|
109
148
|
// Copy back the changes made by Ajv options like `coerceTypes` and `removeAdditional`.
|
|
110
|
-
unnestFieldExpressions(json, fieldExpressions);
|
|
149
|
+
unnestFieldExpressions(json, validationJson, fieldExpressions);
|
|
111
150
|
}
|
|
112
151
|
|
|
113
152
|
return json;
|
|
@@ -148,7 +187,8 @@ class AjvValidator extends Validator {
|
|
|
148
187
|
normalValidator: null,
|
|
149
188
|
|
|
150
189
|
// Validators created for the schema object without the `required`
|
|
151
|
-
// properties that are given
|
|
190
|
+
// properties that are given outside the json, as query properties or
|
|
191
|
+
// json columns written by field expressions, keyed by their names.
|
|
152
192
|
queryPropValidators: null,
|
|
153
193
|
};
|
|
154
194
|
|
|
@@ -234,6 +274,20 @@ class AjvValidator extends Validator {
|
|
|
234
274
|
}
|
|
235
275
|
}
|
|
236
276
|
|
|
277
|
+
// Patches don't need to contain the `required` properties, also not those of
|
|
278
|
+
// the schemas that model schemas reference by `$id`, like the ones added in
|
|
279
|
+
// `onCreateAjv()`. Strip them from all schemas added to the patch instance:
|
|
280
|
+
class PatchAjv extends Ajv {
|
|
281
|
+
addSchema(schema, key, meta, ...args) {
|
|
282
|
+
return super.addSchema(
|
|
283
|
+
meta || Array.isArray(schema) ? schema : jsonSchemaWithoutRequired(schema),
|
|
284
|
+
key,
|
|
285
|
+
meta,
|
|
286
|
+
...args,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
237
291
|
function parseValidationError(errors, modelClass, options, ajvOptions) {
|
|
238
292
|
if (!errors) {
|
|
239
293
|
return null;
|
|
@@ -298,10 +352,11 @@ function parseValidationError(errors, modelClass, options, ajvOptions) {
|
|
|
298
352
|
});
|
|
299
353
|
}
|
|
300
354
|
|
|
301
|
-
function nestFieldExpressions(json) {
|
|
355
|
+
function nestFieldExpressions(json, queryPropKeys, jsonSchema) {
|
|
356
|
+
const isFieldExpression = (key) => isFieldExpressionKey(key, jsonSchema.properties);
|
|
302
357
|
const keys = Object.keys(json);
|
|
303
358
|
|
|
304
|
-
if (!keys.some(isFieldExpression)) {
|
|
359
|
+
if (!keys.some(isFieldExpression) && !queryPropKeys.some(isFieldExpression)) {
|
|
305
360
|
return null;
|
|
306
361
|
}
|
|
307
362
|
|
|
@@ -310,6 +365,15 @@ function nestFieldExpressions(json) {
|
|
|
310
365
|
// The objects created here for the nested paths. Other objects are values
|
|
311
366
|
// of the input json and must not be modified.
|
|
312
367
|
const createdObjects = new Set();
|
|
368
|
+
// The json columns that are only written partially, by field expressions in
|
|
369
|
+
// the json or the query properties.
|
|
370
|
+
const columns = new Set();
|
|
371
|
+
|
|
372
|
+
const addColumn = (expression) => {
|
|
373
|
+
if (expression && !hasOwn(json, expression.column)) {
|
|
374
|
+
columns.add(expression.column);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
313
377
|
|
|
314
378
|
for (const key of keys) {
|
|
315
379
|
if (!isFieldExpression(key)) {
|
|
@@ -320,7 +384,10 @@ function nestFieldExpressions(json) {
|
|
|
320
384
|
|
|
321
385
|
for (const key of keys) {
|
|
322
386
|
if (isFieldExpression(key)) {
|
|
323
|
-
const
|
|
387
|
+
const expression = parseFieldExpressionKey(key);
|
|
388
|
+
// Not a valid field expression, validate it as a top-level property:
|
|
389
|
+
const path = expression ? expression.path : [key];
|
|
390
|
+
addColumn(expression);
|
|
324
391
|
|
|
325
392
|
// Field expressions with array access, and those that conflict with other
|
|
326
393
|
// keys of the patch, can't be validated without the current value of the
|
|
@@ -331,10 +398,16 @@ function nestFieldExpressions(json) {
|
|
|
331
398
|
}
|
|
332
399
|
}
|
|
333
400
|
|
|
334
|
-
|
|
401
|
+
for (const key of queryPropKeys) {
|
|
402
|
+
if (isFieldExpression(key)) {
|
|
403
|
+
addColumn(parseFieldExpressionKey(key));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return { json: nested, paths, columns: [...columns] };
|
|
335
408
|
}
|
|
336
409
|
|
|
337
|
-
function unnestFieldExpressions(json,
|
|
410
|
+
function unnestFieldExpressions(json, nested, { paths, columns }) {
|
|
338
411
|
for (const [key, path] of paths) {
|
|
339
412
|
let value = nested;
|
|
340
413
|
let found = true;
|
|
@@ -356,29 +429,35 @@ function unnestFieldExpressions(json, { json: nested, paths }) {
|
|
|
356
429
|
json[key] = value;
|
|
357
430
|
}
|
|
358
431
|
}
|
|
359
|
-
}
|
|
360
432
|
|
|
361
|
-
|
|
362
|
-
|
|
433
|
+
// Copy over the default values that Ajv set for missing properties, but not
|
|
434
|
+
// the json columns written by the field expressions.
|
|
435
|
+
for (const key of Object.keys(nested)) {
|
|
436
|
+
if (!paths.has(key) && !columns.includes(key)) {
|
|
437
|
+
json[key] = nested[key];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
363
440
|
}
|
|
364
441
|
|
|
365
|
-
|
|
442
|
+
// Parse a field expression key into its json column and nested path, with the
|
|
443
|
+
// path set to `null` if the key accesses array elements.
|
|
444
|
+
function parseFieldExpressionKey(key) {
|
|
366
445
|
let parsed;
|
|
367
446
|
|
|
368
447
|
try {
|
|
369
448
|
parsed = parseFieldExpression(key);
|
|
370
449
|
} catch (err) {
|
|
371
|
-
|
|
372
|
-
return [key];
|
|
450
|
+
return null;
|
|
373
451
|
}
|
|
374
452
|
|
|
453
|
+
const { column, access } = parsed;
|
|
375
454
|
// Numeric keys like the `0` in `tags:0` also access array elements when the
|
|
376
455
|
// json value is an array, so treat them like `[0]`.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
456
|
+
const isArrayAccess = access.some(({ type, ref }) => type !== 'object' || /^-?\d+$/.test(ref));
|
|
457
|
+
return {
|
|
458
|
+
column,
|
|
459
|
+
path: isArrayAccess ? null : [column, ...access.map(({ ref }) => String(ref))],
|
|
460
|
+
};
|
|
382
461
|
}
|
|
383
462
|
|
|
384
463
|
function setPath(obj, path, value, createdObjects) {
|
|
@@ -406,10 +485,6 @@ function setPath(obj, path, value, createdObjects) {
|
|
|
406
485
|
return true;
|
|
407
486
|
}
|
|
408
487
|
|
|
409
|
-
function hasOwn(obj, prop) {
|
|
410
|
-
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
488
|
function cloneDeep(obj) {
|
|
414
489
|
if (isObject(obj) && obj.$isObjectionModel) {
|
|
415
490
|
return obj.$clone();
|
|
@@ -462,22 +537,20 @@ function objectHasDefaults(obj) {
|
|
|
462
537
|
return false;
|
|
463
538
|
}
|
|
464
539
|
|
|
465
|
-
// Returns the keys
|
|
466
|
-
// `
|
|
467
|
-
// `
|
|
468
|
-
//
|
|
469
|
-
|
|
540
|
+
// Returns the `keys` that are required by the schema but given outside of
|
|
541
|
+
// `json`, sorted, or `null` if there are none. Query properties (`raw()`,
|
|
542
|
+
// `val()`, `ref()`, subqueries...) are split from the json before validation,
|
|
543
|
+
// but they do provide a value for the property, and so do field expressions
|
|
544
|
+
// for their json columns.
|
|
545
|
+
function getRequiredKeysGivenOutside(json, jsonSchema, keys) {
|
|
470
546
|
const required = jsonSchema.required;
|
|
471
547
|
|
|
472
548
|
if (!Array.isArray(required) || required.length === 0) {
|
|
473
549
|
return null;
|
|
474
550
|
}
|
|
475
551
|
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
return keys.length > 0 ? keys.sort() : null;
|
|
552
|
+
const givenKeys = required.filter((key) => keys.includes(key) && !hasOwn(json, key));
|
|
553
|
+
return givenKeys.length > 0 ? givenKeys.sort() : null;
|
|
481
554
|
}
|
|
482
555
|
|
|
483
556
|
function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
|
package/lib/model/Model.js
CHANGED
|
@@ -11,6 +11,7 @@ const { toJson, toDatabaseJson } = require('./modelToJson');
|
|
|
11
11
|
const { values, propKey, hasProps } = require('./modelValues');
|
|
12
12
|
const { defineNonEnumerableProperty } = require('./modelUtils');
|
|
13
13
|
const { parseRelationsIntoModelInstances } = require('./modelParseRelations');
|
|
14
|
+
const { removeOverriddenQueryProps } = require('./modelQueryProps');
|
|
14
15
|
const { fetchTableMetadata, tableMetadata } = require('./modelTableMetadata');
|
|
15
16
|
const { asArray, isFunction, isString, asSingle } = require('../utils/objectUtils');
|
|
16
17
|
const { setJson, setFast, setRelated, appendRelated, setDatabaseJson } = require('./modelSet');
|
|
@@ -576,6 +577,7 @@ class Model {
|
|
|
576
577
|
}
|
|
577
578
|
|
|
578
579
|
if (model instanceof modelClass) {
|
|
580
|
+
removeOverriddenQueryProps(model);
|
|
579
581
|
return parseRelationsIntoModelInstances(model, model, options);
|
|
580
582
|
} else {
|
|
581
583
|
return modelClass.fromJson(model, options);
|
|
@@ -240,7 +240,8 @@ class ModelGraphNode {
|
|
|
240
240
|
const idPath = this.idPath;
|
|
241
241
|
|
|
242
242
|
if (idPath) {
|
|
243
|
-
|
|
243
|
+
// JSON keeps the ids and relation names apart, whatever they contain.
|
|
244
|
+
return JSON.stringify(idPath);
|
|
244
245
|
} else {
|
|
245
246
|
return null;
|
|
246
247
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { asArray, isObject, flatten, isString } = require('../utils/objectUtils');
|
|
4
|
+
const { isQueryProp } = require('./modelQueryProps');
|
|
4
5
|
|
|
5
6
|
function parseJsonAttributes(json, modelClass) {
|
|
6
7
|
const jsonAttr = modelClass.getJsonAttributes();
|
|
@@ -31,11 +32,12 @@ function formatJsonAttributes(json, modelClass) {
|
|
|
31
32
|
|
|
32
33
|
if (jsonAttr.length) {
|
|
33
34
|
// All database clients want JSON columns as strings. Do the conversion here.
|
|
35
|
+
// Leave query properties set in hooks like `$beforeUpdate()` as they are.
|
|
34
36
|
for (let i = 0, l = jsonAttr.length; i < l; ++i) {
|
|
35
37
|
const attr = jsonAttr[i];
|
|
36
38
|
const value = json[attr];
|
|
37
39
|
|
|
38
|
-
if (value != null) {
|
|
40
|
+
if (value != null && !isQueryProp(value)) {
|
|
39
41
|
json[attr] = JSON.stringify(value);
|
|
40
42
|
}
|
|
41
43
|
}
|
|
@@ -57,16 +59,7 @@ function getJsonAttributes(modelClass) {
|
|
|
57
59
|
const props = modelClass.getJsonSchema().properties || {};
|
|
58
60
|
|
|
59
61
|
for (const propName of Object.keys(props)) {
|
|
60
|
-
const
|
|
61
|
-
let types = asArray(prop.type).filter((it) => !!it);
|
|
62
|
-
|
|
63
|
-
if (types.length === 0 && Array.isArray(prop.anyOf)) {
|
|
64
|
-
types = flatten(prop.anyOf.map((it) => it.type));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (types.length === 0 && Array.isArray(prop.oneOf)) {
|
|
68
|
-
types = flatten(prop.oneOf.map((it) => it.type));
|
|
69
|
-
}
|
|
62
|
+
const types = getPropertyTypes(props[propName]);
|
|
70
63
|
|
|
71
64
|
if (types.indexOf('object') !== -1 || types.indexOf('array') !== -1) {
|
|
72
65
|
jsonAttributes.push(propName);
|
|
@@ -77,6 +70,21 @@ function getJsonAttributes(modelClass) {
|
|
|
77
70
|
return jsonAttributes;
|
|
78
71
|
}
|
|
79
72
|
|
|
73
|
+
// The types of a property schema, from `type` or else from `anyOf` / `oneOf`.
|
|
74
|
+
function getPropertyTypes(prop) {
|
|
75
|
+
let types = asArray(prop.type).filter((it) => !!it);
|
|
76
|
+
|
|
77
|
+
if (types.length === 0 && Array.isArray(prop.anyOf)) {
|
|
78
|
+
types = flatten(prop.anyOf.map((it) => it.type));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (types.length === 0 && Array.isArray(prop.oneOf)) {
|
|
82
|
+
types = flatten(prop.oneOf.map((it) => it.type));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return types;
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
function tryParseJson(maybeJsonStr) {
|
|
81
89
|
try {
|
|
82
90
|
return JSON.parse(maybeJsonStr);
|
|
@@ -89,4 +97,5 @@ module.exports = {
|
|
|
89
97
|
parseJsonAttributes,
|
|
90
98
|
formatJsonAttributes,
|
|
91
99
|
getJsonAttributes,
|
|
100
|
+
getPropertyTypes,
|
|
92
101
|
};
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { isObject, isFunction } = require('../utils/objectUtils');
|
|
3
|
+
const { isObject, isFunction, omit } = require('../utils/objectUtils');
|
|
4
4
|
const { defineNonEnumerableProperty } = require('./modelUtils');
|
|
5
5
|
const { isKnexRaw, isKnexQueryBuilder } = require('../utils/knexUtils');
|
|
6
|
+
const { parseFieldExpression, isFieldExpressionKey } = require('../utils/parseFieldExpression');
|
|
6
7
|
|
|
7
8
|
const QUERY_PROPS_PROPERTY = '$$queryProps';
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
10
|
+
// Moves the query properties of `json` into a hidden property of `model` so
|
|
11
|
+
// that they can be later merged back to its database json. A key holds either
|
|
12
|
+
// a value or a query property, so the keys given in `json` replace the ones
|
|
13
|
+
// that the model holds as the other kind.
|
|
11
14
|
function splitQueryProps(model, json) {
|
|
12
15
|
const keys = Object.keys(json);
|
|
16
|
+
const oldQueryProps = model[QUERY_PROPS_PROPERTY];
|
|
13
17
|
|
|
14
|
-
if (hasQueryProps(json, keys)) {
|
|
15
|
-
const queryProps = {};
|
|
18
|
+
if (!json.$isObjectionModel && hasQueryProps(json, keys)) {
|
|
19
|
+
const queryProps = Object.assign({}, oldQueryProps);
|
|
16
20
|
const modelProps = {};
|
|
17
21
|
|
|
18
22
|
for (let i = 0, l = keys.length; i < l; ++i) {
|
|
@@ -21,8 +25,10 @@ function splitQueryProps(model, json) {
|
|
|
21
25
|
|
|
22
26
|
if (isQueryProp(value)) {
|
|
23
27
|
queryProps[key] = value;
|
|
28
|
+
delete model[key];
|
|
24
29
|
} else {
|
|
25
30
|
modelProps[key] = value;
|
|
31
|
+
delete queryProps[key];
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
|
|
@@ -30,10 +36,45 @@ function splitQueryProps(model, json) {
|
|
|
30
36
|
|
|
31
37
|
return modelProps;
|
|
32
38
|
} else {
|
|
39
|
+
if (oldQueryProps) {
|
|
40
|
+
defineNonEnumerableProperty(model, QUERY_PROPS_PROPERTY, omit(oldQueryProps, keys));
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
return json;
|
|
34
44
|
}
|
|
35
45
|
}
|
|
36
46
|
|
|
47
|
+
// Omits the values that parsing and validation add to `json` for the keys that
|
|
48
|
+
// `model` holds query properties for, like Ajv defaults.
|
|
49
|
+
function omitQueryProps(model, json) {
|
|
50
|
+
const keys = getQueryPropKeys(model);
|
|
51
|
+
return keys.length > 0 ? omit(json, keys) : json;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Removes the query properties of the keys that were given a value since, by an
|
|
55
|
+
// assignment, `$set()` or a write that fetched the value from the database. We
|
|
56
|
+
// do this when the model is passed to a write, and not when its database json
|
|
57
|
+
// is built, so that the values set by its hooks don't override them.
|
|
58
|
+
function removeOverriddenQueryProps(model) {
|
|
59
|
+
const queryProps = model[QUERY_PROPS_PROPERTY];
|
|
60
|
+
|
|
61
|
+
if (queryProps) {
|
|
62
|
+
const jsonSchema = model.constructor.getJsonSchema();
|
|
63
|
+
const properties = jsonSchema && jsonSchema.properties;
|
|
64
|
+
// The json column of a field expression key like `data:a` overrides it too:
|
|
65
|
+
const keys = Object.keys(queryProps).filter(
|
|
66
|
+
(key) =>
|
|
67
|
+
model[key] !== undefined ||
|
|
68
|
+
(isFieldExpressionKey(key, properties) &&
|
|
69
|
+
model[parseFieldExpression(key).column] !== undefined),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (keys.length > 0) {
|
|
73
|
+
defineNonEnumerableProperty(model, QUERY_PROPS_PROPERTY, omit(queryProps, keys));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
37
78
|
function hasQueryProps(json, keys) {
|
|
38
79
|
for (let i = 0, l = keys.length; i < l; ++i) {
|
|
39
80
|
if (isQueryProp(json[keys[i]])) {
|
|
@@ -149,6 +190,9 @@ function isKnexRawConvertable(queryProp) {
|
|
|
149
190
|
|
|
150
191
|
module.exports = {
|
|
151
192
|
splitQueryProps,
|
|
193
|
+
omitQueryProps,
|
|
194
|
+
removeOverriddenQueryProps,
|
|
152
195
|
mergeQueryProps,
|
|
153
196
|
getQueryPropKeys,
|
|
197
|
+
isQueryProp,
|
|
154
198
|
};
|
package/lib/model/modelSet.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { isInternalProp } = require('../utils/internalPropUtils');
|
|
4
|
-
const { splitQueryProps } = require('./modelQueryProps');
|
|
4
|
+
const { splitQueryProps, omitQueryProps } = require('./modelQueryProps');
|
|
5
5
|
const { isFunction, isString, isPlainObject } = require('../utils/objectUtils');
|
|
6
6
|
const { parseRelationsIntoModelInstances } = require('./modelParseRelations');
|
|
7
7
|
|
|
@@ -17,15 +17,13 @@ function setJson(model, json, options) {
|
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
json = splitQueryProps(model, json);
|
|
25
|
-
}
|
|
26
|
-
|
|
20
|
+
// json can contain "query properties" like `raw` instances, query builders etc.
|
|
21
|
+
// We take them out of `json` and store them to a hidden property $$queryProps
|
|
22
|
+
// in the model instance for later use.
|
|
23
|
+
json = splitQueryProps(model, json);
|
|
27
24
|
json = model.$parseJson(json, options);
|
|
28
25
|
json = model.$validate(json, options);
|
|
26
|
+
json = omitQueryProps(model, json);
|
|
29
27
|
|
|
30
28
|
model.$set(json);
|
|
31
29
|
|
package/lib/model/modelToJson.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { isInternalProp } = require('../utils/internalPropUtils');
|
|
4
4
|
const { mergeQueryProps } = require('./modelQueryProps');
|
|
5
|
-
const { isObject, cloneDeep, isFunction } = require('../utils/objectUtils');
|
|
5
|
+
const { isObject, hasOwn, cloneDeep, isFunction } = require('../utils/objectUtils');
|
|
6
|
+
const { parseFieldExpression, isFieldExpressionKey } = require('../utils/parseFieldExpression');
|
|
6
7
|
const EMPTY_ARRAY = [];
|
|
7
8
|
|
|
8
9
|
function toJson(model, optIn) {
|
|
@@ -134,7 +135,21 @@ function shouldOmit(opt, key) {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
function shouldPick(opt, key) {
|
|
137
|
-
return opt.pick === null || key
|
|
138
|
+
return opt.pick === null || hasOwn(opt.pick, key) || isPickedFieldExpression(opt.pick, key);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Pick field expressions like `'meta:a'` by their json column.
|
|
142
|
+
function isPickedFieldExpression(pick, key) {
|
|
143
|
+
if (isFieldExpressionKey(key, pick)) {
|
|
144
|
+
try {
|
|
145
|
+
return hasOwn(pick, parseFieldExpression(key).column);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// Not a valid field expression.
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
function assignVirtualAttributes(json, model, vAttr, opt) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { clone } = require('./modelClone');
|
|
4
|
+
const { omitQueryProps } = require('./modelQueryProps');
|
|
4
5
|
|
|
5
6
|
function validate(model, json, options = {}) {
|
|
6
7
|
json = json || model;
|
|
@@ -37,7 +38,7 @@ function validate(model, json, options = {}) {
|
|
|
37
38
|
|
|
38
39
|
if (validatingModelInstance) {
|
|
39
40
|
// If we cloned `json`, we need to copy the possible default values.
|
|
40
|
-
return inputJson.$set(json);
|
|
41
|
+
return inputJson.$set(omitQueryProps(inputJson, json));
|
|
41
42
|
} else {
|
|
42
43
|
return json;
|
|
43
44
|
}
|
package/lib/model/modelValues.js
CHANGED
|
@@ -19,16 +19,33 @@ function values(model, args) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Turn the props of a model into a key to look up models by.
|
|
22
23
|
function propKey(model, props) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
if (props.length === 1) {
|
|
25
|
+
return PROP_KEY_PREFIX + propToStr(model[props[0]]);
|
|
26
|
+
} else {
|
|
27
|
+
let key = PROP_KEY_PREFIX + compositePropToStr(model[props[0]]);
|
|
28
|
+
|
|
29
|
+
for (let i = 1, l = props.length; i < l; ++i) {
|
|
30
|
+
key += ',' + compositePropToStr(model[props[i]]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Same as `propKey()`, for values that were already collected.
|
|
38
|
+
function valuesKey(values) {
|
|
39
|
+
if (values.length === 1) {
|
|
40
|
+
return PROP_KEY_PREFIX + propToStr(values[0]);
|
|
41
|
+
} else {
|
|
42
|
+
let key = PROP_KEY_PREFIX + compositePropToStr(values[0]);
|
|
43
|
+
|
|
44
|
+
for (let i = 1, l = values.length; i < l; ++i) {
|
|
45
|
+
key += ',' + compositePropToStr(values[i]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return key;
|
|
32
49
|
}
|
|
33
50
|
}
|
|
34
51
|
|
|
@@ -66,50 +83,27 @@ function valuesN(model, args) {
|
|
|
66
83
|
return ret;
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
function propKey1(model, props) {
|
|
70
|
-
return PROP_KEY_PREFIX + propToStr(model[props[0]]);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function propKey2(model, props) {
|
|
74
|
-
return PROP_KEY_PREFIX + propToStr(model[props[0]]) + ',' + propToStr(model[props[1]]);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function propKey3(model, props) {
|
|
78
|
-
return (
|
|
79
|
-
PROP_KEY_PREFIX +
|
|
80
|
-
propToStr(model[props[0]]) +
|
|
81
|
-
',' +
|
|
82
|
-
propToStr(model[props[1]]) +
|
|
83
|
-
',' +
|
|
84
|
-
propToStr(model[props[2]])
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function propKeyN(model, props) {
|
|
89
|
-
let key = PROP_KEY_PREFIX;
|
|
90
|
-
|
|
91
|
-
for (let i = 0, l = props.length; i < l; ++i) {
|
|
92
|
-
key += propToStr(model[props[i]]);
|
|
93
|
-
|
|
94
|
-
if (i < l - 1) {
|
|
95
|
-
key += ',';
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return key;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
86
|
function propToStr(value) {
|
|
103
|
-
if (value
|
|
104
|
-
return
|
|
105
|
-
} else if (value
|
|
106
|
-
return
|
|
87
|
+
if (!isObject(value)) {
|
|
88
|
+
return `${value}`;
|
|
89
|
+
} else if (value instanceof Date) {
|
|
90
|
+
// Key dates by their time, as some drivers return them as numbers.
|
|
91
|
+
return `${value.getTime()}`;
|
|
107
92
|
} else if (Buffer.isBuffer(value)) {
|
|
108
93
|
return value.toString('hex');
|
|
109
|
-
} else if (isObject(value)) {
|
|
110
|
-
return JSON.stringify(value);
|
|
111
94
|
} else {
|
|
95
|
+
return JSON.stringify(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Escape the separators in composite keys, since strings can contain them.
|
|
100
|
+
// Numbers can't, so we don't need to check them.
|
|
101
|
+
function compositePropToStr(value) {
|
|
102
|
+
if (typeof value === 'number') {
|
|
112
103
|
return `${value}`;
|
|
104
|
+
} else {
|
|
105
|
+
const str = propToStr(value);
|
|
106
|
+
return str.includes(',') || str.includes('\\') ? str.replace(/[,\\]/g, '\\$&') : str;
|
|
113
107
|
}
|
|
114
108
|
}
|
|
115
109
|
|
|
@@ -118,9 +112,8 @@ function isNullOrUndefined(val) {
|
|
|
118
112
|
}
|
|
119
113
|
|
|
120
114
|
module.exports = {
|
|
121
|
-
PROP_KEY_PREFIX,
|
|
122
|
-
propToStr,
|
|
123
115
|
values,
|
|
124
116
|
hasProps,
|
|
125
117
|
propKey,
|
|
118
|
+
valuesKey,
|
|
126
119
|
};
|