objection 3.8.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/lib/model/AjvValidator.js +22 -6
  2. package/lib/model/Model.js +2 -0
  3. package/lib/model/graph/ModelGraphNode.js +2 -16
  4. package/lib/model/modelJsonAttributes.js +20 -11
  5. package/lib/model/modelQueryProps.js +78 -6
  6. package/lib/model/modelSet.js +6 -8
  7. package/lib/model/modelToJson.js +9 -2
  8. package/lib/model/modelUtils.js +1 -1
  9. package/lib/model/modelValidate.js +2 -1
  10. package/lib/model/modelValues.js +43 -50
  11. package/lib/queryBuilder/QueryBuilder.js +17 -7
  12. package/lib/queryBuilder/graph/GraphData.js +39 -0
  13. package/lib/queryBuilder/graph/GraphFetcher.js +7 -1
  14. package/lib/queryBuilder/graph/GraphOptions.js +66 -12
  15. package/lib/queryBuilder/graph/GraphUpsert.js +92 -10
  16. package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
  17. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
  18. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +7 -6
  19. package/lib/queryBuilder/graph/insert/GraphInsert.js +66 -22
  20. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -13
  21. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +16 -3
  22. package/lib/queryBuilder/join/JoinResultParser.js +13 -8
  23. package/lib/queryBuilder/join/RelationJoiner.js +3 -2
  24. package/lib/queryBuilder/join/TableNode.js +6 -68
  25. package/lib/queryBuilder/operations/DeleteOperation.js +20 -0
  26. package/lib/queryBuilder/operations/InsertOperation.js +4 -2
  27. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -0
  28. package/lib/queryBuilder/operations/LimitPerParentOperation.js +22 -15
  29. package/lib/queryBuilder/operations/UpdateOperation.js +40 -3
  30. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +1 -1
  31. package/lib/queryBuilder/operations/select/Selection.js +25 -3
  32. package/lib/relations/RelationFindOperation.js +7 -4
  33. package/lib/relations/RelationOwner.js +2 -5
  34. package/lib/relations/RelationProperty.js +3 -14
  35. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
  36. package/lib/relations/hasMany/HasManyInsertOperation.js +4 -0
  37. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +1 -1
  38. package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
  39. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +1 -16
  40. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +80 -21
  41. package/lib/utils/knexUtils.js +7 -0
  42. package/lib/utils/objectUtils.js +5 -0
  43. package/lib/utils/parseFieldExpression.js +17 -5
  44. package/lib/utils/promiseUtils/map.js +24 -7
  45. package/lib/utils/wrapDbError.js +14 -0
  46. package/package.json +1 -1
  47. package/typings/objection/index.d.ts +21 -11
@@ -4,7 +4,13 @@ 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');
7
+ const {
8
+ isObject,
9
+ hasOwn,
10
+ once,
11
+ cloneDeep: lodashCloneDeep,
12
+ omit,
13
+ } = require('../utils/objectUtils');
8
14
  const { parseFieldExpression, isFieldExpressionKey } = require('../utils/parseFieldExpression');
9
15
  const { getQueryPropKeys } = require('./modelQueryProps');
10
16
 
@@ -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
  }),
@@ -268,6 +274,20 @@ class AjvValidator extends Validator {
268
274
  }
269
275
  }
