objection 3.2.0 → 3.3.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 (52) hide show
  1. package/README.md +13 -13
  2. package/lib/model/AjvValidator.js +133 -3
  3. package/lib/queryBuilder/QueryBuilder.js +186 -5
  4. package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
  5. package/lib/queryBuilder/ReferenceBuilder.js +5 -3
  6. package/lib/queryBuilder/RelationExpression.js +13 -1
  7. package/lib/queryBuilder/StaticHookArguments.js +12 -1
  8. package/lib/queryBuilder/graph/GraphUpsert.js +22 -0
  9. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
  10. package/lib/queryBuilder/graph/insert/GraphInsert.js +107 -8
  11. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -1
  12. package/lib/queryBuilder/join/JoinResultParser.js +22 -0
  13. package/lib/queryBuilder/join/RelationJoiner.js +71 -22
  14. package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
  15. package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
  16. package/lib/queryBuilder/operations/InsertOperation.js +121 -12
  17. package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
  18. package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
  19. package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
  20. package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
  21. package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
  22. package/lib/queryBuilder/operations/eager/EagerOperation.js +4 -0
  23. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +2 -6
  24. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +10 -33
  25. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
  26. package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
  27. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +41 -34
  28. package/lib/queryBuilder/operations/select/Selection.js +95 -4
  29. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
  30. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
  31. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
  32. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
  33. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
  34. package/lib/queryBuilder/whereGroups.js +125 -0
  35. package/lib/relations/Relation.js +5 -2
  36. package/lib/relations/hasMany/HasManyRelateOperation.js +5 -2
  37. package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
  38. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
  39. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
  40. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
  41. package/lib/relations/manyToMany/ManyToManyRelation.js +70 -48
  42. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
  43. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
  44. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
  45. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
  46. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +8 -2
  47. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
  48. package/lib/utils/deprecate.js +6 -0
  49. package/lib/utils/identifierMapping.js +15 -1
  50. package/lib/utils/parseFieldExpression.js +28 -0
  51. package/package.json +10 -6
  52. package/typings/objection/index.d.ts +280 -28
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Tests](https://github.com/ditojs/objection/actions/workflows/test.yml/badge.svg)](https://github.com/ditojs/objection/actions)
4
4
  [![Join the chat at https://gitter.im/Vincit/objection.js](https://badges.gitter.im/Vincit/objection.js.svg)](https://gitter.im/Vincit/objection.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
5
5
 
6
- # [Objection.js](https://vincit.github.io/objection.js)
6
+ # [Objection.js](https://ditojs.github.io/objection/)
7
7
 
8
8
  Objection.js is an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) for [Node.js](https://nodejs.org/) that aims to stay out of your way and make it as easy as possible to use the full power of SQL and the underlying database engine while still making the common stuff easy and enjoyable.
9
9
 
@@ -13,13 +13,13 @@ Objection.js is built on an SQL query builder called [knex](http://knexjs.org).
13
13
 
14
14
  What objection.js gives you:
15
15
 
16
- - **An easy declarative way of [defining models](https://vincit.github.io/objection.js/guide/models.html) and relationships between them**
17
- - **Simple and fun way to [fetch, insert, update and delete](https://vincit.github.io/objection.js/guide/query-examples.html) objects using the full power of SQL**
18
- - **Powerful mechanisms for [eager loading](https://vincit.github.io/objection.js/guide/query-examples.html#eager-loading), [inserting](https://vincit.github.io/objection.js/guide/query-examples.html#graph-inserts) and [upserting](https://vincit.github.io/objection.js/guide/query-examples.html#graph-upserts) object graphs**
19
- - **Easy to use [transactions](https://vincit.github.io/objection.js/guide/transactions.html)**
16
+ - **An easy declarative way of [defining models](https://ditojs.github.io/objection/guide/models.html) and relationships between them**
17
+ - **Simple and fun way to [fetch, insert, update and delete](https://ditojs.github.io/objection/guide/query-examples.html) objects using the full power of SQL**
18
+ - **Powerful mechanisms for [eager loading](https://ditojs.github.io/objection/guide/query-examples.html#eager-loading), [inserting](https://ditojs.github.io/objection/guide/query-examples.html#graph-inserts) and [upserting](https://ditojs.github.io/objection/guide/query-examples.html#graph-upserts) object graphs**
19
+ - **Easy to use [transactions](https://ditojs.github.io/objection/guide/transactions.html)**
20
20
  - **Official [TypeScript](https://github.com/ditojs/objection/blob/main/typings/objection/index.d.ts) support**
21
- - **Optional [JSON schema](https://vincit.github.io/objection.js/guide/validation.html) validation**
22
- - **A way to [store complex documents](https://vincit.github.io/objection.js/guide/documents.html) as single rows**
21
+ - **Optional [JSON schema](https://ditojs.github.io/objection/guide/validation.html) validation**
22
+ - **A way to [store complex documents](https://ditojs.github.io/objection/guide/documents.html) as single rows**
23
23
 
24
24
  What objection.js **doesn't** give you:
25
25
 
@@ -31,7 +31,7 @@ What objection.js **doesn't** give you:
31
31
  - **A custom query DSL. SQL is used as a query language.**
32
32
  This doesn't mean you have to write SQL strings though. A query builder based on [knex](http://knexjs.org) is
33
33
  used to build the SQL. However, if the query builder fails you for some reason, raw SQL strings can be easily
34
- written using the [raw](https://vincit.github.io/objection.js/api/objection/#raw) helper function.
34
+ written using the [raw](https://ditojs.github.io/objection/api/objection/#raw) helper function.
35
35
  - **Automatic database schema creation and migration from model definitions.**
36
36
  For simple things it is useful that the database schema is automatically generated from the model definitions,
37
37
  but usually just gets in your way when doing anything non-trivial. Objection.js leaves the schema related things
@@ -45,9 +45,9 @@ Check out [this issue](https://github.com/Vincit/objection.js/issues/1069) to se
45
45
  Shortcuts:
46
46
 
47
47
  - [Who uses objection.js](https://github.com/Vincit/objection.js/discussions/2464)
48
- - [API reference](https://vincit.github.io/objection.js/api/query-builder/)
48
+ - [API reference](https://ditojs.github.io/objection/api/query-builder/)
49
49
  - [Example projects](https://github.com/Vincit/objection.js/tree/main/examples)
50
- - [Changelog](https://vincit.github.io/objection.js/release-notes/changelog.html)
51
- - [v1 -> v2 -> v3 migration guide](https://vincit.github.io/objection.js/release-notes/migration.html)
52
- - [Contribution guide](https://vincit.github.io/objection.js/guide/contributing.html)
53
- - [Plugins](https://vincit.github.io/objection.js/guide/plugins.html)
50
+ - [Changelog](https://ditojs.github.io/objection/release-notes/changelog.html)
51
+ - [v1 -> v2 -> v3 migration guide](https://ditojs.github.io/objection/release-notes/migration.html)
52
+ - [Contribution guide](https://ditojs.github.io/objection/guide/contributing.html)
53
+ - [Plugins](https://ditojs.github.io/objection/guide/plugins.html)
@@ -5,6 +5,7 @@ const addFormats = require('ajv-formats');
5
5
  const { Validator } = require('./Validator');
6
6
  const { ValidationErrorType } = require('../model/ValidationError');
7
7
  const { isObject, once, cloneDeep: lodashCloneDeep, omit } = require('../utils/objectUtils');
8
+ const { parseFieldExpression } = require('../utils/parseFieldExpression');
8
9
 
9
10
  class AjvValidator extends Validator {
10
11
  static init(self, conf) {
@@ -81,13 +82,24 @@ class AjvValidator extends Validator {
81
82
  json = cloneDeep(json);
82
83
  }
83
84
 
84
- validator.call(model, json);
85
+ // Patch objects can contain field expression keys like `'meta:b'` that update
86
+ // a single property inside a json column. Validate them against the nested
87
+ // schema by converting them into nested objects like `{ meta: { b: value } }`.
88
+ const fieldExpressions = options.patch ? nestFieldExpressions(json) : null;
89
+ const validationJson = fieldExpressions ? fieldExpressions.json : json;
90
+
91
+ validator.call(model, validationJson);
85
92
  const error = parseValidationError(validator.errors, modelClass, options, this.ajvOptions);
86
93
 
87
94
  if (error) {
88
95
  throw error;
89
96
  }
90
97
 
98
+ if (fieldExpressions) {
99
+ // Copy back the changes made by Ajv options like `coerceTypes` and `removeAdditional`.
100
+ unnestFieldExpressions(json, fieldExpressions);
101
+ }
102
+
91
103
  return json;
92
104
  }
93
105
 
@@ -223,6 +235,116 @@ function parseValidationError(errors, modelClass, options, ajvOptions) {
223
235
  });
224
236
  }
225
237
 
238
+ function nestFieldExpressions(json) {
239
+ const keys = Object.keys(json);
240
+
241
+ if (!keys.some(isFieldExpression)) {
242
+ return null;
243
+ }
244
+
245
+ const nested = Object.create(null);
246
+ const paths = new Map();
247
+ // The objects created here for the nested paths. Other objects are values
248
+ // of the input json and must not be modified.
249
+ const createdObjects = new Set();
250
+
251
+ for (const key of keys) {
252
+ if (!isFieldExpression(key)) {
253
+ nested[key] = json[key];
254
+ paths.set(key, [key]);
255
+ }
256
+ }
257
+
258
+ for (const key of keys) {
259
+ if (isFieldExpression(key)) {
260
+ const path = fieldExpressionPath(key);
261
+
262
+ // Field expressions with array access, and those that conflict with other
263
+ // keys of the patch, can't be validated without the current value of the
264
+ // json column. Leave them out of the validation.
265
+ if (path && setPath(nested, path, json[key], createdObjects)) {
266
+ paths.set(key, path);
267
+ }
268
+ }
269
+ }
270
+
271
+ return { json: nested, paths };
272
+ }
273
+
274
+ function unnestFieldExpressions(json, { json: nested, paths }) {
275
+ for (const [key, path] of paths) {
276
+ let value = nested;
277
+ let found = true;
278
+
279
+ for (const prop of path) {
280
+ found = isObject(value) && hasOwn(value, prop);
281
+
282
+ if (!found) {
283
+ break;
284
+ }
285
+
286
+ value = value[prop];
287
+ }
288
+
289
+ if (!found) {
290
+ // Removed by `removeAdditional`.
291
+ delete json[key];
292
+ } else if (json[key] !== value) {
293
+ json[key] = value;
294
+ }
295
+ }
296
+ }
297
+
298
+ function isFieldExpression(key) {
299
+ return key.indexOf(':') !== -1;
300
+ }
301
+
302
+ function fieldExpressionPath(key) {
303
+ let parsed;
304
+
305
+ try {
306
+ parsed = parseFieldExpression(key);
307
+ } catch (err) {
308
+ // Not a valid field expression, validate it as a top-level property.
309
+ return [key];
310
+ }
311
+
312
+ if (parsed.access.some((access) => access.type !== 'object')) {
313
+ return null;
314
+ }
315
+
316
+ return [parsed.column, ...parsed.access.map((access) => String(access.ref))];
317
+ }
318
+
319
+ function setPath(obj, path, value, createdObjects) {
320
+ const last = path.length - 1;
321
+
322
+ for (let i = 0; i < last; ++i) {
323
+ const prop = path[i];
324
+
325
+ if (!hasOwn(obj, prop)) {
326
+ const child = Object.create(null);
327
+ createdObjects.add(child);
328
+ obj[prop] = child;
329
+ } else if (!createdObjects.has(obj[prop])) {
330
+ return false;
331
+ }
332
+
333
+ obj = obj[prop];
334
+ }
335
+
336
+ if (hasOwn(obj, path[last])) {
337
+ return false;
338
+ }
339
+
340
+ obj[path[last]] = value;
341
+ return true;
342
+ }
343
+
344
+ function hasOwn(obj, prop) {
345
+ return Object.prototype.hasOwnProperty.call(obj, prop);
346
+ }
347
+
226
348
  function cloneDeep(obj) {
227
349
  if (isObject(obj) && obj.$isObjectionModel) {
228
350
  return obj.$clone();
@@ -276,7 +398,12 @@ function objectHasDefaults(obj) {
276
398
  }
277
399
 
278
400
  function jsonSchemaWithoutRequired(jsonSchema) {
279
- const subSchemaProps = ['anyOf', 'oneOf', 'allOf', 'not', 'then', 'else', 'properties'];
401
+ // `not` is intentionally left untouched: Stripping `required` inside it
402
+ // would turn it into a schema that rejects everything (e.g.
403
+ // `not: { required: ['a'] }` -> `not: {}`). Kept as is, it still holds for
404
+ // patches: Any property present in the patch is also present in the
405
+ // resulting object.
406
+ const subSchemaProps = ['anyOf', 'oneOf', 'allOf', 'then', 'else', 'properties'];
280
407
  // `definitions` was renamed to `$defs` in draft 2019-09. Support both.
281
408
  const definitionProps = ['definitions', '$defs'];
282
409
  return Object.assign(
@@ -318,7 +445,10 @@ function subSchemaWithoutRequired(jsonSchema, prop) {
318
445
  } else {
319
446
  return {};
320
447
  }
321
- } else if (jsonSchema.type === 'object' && prop === 'properties') {
448
+ } else if (prop === 'properties') {
449
+ // `properties` is a map of property names to schemas, regardless of the
450
+ // schema's `type` (which may also be e.g. `['object', 'null']`, or be
451
+ // missing entirely).
322
452
  return {
323
453
  [prop]: Object.fromEntries(
324
454
  Object.entries(jsonSchema[prop]).map(([key, schema]) => [
@@ -46,6 +46,7 @@ const { FirstOperation } = require('./operations/FirstOperation');
46
46
  const { NoneOperation } = require('./operations/NoneOperation');
47
47
  const { FromOperation } = require('./operations/FromOperation');
48
48
  const { KnexOperation } = require('./operations/KnexOperation');
49
+ const { wrapWhereGroups } = require('./whereGroups');
49
50
 
50
51
  class QueryBuilder extends QueryBuilderBase {
51
52
  static init(self, modelClass) {
@@ -58,8 +59,12 @@ class QueryBuilder extends QueryBuilderBase {
58
59
 
59
60
  self._allowedGraphExpression = null;
60
61
  self._graphModifiersAtPath = [];
62
+ // The eager algorithm that was most recently used explicitly, see `withGraph()`.
63
+ self._graphAlgorithm = null;
64
+ self._isJoinChildQuery = false;
61
65
  self._findOperationOptions = modelClass.defaultFindOptions;
62
66
  self._relatedQueryFor = null;
67
+ self._internalSelections = new Set();
63
68
 
64
69
  self._findOperationFactory = findOperationFactory;
65
70
  self._insertOperationFactory = insertOperationFactory;
@@ -78,6 +83,27 @@ class QueryBuilder extends QueryBuilderBase {
78
83
  return RelationExpression.create(expr).toPojo();
79
84
  }
80
85
 
86
+ childQueryOf(query, options = {}) {
87
+ super.childQueryOf(query, options);
88
+
89
+ if (query) {
90
+ this._isJoinChildQuery = !!options.isJoinChildQuery;
91
+
92
+ if (query.isObjectionQueryBuilder) {
93
+ this._graphAlgorithm = query._graphAlgorithm;
94
+ }
95
+ }
96
+
97
+ return this;
98
+ }
99
+
100
+ // Returns true if this query was created for a relation that is loaded using
101
+ // `withGraphJoined()`. Such queries are only used to build the join and are
102
+ // never executed themselves.
103
+ isJoinChildQuery() {
104
+ return this._isJoinChildQuery;
105
+ }
106
+
81
107
  tableNameFor(modelClassOrTableName, newTableName) {
82
108
  return super.tableNameFor(getTableName(modelClassOrTableName), newTableName);
83
109
  }
@@ -191,19 +217,50 @@ class QueryBuilder extends QueryBuilderBase {
191
217
  }
192
218
 
193
219
  withGraphFetched(exp, options = {}) {
194
- return this._withGraph(exp, options, getWhereInEagerAlgorithm(this));
220
+ return this._withGraphUsing(exp, options, getWhereInEagerAlgorithm(this));
195
221
  }
196
222
 
197
223
  withGraphJoined(exp, options = {}) {
198
- return this._withGraph(exp, options, getJoinEagerAlgorithm(this));
224
+ return this._withGraphUsing(exp, options, getJoinEagerAlgorithm(this));
225
+ }
226
+
227
+ // Like `withGraphFetched()` or `withGraphJoined()` with `options.algorithm`
228
+ // set to 'fetch' or 'join'. Without it, the top-level relations that are
229
+ // already loaded by one of the algorithms are merged into it, and new ones
230
+ // use the most recently used algorithm.
231
+ withGraph(exp, { algorithm, ...options } = {}) {
232
+ if (algorithm !== undefined) {
233
+ return this._withGraphUsing(exp, options, getEagerAlgorithmByName(this, algorithm));
234
+ }
235
+
236
+ const parsedExp = parseRelationExpression(this.modelClass(), exp);
237
+
238
+ for (const [algorithm, groupExp] of groupByEagerAlgorithm(this, parsedExp)) {
239
+ this._withGraph(groupExp, options, algorithm);
240
+ }
241
+
242
+ return this;
243
+ }
244
+
245
+ _withGraphUsing(exp, options, algorithm) {
246
+ this._graphAlgorithm = algorithm;
247
+ return this._withGraph(exp, options, algorithm);
199
248
  }
200
249
 
201
250
  _withGraph(exp, options, algorithm) {
202
251
  const eagerOp = ensureEagerOperation(this, algorithm);
203
252
  const parsedExp = parseRelationExpression(this.modelClass(), exp);
204
253
 
254
+ // The `joinOperation` option only applies to the relations of this call,
255
+ // see `RelationJoiner`. All other options are shared by all calls.
256
+ const { joinOperation, ...graphOptions } = options;
257
+
205
258
  eagerOp.expression = eagerOp.expression.merge(parsedExp);
206
- eagerOp.graphOptions = { ...eagerOp.graphOptions, ...options };
259
+ eagerOp.graphOptions = { ...eagerOp.graphOptions, ...graphOptions };
260
+
261
+ if (joinOperation) {
262
+ eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
263
+ }
207
264
 
208
265
  checkMixedEager(this);
209
266
  checkEager(this);
@@ -355,6 +412,8 @@ class QueryBuilder extends QueryBuilderBase {
355
412
 
356
413
  builder._allowedGraphExpression = this._allowedGraphExpression;
357
414
  builder._graphModifiersAtPath = this._graphModifiersAtPath.slice();
415
+ builder._graphAlgorithm = this._graphAlgorithm;
416
+ builder._isJoinChildQuery = this._isJoinChildQuery;
358
417
  builder._findOperationOptions = this._findOperationOptions;
359
418
  builder._relatedQueryFor = this._relatedQueryFor;
360
419
 
@@ -376,8 +435,17 @@ class QueryBuilder extends QueryBuilderBase {
376
435
  return builder;
377
436
  }
378
437
 
438
+ // Registers a property of the result models that was selected internally,
439
+ // e.g. by an eager operation, without the user asking for it. Such properties
440
+ // are omitted from the result models before `runAfter` hooks are called.
441
+ addInternalSelection(prop) {
442
+ this._internalSelections.add(prop);
443
+ return this;
444
+ }
445
+
379
446
  clearWithGraph() {
380
447
  this.clear(EagerOperation);
448
+ this._graphAlgorithm = null;
381
449
  return this;
382
450
  }
383
451
 
@@ -424,11 +492,27 @@ class QueryBuilder extends QueryBuilderBase {
424
492
  async resultSize() {
425
493
  const knex = this.knex();
426
494
  const builder = this.clone().clear(/orderBy|offset|limit/);
495
+ const joinEagerOp = builder.findOperation(JoinEagerOperation);
496
+
497
+ if (joinEagerOp) {
498
+ // `withGraphJoined` needs the table metadata to build its selects.
499
+ await joinEagerOp.onBefore3(builder);
500
+ }
427
501
 
428
502
  const countQuery = knex.count('* as count').from((knexBuilder) => {
429
503
  // Also clear the order on the built knex query, since `orderBy` calls
430
504
  // can be added at build time, e.g. by a relation's `modify`.
431
- builder.toKnexQuery(knexBuilder).clearOrder().as('temp');
505
+ knexBuilder = builder.toKnexQuery(knexBuilder).clearOrder();
506
+
507
+ if (joinEagerOp) {
508
+ // The joins of `withGraphJoined` return one row per joined child.
509
+ // Count the distinct root models instead.
510
+ const tableRef = builder.tableRef();
511
+ const idColumns = builder.modelClass().getIdColumnArray();
512
+ knexBuilder.clearSelect().distinct(idColumns.map((col) => `${tableRef}.${col}`));
513
+ }
514
+
515
+ knexBuilder.as('temp');
432
516
  });
433
517
 
434
518
  if (this.internalOptions().debug) {
@@ -891,6 +975,26 @@ class QueryBuilder extends QueryBuilderBase {
891
975
  return this;
892
976
  }
893
977
 
978
+ updateById(id, modelOrObject) {
979
+ return this.findById(id)
980
+ .update(modelOrObject)
981
+ .runBefore((_, builder) => {
982
+ if (!builder.internalOptions().skipUndefined) {
983
+ assertIdNotUndefined(id, `undefined was passed to updateById`);
984
+ }
985
+ });
986
+ }
987
+
988
+ patchById(id, modelOrObject) {
989
+ return this.findById(id)
990
+ .patch(modelOrObject)
991
+ .runBefore((_, builder) => {
992
+ if (!builder.internalOptions().skipUndefined) {
993
+ assertIdNotUndefined(id, `undefined was passed to patchById`);
994
+ }
995
+ });
996
+ }
997
+
894
998
  deleteById(id) {
895
999
  return this.findById(id)
896
1000
  .delete()
@@ -1055,6 +1159,66 @@ function getGraphExpression(builder) {
1055
1159
  );
1056
1160
  }
1057
1161
 
1162
+ function getEagerAlgorithmByName(builder, name) {
1163
+ if (name === 'fetch') {
1164
+ return getWhereInEagerAlgorithm(builder);
1165
+ } else if (name === 'join') {
1166
+ return getJoinEagerAlgorithm(builder);
1167
+ } else {
1168
+ throw new Error(`unknown graph algorithm "${name}", expected "fetch" or "join"`);
1169
+ }
1170
+ }
1171
+
1172
+ function getEagerAlgorithmOfOperation(builder, eagerOp) {
1173
+ if (eagerOp instanceof JoinEagerOperation) {
1174
+ return getJoinEagerAlgorithm(builder);
1175
+ } else if (eagerOp instanceof NaiveEagerOperation) {
1176
+ return getNaiveEagerAlgorithm(builder);
1177
+ } else {
1178
+ return getWhereInEagerAlgorithm(builder);
1179
+ }
1180
+ }
1181
+
1182
+ // Splits `expression` by its top-level relations into one expression per eager
1183
+ // algorithm. Relations that are already loaded by one of the eager operations
1184
+ // keep its algorithm, new ones use the most recently used one.
1185
+ function groupByEagerAlgorithm(builder, expression) {
1186
+ const defaultAlgorithm = builder._graphAlgorithm || getWhereInEagerAlgorithm(builder);
1187
+ const { node } = expression;
1188
+ const rootProps = {};
1189
+ const groups = new Map();
1190
+
1191
+ for (const key of Object.keys(node)) {
1192
+ if (key.startsWith('$')) {
1193
+ rootProps[key] = node[key];
1194
+ }
1195
+ }
1196
+
1197
+ for (const childName of node.$childNames) {
1198
+ const eagerOp = findEagerOperations(builder).find(
1199
+ (op) => op.expression.node.$allRecursive || op.expression.node[childName],
1200
+ );
1201
+
1202
+ const algorithm = eagerOp ? getEagerAlgorithmOfOperation(builder, eagerOp) : defaultAlgorithm;
1203
+ let group = groups.get(algorithm);
1204
+
1205
+ if (!group) {
1206
+ group = { ...rootProps, $childNames: [] };
1207
+ groups.set(algorithm, group);
1208
+ }
1209
+
1210
+ group.$childNames.push(childName);
1211
+ group[childName] = node[childName];
1212
+ }
1213
+
1214
+ if (groups.size === 0) {
1215
+ // No top-level relations, e.g. `*` or an empty expression.
1216
+ return [[defaultAlgorithm, expression]];
1217
+ }
1218
+
1219
+ return [...groups].map(([algorithm, group]) => [algorithm, RelationExpression.create(group)]);
1220
+ }
1221
+
1058
1222
  function getWhereInEagerAlgorithm(builder) {
1059
1223
  return builder.modelClass().WhereInEagerAlgorithm;
1060
1224
  }
@@ -1200,6 +1364,7 @@ function afterExecute(builder, result) {
1200
1364
 
1201
1365
  promise = chainOperationHooks(promise, builder, 'onAfter1');
1202
1366
  promise = chainOperationHooks(promise, builder, 'onAfter2');
1367
+ promise = promise.then((result) => omitInternalSelections(builder, result));
1203
1368
 
1204
1369
  promise = chainHooks(promise, builder, builder.context().runAfter);
1205
1370
  promise = chainHooks(promise, builder, builder.internalContext().runAfter);
@@ -1209,6 +1374,22 @@ function afterExecute(builder, result) {
1209
1374
  return promise;
1210
1375
  }
1211
1376
 
1377
+ function omitInternalSelections(builder, result) {
1378
+ const props = builder._internalSelections;
1379
+
1380
+ if (props.size && Array.isArray(result) && isObject(result[0])) {
1381
+ const modelClass = builder.resultModelClass();
1382
+
1383
+ for (const model of result) {
1384
+ for (const prop of props) {
1385
+ modelClass.omitImpl(model, prop);
1386
+ }
1387
+ }
1388
+ }
1389
+
1390
+ return result;
1391
+ }
1392
+
1212
1393
  class ReturnImmediatelyException {
1213
1394
  constructor(value) {
1214
1395
  this.value = value;
@@ -1331,7 +1512,7 @@ function callOnBuildHooks(builder) {
1331
1512
  callOnBuildFuncs(builder, builder.internalContext().onBuild);
1332
1513
 
1333
1514
  builder.executeOnBuild();
1334
- return builder;
1515
+ return wrapWhereGroups(builder);
1335
1516
  }
1336
1517
 
1337
1518
  function callOnBuildFuncs(builder, func) {
@@ -26,6 +26,7 @@ class QueryBuilderOperationSupport {
26
26
  self._parentQuery = null;
27
27
  self._isPartialQuery = false;
28
28
  self._activeOperations = [];
29
+ self._whereGroup = null;
29
30
  }
30
31
 
31
32
  static forClass(modelClass) {
@@ -413,6 +414,10 @@ class QueryBuilderOperationSupport {
413
414
  const shouldAdd = this.callOperationMethod(operation, 'onAdd', [this, args]);
414
415
 
415
416
  if (shouldAdd) {
417
+ if (this._whereGroup) {
418
+ operation.whereGroup = this._whereGroup;
419
+ }
420
+
416
421
  if (this._activeOperations.length) {
417
422
  const { operation: parentOperation, hookName } = last(this._activeOperations);
418
423
  parentOperation.addChildOperation(hookName, operation);
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { parseFieldExpression } = require('../utils/parseFieldExpression');
3
+ const {
4
+ parseFieldExpression,
5
+ toPostgresJsonPathLiteral,
6
+ } = require('../utils/parseFieldExpression');
4
7
  const { isObject } = require('../utils/objectUtils');
5
8
 
6
9
  class ReferenceBuilder {
@@ -185,8 +188,7 @@ class ReferenceBuilder {
185
188
 
186
189
  if (this._parsedExpr.access.length > 0) {
187
190
  const extractor = this._cast ? '#>>' : '#>';
188
- const jsonFieldRef = this._parsedExpr.access.map((field) => field.ref).join(',');
189
- return `??${extractor}'{${jsonFieldRef}}'`;
191
+ return `??${extractor}${toPostgresJsonPathLiteral(this._parsedExpr.access)}`;
190
192
  } else {
191
193
  return '??';
192
194
  }
@@ -355,7 +355,19 @@ function normalizeAllRecursive(node, allRecusive) {
355
355
  }
356
356
 
357
357
  function normalizeChildNames(node) {
358
- return (node && node.$childNames && node.$childNames.slice()) || getChildNames(node);
358
+ const childNames = getChildNames(node);
359
+
360
+ if (node && node.$childNames) {
361
+ // The actual keys of the node are authoritative, since the node may be a
362
+ // modified copy of `graphExpressionObject()` with a stale `$childNames`.
363
+ // Keep the order of `$childNames` for the children that still exist.
364
+ return union(
365
+ node.$childNames.filter((childName) => childNames.includes(childName)),
366
+ childNames,
367
+ );
368
+ } else {
369
+ return childNames;
370
+ }
359
371
  }
360
372
 
361
373
  function findExpressionsAtPath(target, path, results) {
@@ -24,7 +24,13 @@ class StaticHookArguments {
24
24
 
25
25
  get asFindQuery() {
26
26
  return () => {
27
- return this[BUILDER_SYMBOL].toFindQuery().clearWithGraphFetched().runAfter(asArray);
27
+ // The `runAfter()` callbacks of the query being executed (including the
28
+ // ones installed by `throwIfNotFound()` and `traverse()`) are meant for
29
+ // its result, not for the result of this find query.
30
+ return this[BUILDER_SYMBOL].toFindQuery()
31
+ .clear('runAfter')
32
+ .clearWithGraphFetched()
33
+ .runAfter(toArray);
28
34
  };
29
35
  }
30
36
 
@@ -97,6 +103,11 @@ class StaticHookArguments {
97
103
  }
98
104
  }
99
105
 
106
+ // Like `asArray()`, but an empty `first()` result becomes an empty array.
107
+ function toArray(result) {
108
+ return result === undefined ? [] : asArray(result);
109
+ }
110
+
100
111
  function getRelation(op) {
101
112
  return op.relation;
102
113
  }
@@ -14,6 +14,7 @@ const { RelationExpression } = require('../RelationExpression');
14
14
  const { GraphNodeDbExistence } = require('./GraphNodeDbExistence');
15
15
  const { GraphData } = require('./GraphData');
16
16
  const { uniqBy, asArray, isObject } = require('../../utils/objectUtils');
17
+ const { deprecate } = require('../../utils/deprecate');
17
18
 
18
19
  class GraphUpsert {
19
20
  constructor({ rootModelClass, objects, upsertOptions }) {
@@ -28,6 +29,23 @@ class GraphUpsert {
28
29
  return { skipValidation: true };
29
30
  }
30
31
 
32
+ // A graph is written using many insert queries (one per batch and table),
33
+ // so there's no single insert query an `onConflict` clause could be applied
34
+ // to. Even applying it to the root table only would be unsound: rows skipped
35
+ // by `ignore()` aren't returned, which leaves the root models without ids,
36
+ // so that related rows would be inserted with missing or wrong foreign keys.
37
+ // The clause is ignored, and a warning is logged once per method.
38
+ // TODO: Throw an error instead in objection 4.0.
39
+ static warnOnConflict(builder, method) {
40
+ if (builder.has(isOnConflictOperation)) {
41
+ deprecate(
42
+ `onConflict(), ignore() and merge() are not supported by ${method}(). ` +
43
+ 'Insert the conflicting rows with a separate insert() query instead. ' +
44
+ 'This will throw in objection 4.0.',
45
+ );
46
+ }
47
+ }
48
+
31
49
  run(builder) {
32
50
  const modelClass = builder.modelClass();
33
51
  const graphOptions = new GraphOptions(this.upsertOpt);
@@ -326,6 +344,10 @@ function executeActions(builder, actions) {
326
344
  );
327
345
  }
328
346
 
347
+ function isOnConflictOperation(op) {
348
+ return op.name === 'onConflict' || op.name === 'ignore' || op.name === 'merge';
349
+ }
350
+
329
351
  function returnResult(objects, isArray) {
330
352
  return () => (isArray ? objects : objects[0]);
331
353
  }