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.
Files changed (65) hide show
  1. package/lib/model/AjvValidator.js +123 -50
  2. package/lib/model/Model.js +2 -0
  3. package/lib/model/graph/ModelGraphNode.js +2 -1
  4. package/lib/model/modelJsonAttributes.js +20 -11
  5. package/lib/model/modelQueryProps.js +49 -5
  6. package/lib/model/modelSet.js +6 -8
  7. package/lib/model/modelToJson.js +17 -2
  8. package/lib/model/modelValidate.js +2 -1
  9. package/lib/model/modelValues.js +43 -50
  10. package/lib/queryBuilder/QueryBuilder.js +106 -38
  11. package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
  12. package/lib/queryBuilder/StaticHookArguments.js +4 -1
  13. package/lib/queryBuilder/graph/GraphData.js +39 -0
  14. package/lib/queryBuilder/graph/GraphFetcher.js +9 -1
  15. package/lib/queryBuilder/graph/GraphOptions.js +77 -38
  16. package/lib/queryBuilder/graph/GraphUpsert.js +98 -14
  17. package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
  18. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
  19. package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -1
  20. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +3 -2
  21. package/lib/queryBuilder/join/JoinResultParser.js +13 -8
  22. package/lib/queryBuilder/join/RelationJoiner.js +105 -12
  23. package/lib/queryBuilder/join/TableNode.js +6 -68
  24. package/lib/queryBuilder/join/utils.js +2 -2
  25. package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
  26. package/lib/queryBuilder/operations/DeleteOperation.js +13 -0
  27. package/lib/queryBuilder/operations/FirstOperation.js +5 -3
  28. package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
  29. package/lib/queryBuilder/operations/InsertOperation.js +2 -2
  30. package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
  31. package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
  32. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +4 -1
  33. package/lib/queryBuilder/operations/LimitPerParentOperation.js +152 -0
  34. package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
  35. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
  36. package/lib/queryBuilder/operations/UpdateOperation.js +54 -5
  37. package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
  38. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
  39. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
  40. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +3 -3
  41. package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
  42. package/lib/queryBuilder/operations/select/Selection.js +39 -16
  43. package/lib/queryBuilder/parsers/relationExpressionParser.js +5 -3
  44. package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +6 -2
  45. package/lib/queryBuilder/whereGroups.js +35 -40
  46. package/lib/queryBuilder/withoutOrder.js +33 -0
  47. package/lib/relations/Relation.js +3 -0
  48. package/lib/relations/RelationFindOperation.js +13 -5
  49. package/lib/relations/RelationOwner.js +2 -5
  50. package/lib/relations/RelationProperty.js +3 -14
  51. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
  52. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +31 -9
  53. package/lib/relations/manyToMany/ManyToManyRelation.js +7 -0
  54. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +7 -2
  55. package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
  56. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +123 -17
  57. package/lib/utils/identifierMapping.js +2 -2
  58. package/lib/utils/knexUtils.js +21 -0
  59. package/lib/utils/objectUtils.js +5 -0
  60. package/lib/utils/parseFieldExpression.js +9 -0
  61. package/lib/utils/warnings.js +26 -0
  62. package/lib/utils/wrapDbError.js +34 -20
  63. package/package.json +5 -4
  64. package/typings/objection/index.d.ts +31 -13
  65. 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 { isObject, once, cloneDeep: lodashCloneDeep, omit } = require('../utils/objectUtils');
8
- const { parseFieldExpression } = require('../utils/parseFieldExpression');
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 Ajv(
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'` that update
96
- // a single property inside a json column. Validate them against the nested
97
- // schema by converting them into nested objects like `{ meta: { b: value } }`.
98
- const fieldExpressions = options.patch ? nestFieldExpressions(json) : null;
99
- const validationJson = fieldExpressions ? fieldExpressions.json : json;
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
- validator.call(model, validationJson);
102
- const error = parseValidationError(validator.errors, modelClass, options, this.ajvOptions);
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 as query properties, keyed by their names.
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 path = fieldExpressionPath(key);
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
- return { json: nested, paths };
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, { json: nested, paths }) {
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
- function isFieldExpression(key) {
362
- return key.indexOf(':') !== -1;
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
- function fieldExpressionPath(key) {
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
- // Not a valid field expression, validate it as a top-level property.
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
- if (parsed.access.some((access) => access.type !== 'object' || /^-?\d+$/.test(access.ref))) {
378
- return null;
379
- }
380
-
381
- return [parsed.column, ...parsed.access.map((access) => String(access.ref))];
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 of the query properties of `model` (`raw()`, `val()`,
466
- // `ref()`, subqueries...) that are required by the schema but missing from
467
- // `json`, sorted, or `null` if there are none. Query properties are split from
468
- // the json before validation, but they do provide a value for the property.
469
- function getMissingQueryPropKeys(model, json, jsonSchema) {
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 keys = getQueryPropKeys(model).filter(
477
- (key) => required.includes(key) && !Object.prototype.hasOwnProperty.call(json, key),
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) {
@@ -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
- return this.idPath.join('.');
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 prop = props[propName];
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
- // Removes query properties from `json` and stores them into a hidden property
10
- // inside `model` so that they can be later merged back to `json`.
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
  };
@@ -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
- if (!json.$isObjectionModel) {
21
- // json can contain "query properties" like `raw` instances, query builders etc.
22
- // We take them out of `json` and store them to a hidden property $$queryProps
23
- // in the model instance for later use.
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
 
@@ -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 in opt.pick;
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
  }
@@ -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
- switch (props.length) {
24
- case 1:
25
- return propKey1(model, props);
26
- case 2:
27
- return propKey2(model, props);
28
- case 3:
29
- return propKey3(model, props);
30
- default:
31
- return propKeyN(model, props);
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 === null) {
104
- return 'null';
105
- } else if (value === undefined) {
106
- return 'undefined';
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
  };