270
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
+
271
291
  function parseValidationError(errors, modelClass, options, ajvOptions) {
272
292
  if (!errors) {
273
293
  return null;
@@ -465,10 +485,6 @@ function setPath(obj, path, value, createdObjects) {
465
485
  return true;
466
486
  }
467
487
 
468
- function hasOwn(obj, prop) {
469
- return Object.prototype.hasOwnProperty.call(obj, prop);
470
- }
471
-
472
488
  function cloneDeep(obj) {
473
489
  if (isObject(obj) && obj.$isObjectionModel) {
474
490
  return obj.$clone();
@@ -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);
@@ -143,21 +143,6 @@ class ModelGraphNode {
143
143
  return null;
144
144
  }
145
145
 
146
- /**
147
- * Returns all nodes that are references to this node.
148
- */
149
- get referencingNodes() {
150
- const nodes = [];
151
-
152
- for (const edge of this.refEdges) {
153
- if (edge.refType === ModelGraphEdge.ReferenceType.Object && edge.isRelatedNode(this)) {
154
- nodes.push(edge.ownerNode);
155
- }
156
- }
157
-
158
- return nodes;
159
- }
160
-
161
146
  get descendantRelationNodes() {
162
147
  return this._collectDescendantRelationNodes([]);
163
148
  }
@@ -240,7 +225,8 @@ class ModelGraphNode {
240
225
  const idPath = this.idPath;
241
226
 
242
227
  if (idPath) {
243
- return this.idPath.join('.');
228
+ // JSON keeps the ids and relation names apart, whatever they contain.
229
+ return JSON.stringify(idPath);
244
230
  } else {
245
231
  return null;
246
232
  }
@@ -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,39 +1,107 @@
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 { getFieldExpressionColumn } = require('../utils/parseFieldExpression');
6
7
 
7
8
  const QUERY_PROPS_PROPERTY = '$$queryProps';
8
-
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`.
9
+ // The keys of the field expression query properties like `'data:a'` that were
10
+ // given while their json column `data` had a value:
11
+ const COMBINED_QUERY_PROPS_PROPERTY = '$$combinedQueryProps';
12
+
13
+ // Moves the query properties of `json` into a hidden property of `model` so
14
+ // that they can be later merged back to its database json. A key holds either
15
+ // a value or a query property, so the keys given in `json` replace the ones
16
+ // that the model holds as the other kind.
11
17
  function splitQueryProps(model, json) {
12
18
  const keys = Object.keys(json);
19
+ const oldQueryProps = model[QUERY_PROPS_PROPERTY];
13
20
 
14
- if (hasQueryProps(json, keys)) {
15
- const queryProps = {};
21
+ if (!json.$isObjectionModel && hasQueryProps(json, keys)) {
22
+ const queryProps = Object.assign({}, oldQueryProps);
23
+ const combinedKeys = new Set(model[COMBINED_QUERY_PROPS_PROPERTY]);
16
24
  const modelProps = {};
25
+ const jsonSchema = model.constructor.getJsonSchema();
26
+ const properties = jsonSchema && jsonSchema.properties;
17
27
 
18
28
  for (let i = 0, l = keys.length; i < l; ++i) {
19
29
  const key = keys[i];
20
30
  const value = json[key];
21
31
 
22
32
  if (isQueryProp(value)) {
33
+ const column = getFieldExpressionColumn(key, properties);
34
+
35
+ if (column !== null && (json[column] !== undefined || model[column] !== undefined)) {
36
+ combinedKeys.add(key);
37
+ } else {
38
+ combinedKeys.delete(key);
39
+ }
40
+
23
41
  queryProps[key] = value;
42
+ delete model[key];
24
43
  } else {
25
44
  modelProps[key] = value;
45
+ delete queryProps[key];
26
46
  }
27
47
  }
28
48
 
29
49
  defineNonEnumerableProperty(model, QUERY_PROPS_PROPERTY, queryProps);
50
+ defineNonEnumerableProperty(model, COMBINED_QUERY_PROPS_PROPERTY, [...combinedKeys]);
30
51
 
31
52
  return modelProps;
32
53
  } else {
54
+ if (oldQueryProps) {
55
+ defineNonEnumerableProperty(model, QUERY_PROPS_PROPERTY, omit(oldQueryProps, keys));
56
+ }
57
+
33
58
  return json;
34
59
  }
35
60
  }
36
61
 
62
+ // Omits the values that parsing and validation add to `json` for the keys that
63
+ // `model` holds query properties for, like Ajv defaults.
64
+ function omitQueryProps(model, json) {
65
+ const keys = getQueryPropKeys(model);
66
+ return keys.length > 0 ? omit(json, keys) : json;
67
+ }
68
+
69
+ // Removes the query properties of the keys that were given a value since, by an
70
+ // assignment, `$set()` or a write that fetched the value from the database. We
71
+ // do this when the model is passed to a write, and not when its database json
72
+ // is built, so that the values set by its hooks don't override them.
73
+ function removeOverriddenQueryProps(model) {
74
+ const queryProps = model[QUERY_PROPS_PROPERTY];
75
+
76
+ if (queryProps) {
77
+ const jsonSchema = model.constructor.getJsonSchema();
78
+ const properties = jsonSchema && jsonSchema.properties;
79
+ const combinedKeys = model[COMBINED_QUERY_PROPS_PROPERTY] || [];
80
+ const keys = Object.keys(queryProps).filter((key) => {
81
+ const column = getFieldExpressionColumn(key, properties);
82
+ return (
83
+ model[key] !== undefined ||
84
+ // The json column of a field expression key like `data:a` overrides it
85
+ // too, unless it was given while the column had a value (#206):
86
+ (column !== null && !combinedKeys.includes(key) && model[column] !== undefined)
87
+ );
88
+ });
89
+
90
+ if (keys.length > 0) {
91
+ defineNonEnumerableProperty(model, QUERY_PROPS_PROPERTY, omit(queryProps, keys));
92
+ }
93
+ }
94
+ }
95
+
96
+ // Removes the values that the hooks of a write set for the keys that `model`
97
+ // holds query properties for. The query properties are written instead, so the
98
+ // values must not count as given in later writes.
99
+ function removeOverriddenValues(model) {
100
+ for (const key of getQueryPropKeys(model)) {
101
+ delete model[key];
102
+ }
103
+ }
104
+
37
105
  function hasQueryProps(json, keys) {
38
106
  for (let i = 0, l = keys.length; i < l; ++i) {
39
107
  if (isQueryProp(json[keys[i]])) {
@@ -149,6 +217,10 @@ function isKnexRawConvertable(queryProp) {
149
217
 
150
218
  module.exports = {
151
219
  splitQueryProps,
220
+ omitQueryProps,
221
+ removeOverriddenQueryProps,
222
+ removeOverriddenValues,
152
223
  mergeQueryProps,
153
224
  getQueryPropKeys,
225
+ isQueryProp,
154
226
  };
@@ -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 { getFieldExpressionColumn } = require('../utils/parseFieldExpression');
6
7
  const EMPTY_ARRAY = [];
7
8
 
8
9
  function toJson(model, optIn) {
@@ -134,7 +135,13 @@ 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
+ const column = getFieldExpressionColumn(key, pick);
144
+ return column !== null && hasOwn(pick, column);
138
145
  }
139
146
 
140
147
  function assignVirtualAttributes(json, model, vAttr, opt) {
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const hiddenProps = ['$$queryProps'];
3
+ const hiddenProps = ['$$queryProps', '$$combinedQueryProps'];
4
4
 
5
5
  const staticHiddenProps = [
6
6
  '$$knex',
@@ -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
  };
@@ -50,6 +50,7 @@ const { LimitPerParentOperation } = require('./operations/LimitPerParentOperatio
50
50
  const { FromOperation } = require('./operations/FromOperation');
51
51
  const { KnexOperation } = require('./operations/KnexOperation');
52
52
  const { clearUnsupportedOrder } = require('./withoutOrder');
53
+ const { addWhereGroup } = require('./whereGroups');
53
54
 
54
55
  class QueryBuilder extends QueryBuilderBase {
55
56
  static init(self, modelClass) {
@@ -512,7 +513,10 @@ class QueryBuilder extends QueryBuilderBase {
512
513
 
513
514
  async resultSize() {
514
515
  const knex = this.knex();
515
- const builder = this.clone().clear(/orderBy|offset|limit/);
516
+ // `distinctOn` picks the first row of each group in the order of the query,
517
+ // which decides which root models are counted with to-many joins.
518
+ const shouldKeepOrder = this.has('distinctOn');
519
+ const builder = this.clone().clear(shouldKeepOrder ? /offset|limit/ : /orderBy|offset|limit/);
516
520
  const joinEagerOp = builder.findOperation(JoinEagerOperation);
517
521
 
518
522
  if (joinEagerOp) {
@@ -530,16 +534,20 @@ class QueryBuilder extends QueryBuilderBase {
530
534
 
531
535
  // Also clear the order on the built knex query, since `orderBy` calls
532
536
  // can be added at build time, e.g. by a relation's `modify`.
533
- const buildQuery = (knexBuilder) => builder.toKnexQuery(knexBuilder).clearOrder();
537
+ const buildQuery = (knexBuilder) => {
538
+ const query = builder.toKnexQuery(knexBuilder);
539
+ return shouldKeepOrder ? query : query.clearOrder();
540
+ };
534
541
 
535
542
  const countQuery = knex.count('* as count').from((knexBuilder) => {
536
543
  if (!joinEagerOp || idColumns.length === 0) {
537
544
  // Without ids, the joins are all to-one and return one row per root
538
545
  // model.
539
546
  buildQuery(knexBuilder);
540
- } else if (refersToSelections(builder)) {
547
+ } else if (refersToSelections(builder) || builder.has('distinctOn')) {
541
548
  // `having`, and `groupBy` with an alias, may refer to aliased
542
- // selections, so keep them and select the distinct ids from the
549
+ // selections, and clearing the selections would also clear
550
+ // `distinctOn`, so keep them and select the distinct ids from the
543
551
  // result instead.
544
552
  knexBuilder.distinct(idColumns).from((knexBuilder) => {
545
553
  buildQuery(knexBuilder).as('inner');
@@ -1015,7 +1023,9 @@ class QueryBuilder extends QueryBuilderBase {
1015
1023
  }
1016
1024
 
1017
1025
  updateById(id, modelOrObject) {
1018
- return this.findById(id)
1026
+ // Keep the id condition in its own where group, so that a user's `orWhere()`
1027
+ // can't get around it. See `whereGroups.js`.
1028
+ return addWhereGroup(this, () => this.findById(id))
1019
1029
  .update(modelOrObject)
1020
1030
  .runBefore((_, builder) => {
1021
1031
  if (!builder.internalOptions().skipUndefined) {
@@ -1025,7 +1035,7 @@ class QueryBuilder extends QueryBuilderBase {
1025
1035
  }
1026
1036
 
1027
1037
  patchById(id, modelOrObject) {
1028
- return this.findById(id)
1038
+ return addWhereGroup(this, () => this.findById(id))
1029
1039
  .patch(modelOrObject)
1030
1040
  .runBefore((_, builder) => {
1031
1041
  if (!builder.internalOptions().skipUndefined) {
@@ -1035,7 +1045,7 @@ class QueryBuilder extends QueryBuilderBase {
1035
1045
  }
1036
1046
 
1037
1047
  deleteById(id) {
1038
- return this.findById(id)
1048
+ return addWhereGroup(this, () => this.findById(id))
1039
1049
  .delete()
1040
1050
  .runBefore((_, builder) => {
1041
1051
  if (!builder.internalOptions().skipUndefined) {
@@ -6,7 +6,46 @@ class GraphData {
6
6
  this.currentGraph = currentGraph;
7
7
  this.graphOptions = graphOptions;
8
8
  this.nodeDbExistence = nodeDbExistence;
9
+ // Pruning replaces `graph.nodes`, but `keptNodeForRow()` also needs the
10
+ // branches below related nodes.
11
+ this._unprunedNodes = graph.nodes;
12
+ this._keptNodesByRowKey = null;
9
13
  }
14
+
15
+ // Find the node that keeps the row of `currentNode` in another place of the
16
+ // graph, by relating or patching it there, or by upserting it below a related
17
+ // node. Only call this after pruning.
18
+ keptNodeForRow(currentNode) {
19
+ if (!this._keptNodesByRowKey) {
20
+ this._keptNodesByRowKey = new Map();
21
+ const nodes = new Set(this.graph.nodes);
22
+
23
+ // Nested upserts write the pruned branches below related nodes, except
24
+ // for models marked with `#unrelate` or `#delete` and their descendants:
25
+ const isUpsertedBelowRelatedNode = (node) =>
26
+ !node.toBeUnrelatedOrDeleted &&
27
+ (nodes.has(node.parentNode)
28
+ ? this.graphOptions.shouldRelate(node.parentNode, this)
29
+ : isUpsertedBelowRelatedNode(node.parentNode));
30
+
31
+ for (const node of this._unprunedNodes) {
32
+ if (
33
+ node.obj.$hasId() &&
34
+ (nodes.has(node)
35
+ ? this.graphOptions.shouldPatchOrUpdateIgnoreDisable(node, this)
36
+ : isUpsertedBelowRelatedNode(node))
37
+ ) {
38
+ this._keptNodesByRowKey.set(getRowKey(node), node);
39
+ }
40
+ }
41
+ }
42
+
43
+ return this._keptNodesByRowKey.get(getRowKey(currentNode)) || null;
44
+ }
45
+ }
46
+
47
+ function getRowKey(node) {
48
+ return `${node.modelClass.getTableName()}:${node.obj.$idKey()}`;
10
49
  }
11
50
 
12
51
  module.exports = {
@@ -4,6 +4,8 @@ const { asArray, groupBy } = require('../../utils/objectUtils');
4
4
  const { ModelGraph } = require('../../model/graph/ModelGraph');
5
5
  const { FetchStrategy } = require('./GraphOptions');
6
6
  const { RelationExpression } = require('../RelationExpression');
7
+ const { getDeclaredColumns } = require('../operations/UpdateOperation');
8
+ const { getFieldExpressionColumn } = require('../../utils/parseFieldExpression');
7
9
 
8
10
  class GraphFetcher {
9
11
  /**
@@ -106,8 +108,12 @@ function createInputColumnSelector(nodes) {
106
108
 
107
109
  for (const node of nodes) {
108
110
  const databaseJson = node.obj.$toDatabaseJson(builder);
111
+ const declaredColumns = getDeclaredColumns(node.modelClass);
112
+
113
+ for (const key of Object.keys(databaseJson)) {
114
+ // Select the json column of field expressions like `'meta:a'`:
115
+ const column = getFieldExpressionColumn(key, declaredColumns) ?? key;
109
116
 
110
- for (const column of Object.keys(databaseJson)) {
111
117
  if (!shouldSelectColumn(column, selects, node)) {
112
118
  continue;
113
119
  }