objection 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +13 -13
  2. package/lib/model/AjvValidator.js +220 -13
  3. package/lib/model/Model.js +3 -1
  4. package/lib/model/modelId.js +10 -2
  5. package/lib/model/modelQueryProps.js +7 -0
  6. package/lib/queryBuilder/QueryBuilder.js +191 -8
  7. package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
  8. package/lib/queryBuilder/ReferenceBuilder.js +5 -3
  9. package/lib/queryBuilder/RelationExpression.js +13 -1
  10. package/lib/queryBuilder/StaticHookArguments.js +12 -1
  11. package/lib/queryBuilder/graph/GraphFetcher.js +1 -3
  12. package/lib/queryBuilder/graph/GraphOptions.js +30 -1
  13. package/lib/queryBuilder/graph/GraphUpsert.js +38 -1
  14. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
  15. package/lib/queryBuilder/graph/insert/GraphInsert.js +107 -8
  16. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -1
  17. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +8 -2
  18. package/lib/queryBuilder/join/JoinResultParser.js +22 -0
  19. package/lib/queryBuilder/join/RelationJoiner.js +80 -27
  20. package/lib/queryBuilder/join/utils.js +20 -2
  21. package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
  22. package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
  23. package/lib/queryBuilder/operations/InsertOperation.js +129 -13
  24. package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
  25. package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
  26. package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
  27. package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
  28. package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
  29. package/lib/queryBuilder/operations/eager/EagerOperation.js +4 -0
  30. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +2 -6
  31. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +10 -33
  32. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
  33. package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
  34. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +41 -34
  35. package/lib/queryBuilder/operations/select/Selection.js +95 -4
  36. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
  37. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
  38. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
  39. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
  40. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
  41. package/lib/queryBuilder/whereGroups.js +125 -0
  42. package/lib/relations/Relation.js +5 -2
  43. package/lib/relations/RelationFindOperation.js +46 -2
  44. package/lib/relations/RelationProperty.js +3 -2
  45. package/lib/relations/hasMany/HasManyRelateOperation.js +5 -2
  46. package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
  47. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
  48. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
  49. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
  50. package/lib/relations/manyToMany/ManyToManyRelation.js +104 -51
  51. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +4 -2
  52. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
  53. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
  54. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
  55. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
  56. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +12 -2
  57. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
  58. package/lib/utils/assert.js +9 -0
  59. package/lib/utils/deprecate.js +6 -0
  60. package/lib/utils/identifierMapping.js +15 -1
  61. package/lib/utils/parseFieldExpression.js +28 -0
  62. package/lib/utils/wrapDbError.js +36 -0
  63. package/package.json +8 -7
  64. package/typings/objection/index.d.ts +281 -29
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const { wrapError } = require('db-errors');
3
+ const { wrapDbError } = require('../utils/wrapDbError');
4
4
 
5
5
  const { raw } = require('./RawBuilder');
6
6
  const { createModifier } = require('../utils/createModifier');
7
7
  const { ValidationErrorType } = require('../model/ValidationError');
8
8
  const { isObject, isString, isFunction, last, flatten } = require('../utils/objectUtils');
9
9
  const { RelationExpression, DuplicateRelationError } = require('./RelationExpression');
10
- const { assertIdNotUndefined } = require('../utils/assert');
10
+ const { assertIdNotUndefined, assertHasIdColumn } = require('../utils/assert');
11
11
 
12
12
  const { Selection } = require('./operations/select/Selection');
13
13
 
@@ -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
  }
@@ -99,6 +125,8 @@ class QueryBuilder extends QueryBuilderBase {
99
125
  }
100
126
 
101
127
  fullIdColumnFor(modelClass) {
128
+ assertHasIdColumn(modelClass);
129
+
102
130
  const tableName = this.tableRefFor(modelClass);
103
131
  const idColumn = modelClass.getIdColumn();
104
132
 
@@ -191,19 +219,50 @@ class QueryBuilder extends QueryBuilderBase {
191
219
  }
192
220
 
193
221
  withGraphFetched(exp, options = {}) {
194
- return this._withGraph(exp, options, getWhereInEagerAlgorithm(this));
222
+ return this._withGraphUsing(exp, options, getWhereInEagerAlgorithm(this));
195
223
  }
196
224
 
197
225
  withGraphJoined(exp, options = {}) {
198
- return this._withGraph(exp, options, getJoinEagerAlgorithm(this));
226
+ return this._withGraphUsing(exp, options, getJoinEagerAlgorithm(this));
227
+ }
228
+
229
+ // Like `withGraphFetched()` or `withGraphJoined()` with `options.algorithm`
230
+ // set to 'fetch' or 'join'. Without it, the top-level relations that are
231
+ // already loaded by one of the algorithms are merged into it, and new ones
232
+ // use the most recently used algorithm.
233
+ withGraph(exp, { algorithm, ...options } = {}) {
234
+ if (algorithm !== undefined) {
235
+ return this._withGraphUsing(exp, options, getEagerAlgorithmByName(this, algorithm));
236
+ }
237
+
238
+ const parsedExp = parseRelationExpression(this.modelClass(), exp);
239
+
240
+ for (const [algorithm, groupExp] of groupByEagerAlgorithm(this, parsedExp)) {
241
+ this._withGraph(groupExp, options, algorithm);
242
+ }
243
+
244
+ return this;
245
+ }
246
+
247
+ _withGraphUsing(exp, options, algorithm) {
248
+ this._graphAlgorithm = algorithm;
249
+ return this._withGraph(exp, options, algorithm);
199
250
  }
200
251
 
201
252
  _withGraph(exp, options, algorithm) {
202
253
  const eagerOp = ensureEagerOperation(this, algorithm);
203
254
  const parsedExp = parseRelationExpression(this.modelClass(), exp);
204
255
 
256
+ // The `joinOperation` option only applies to the relations of this call,
257
+ // see `RelationJoiner`. All other options are shared by all calls.
258
+ const { joinOperation, ...graphOptions } = options;
259
+
205
260
  eagerOp.expression = eagerOp.expression.merge(parsedExp);
206
- eagerOp.graphOptions = { ...eagerOp.graphOptions, ...options };
261
+ eagerOp.graphOptions = { ...eagerOp.graphOptions, ...graphOptions };
262
+
263
+ if (joinOperation) {
264
+ eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
265
+ }
207
266
 
208
267
  checkMixedEager(this);
209
268
  checkEager(this);
@@ -355,6 +414,8 @@ class QueryBuilder extends QueryBuilderBase {
355
414
 
356
415
  builder._allowedGraphExpression = this._allowedGraphExpression;
357
416
  builder._graphModifiersAtPath = this._graphModifiersAtPath.slice();
417
+ builder._graphAlgorithm = this._graphAlgorithm;
418
+ builder._isJoinChildQuery = this._isJoinChildQuery;
358
419
  builder._findOperationOptions = this._findOperationOptions;
359
420
  builder._relatedQueryFor = this._relatedQueryFor;
360
421
 
@@ -376,8 +437,17 @@ class QueryBuilder extends QueryBuilderBase {
376
437
  return builder;
377
438
  }
378
439
 
440
+ // Registers a property of the result models that was selected internally,
441
+ // e.g. by an eager operation, without the user asking for it. Such properties
442
+ // are omitted from the result models before `runAfter` hooks are called.
443
+ addInternalSelection(prop) {
444
+ this._internalSelections.add(prop);
445
+ return this;
446
+ }
447
+
379
448
  clearWithGraph() {
380
449
  this.clear(EagerOperation);
450
+ this._graphAlgorithm = null;
381
451
  return this;
382
452
  }
383
453
 
@@ -424,11 +494,27 @@ class QueryBuilder extends QueryBuilderBase {
424
494
  async resultSize() {
425
495
  const knex = this.knex();
426
496
  const builder = this.clone().clear(/orderBy|offset|limit/);
497
+ const joinEagerOp = builder.findOperation(JoinEagerOperation);
498
+
499
+ if (joinEagerOp) {
500
+ // `withGraphJoined` needs the table metadata to build its selects.
501
+ await joinEagerOp.onBefore3(builder);
502
+ }
427
503
 
428
504
  const countQuery = knex.count('* as count').from((knexBuilder) => {
429
505
  // Also clear the order on the built knex query, since `orderBy` calls
430
506
  // can be added at build time, e.g. by a relation's `modify`.
431
- builder.toKnexQuery(knexBuilder).clearOrder().as('temp');
507
+ knexBuilder = builder.toKnexQuery(knexBuilder).clearOrder();
508
+
509
+ if (joinEagerOp) {
510
+ // The joins of `withGraphJoined` return one row per joined child.
511
+ // Count the distinct root models instead.
512
+ const tableRef = builder.tableRef();
513
+ const idColumns = builder.modelClass().getIdColumnArray();
514
+ knexBuilder.clearSelect().distinct(idColumns.map((col) => `${tableRef}.${col}`));
515
+ }
516
+
517
+ knexBuilder.as('temp');
432
518
  });
433
519
 
434
520
  if (this.internalOptions().debug) {
@@ -891,6 +977,26 @@ class QueryBuilder extends QueryBuilderBase {
891
977
  return this;
892
978
  }
893
979
 
980
+ updateById(id, modelOrObject) {
981
+ return this.findById(id)
982
+ .update(modelOrObject)
983
+ .runBefore((_, builder) => {
984
+ if (!builder.internalOptions().skipUndefined) {
985
+ assertIdNotUndefined(id, `undefined was passed to updateById`);
986
+ }
987
+ });
988
+ }
989
+
990
+ patchById(id, modelOrObject) {
991
+ return this.findById(id)
992
+ .patch(modelOrObject)
993
+ .runBefore((_, builder) => {
994
+ if (!builder.internalOptions().skipUndefined) {
995
+ assertIdNotUndefined(id, `undefined was passed to patchById`);
996
+ }
997
+ });
998
+ }
999
+
894
1000
  deleteById(id) {
895
1001
  return this.findById(id)
896
1002
  .delete()
@@ -1055,6 +1161,66 @@ function getGraphExpression(builder) {
1055
1161
  );
1056
1162
  }
1057
1163
 
1164
+ function getEagerAlgorithmByName(builder, name) {
1165
+ if (name === 'fetch') {
1166
+ return getWhereInEagerAlgorithm(builder);
1167
+ } else if (name === 'join') {
1168
+ return getJoinEagerAlgorithm(builder);
1169
+ } else {
1170
+ throw new Error(`unknown graph algorithm "${name}", expected "fetch" or "join"`);
1171
+ }
1172
+ }
1173
+
1174
+ function getEagerAlgorithmOfOperation(builder, eagerOp) {
1175
+ if (eagerOp instanceof JoinEagerOperation) {
1176
+ return getJoinEagerAlgorithm(builder);
1177
+ } else if (eagerOp instanceof NaiveEagerOperation) {
1178
+ return getNaiveEagerAlgorithm(builder);
1179
+ } else {
1180
+ return getWhereInEagerAlgorithm(builder);
1181
+ }
1182
+ }
1183
+
1184
+ // Splits `expression` by its top-level relations into one expression per eager
1185
+ // algorithm. Relations that are already loaded by one of the eager operations
1186
+ // keep its algorithm, new ones use the most recently used one.
1187
+ function groupByEagerAlgorithm(builder, expression) {
1188
+ const defaultAlgorithm = builder._graphAlgorithm || getWhereInEagerAlgorithm(builder);
1189
+ const { node } = expression;
1190
+ const rootProps = {};
1191
+ const groups = new Map();
1192
+
1193
+ for (const key of Object.keys(node)) {
1194
+ if (key.startsWith('$')) {
1195
+ rootProps[key] = node[key];
1196
+ }
1197
+ }
1198
+
1199
+ for (const childName of node.$childNames) {
1200
+ const eagerOp = findEagerOperations(builder).find(
1201
+ (op) => op.expression.node.$allRecursive || op.expression.node[childName],
1202
+ );
1203
+
1204
+ const algorithm = eagerOp ? getEagerAlgorithmOfOperation(builder, eagerOp) : defaultAlgorithm;
1205
+ let group = groups.get(algorithm);
1206
+
1207
+ if (!group) {
1208
+ group = { ...rootProps, $childNames: [] };
1209
+ groups.set(algorithm, group);
1210
+ }
1211
+
1212
+ group.$childNames.push(childName);
1213
+ group[childName] = node[childName];
1214
+ }
1215
+
1216
+ if (groups.size === 0) {
1217
+ // No top-level relations, e.g. `*` or an empty expression.
1218
+ return [[defaultAlgorithm, expression]];
1219
+ }
1220
+
1221
+ return [...groups].map(([algorithm, group]) => [algorithm, RelationExpression.create(group)]);
1222
+ }
1223
+
1058
1224
  function getWhereInEagerAlgorithm(builder) {
1059
1225
  return builder.modelClass().WhereInEagerAlgorithm;
1060
1226
  }
@@ -1200,6 +1366,7 @@ function afterExecute(builder, result) {
1200
1366
 
1201
1367
  promise = chainOperationHooks(promise, builder, 'onAfter1');
1202
1368
  promise = chainOperationHooks(promise, builder, 'onAfter2');
1369
+ promise = promise.then((result) => omitInternalSelections(builder, result));
1203
1370
 
1204
1371
  promise = chainHooks(promise, builder, builder.context().runAfter);
1205
1372
  promise = chainHooks(promise, builder, builder.internalContext().runAfter);
@@ -1209,6 +1376,22 @@ function afterExecute(builder, result) {
1209
1376
  return promise;
1210
1377
  }
1211
1378
 
1379
+ function omitInternalSelections(builder, result) {
1380
+ const props = builder._internalSelections;
1381
+
1382
+ if (props.size && Array.isArray(result) && isObject(result[0])) {
1383
+ const modelClass = builder.resultModelClass();
1384
+
1385
+ for (const model of result) {
1386
+ for (const prop of props) {
1387
+ modelClass.omitImpl(model, prop);
1388
+ }
1389
+ }
1390
+ }
1391
+
1392
+ return result;
1393
+ }
1394
+
1212
1395
  class ReturnImmediatelyException {
1213
1396
  constructor(value) {
1214
1397
  this.value = value;
@@ -1227,7 +1410,7 @@ function handleExecuteError(builder, err) {
1227
1410
  return Promise.resolve(err.value);
1228
1411
  }
1229
1412
 
1230
- let promise = Promise.reject(wrapError(err));
1413
+ let promise = Promise.reject(wrapDbError(err));
1231
1414
 
1232
1415
  builder.forEachOperation(true, (op) => {
1233
1416
  if (op.hasOnError()) {
@@ -1331,7 +1514,7 @@ function callOnBuildHooks(builder) {
1331
1514
  callOnBuildFuncs(builder, builder.internalContext().onBuild);
1332
1515
 
1333
1516
  builder.executeOnBuild();
1334
- return builder;
1517
+ return wrapWhereGroups(builder);
1335
1518
  }
1336
1519
 
1337
1520
  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
  }
@@ -153,9 +153,7 @@ function createManyToManyExtraSelectionIfNeeded(builder, column, node) {
153
153
  const extra = relation.joinTableExtras.find((extra) => extra.aliasCol === column);
154
154
 
155
155
  if (extra) {
156
- return `${builder.tableRefFor(relation.joinModelClass)}.${extra.joinTableCol} as ${
157
- extra.aliasCol
158
- }`;
156
+ return `${relation.joinTableAliasFor(builder)}.${extra.joinTableCol} as ${extra.aliasCol}`;
159
157
  }
160
158
  }
161
159
 
@@ -13,6 +13,20 @@ const INSERT_MISSING = 'insertMissing';
13
13
  const FETCH_STRATEGY = 'fetchStrategy';
14
14
  const ALLOW_REFS = 'allowRefs';
15
15
 
16
+ const OPTION_NAMES = [
17
+ NO_RELATE,
18
+ NO_UNRELATE,
19
+ NO_INSERT,
20
+ NO_UPDATE,
21
+ NO_DELETE,
22
+ UPDATE,
23
+ RELATE,
24
+ UNRELATE,
25
+ INSERT_MISSING,
26
+ FETCH_STRATEGY,
27
+ ALLOW_REFS,
28
+ ];
29
+
16
30
  const FetchStrategy = {
17
31
  OnlyIdentifiers: 'OnlyIdentifiers',
18
32
  Everything: 'Everything',
@@ -64,7 +78,7 @@ class GraphOptions {
64
78
  !getCurrentNode(node, graphData) &&
65
79
  !!node.parentEdge &&
66
80
  !!node.parentEdge.relation &&
67
- node.parentEdge.relation.hasRelateProp(node.obj) &&
81
+ hasRelateProp(node) &&
68
82
  graphData.nodeDbExistence.doesNodeExistInDb(node)
69
83
  );
70
84
  }
@@ -212,6 +226,20 @@ function getCurrentNode(node, graphData) {
212
226
  return graphData.currentGraph.nodeForNode(node);
213
227
  }
214
228
 
229
+ // For `HasMany` and `HasOne` relations, the relate prop is the id of the
230
+ // related model. Use the id state of the input graph, since the parent's
231
+ // insert copies the foreign key into the related model, which can complete
232
+ // a composite id that contains it and would turn an insert into a relate (#2544).
233
+ function hasRelateProp(node) {
234
+ const relation = node.parentEdge.relation;
235
+
236
+ if (relation.isObjectionHasManyRelation) {
237
+ return node.hasId;
238
+ } else {
239
+ return relation.hasRelateProp(node.obj);
240
+ }
241
+ }
242
+
215
243
  function getNode(currentNode, graph) {
216
244
  if (!graph || !currentNode) {
217
245
  return null;
@@ -222,5 +250,6 @@ function getNode(currentNode, graph) {
222
250
 
223
251
  module.exports = {
224
252
  GraphOptions,
253
+ OPTION_NAMES,
225
254
  FetchStrategy,
226
255
  };
@@ -8,16 +8,18 @@ const { GraphInsert } = require('./insert/GraphInsert');
8
8
  const { GraphPatch } = require('./patch/GraphPatch');
9
9
  const { GraphDelete } = require('./delete/GraphDelete');
10
10
  const { GraphRecursiveUpsert } = require('./recursiveUpsert/GraphRecursiveUpsert');
11
- const { GraphOptions } = require('./GraphOptions');
11
+ const { GraphOptions, OPTION_NAMES } = require('./GraphOptions');
12
12
  const { ValidationErrorType } = require('../../model/ValidationError');
13
13
  const { RelationExpression } = require('../RelationExpression');
14
14
  const { GraphNodeDbExistence } = require('./GraphNodeDbExistence');
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 }) {
20
21
  checkCanBeConvertedToModels(rootModelClass, objects);
22
+ warnOnUnknownOptions(upsertOptions);
21
23
 
22
24
  this.objects = rootModelClass.ensureModelArray(objects, GraphUpsert.modelOptions);
23
25
  this.isArray = Array.isArray(objects);
@@ -28,6 +30,23 @@ class GraphUpsert {
28
30
  return { skipValidation: true };
29
31
  }
30
32
 
33
+ // A graph is written using many insert queries (one per batch and table),
34
+ // so there's no single insert query an `onConflict` clause could be applied
35
+ // to. Even applying it to the root table only would be unsound: rows skipped
36
+ // by `ignore()` aren't returned, which leaves the root models without ids,
37
+ // so that related rows would be inserted with missing or wrong foreign keys.
38
+ // The clause is ignored, and a warning is logged once per method.
39
+ // TODO: Throw an error instead in objection 4.0.
40
+ static warnOnConflict(builder, method) {
41
+ if (builder.has(isOnConflictOperation)) {
42
+ deprecate(
43
+ `onConflict(), ignore() and merge() are not supported by ${method}(). ` +
44
+ 'Insert the conflicting rows with a separate insert() query instead. ' +
45
+ 'This will throw in objection 4.0.',
46
+ );
47
+ }
48
+ }
49
+
31
50
  run(builder) {
32
51
  const modelClass = builder.modelClass();
33
52
  const graphOptions = new GraphOptions(this.upsertOpt);
@@ -43,6 +62,20 @@ class GraphUpsert {
43
62
  }
44
63
  }
45
64
 
65
+ // Misspelled options are silently ignored, so the operation runs with the
66
+ // default behaviour instead (#84). A warning is logged once per option.
67
+ // TODO: Throw an error instead in objection 4.0.
68
+ function warnOnUnknownOptions(upsertOptions) {
69
+ for (const name of Object.keys(upsertOptions)) {
70
+ if (!OPTION_NAMES.includes(name)) {
71
+ deprecate(
72
+ `Unknown graph option "${name}" is ignored. ` +
73
+ `Valid options are: ${OPTION_NAMES.join(', ')}. This will throw in objection 4.0.`,
74
+ );
75
+ }
76
+ }
77
+ }
78
+
46
79
  function checkCanBeConvertedToModels(modelClass, objects) {
47
80
  asArray(objects).forEach((obj) => {
48
81
  if (!isObject(obj)) {
@@ -326,6 +359,10 @@ function executeActions(builder, actions) {
326
359
  );
327
360
  }
328
361
 
362
+ function isOnConflictOperation(op) {
363
+ return op.name === 'onConflict' || op.name === 'ignore' || op.name === 'merge';
364
+ }
365
+
329
366
  function returnResult(objects, isArray) {
330
367
  return () => (isArray ? objects : objects[0]);
331
368
  }
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const { GraphAction } = require('../GraphAction');
4
+ const { groupBy } = require('../../../utils/objectUtils');
5
+ const promiseUtils = require('../../../utils/promiseUtils');
6
+
7
+ /**
8
+ * Patches in the foreign keys of `BelongsToOneRelation` edges that `GraphInsert`
9
+ * had to defer to break a dependency cycle. The owner nodes of these edges were
10
+ * inserted without the foreign keys. `GraphInsertAction` already copied them to
11
+ * the owner objects once the related nodes were inserted, so all that's left to
12
+ * do is one patch query per owner.
13
+ */
14
+ class DeferredRelationGraphInsertAction extends GraphAction {
15
+ constructor(graphData, { edges }) {
16
+ super(graphData);
17
+
18
+ // `BelongsToOneRelation` edges whose owner node needs to be patched.
19
+ this.edges = edges;
20
+ }
21
+
22
+ run(builder) {
23
+ const edgesByOwner = groupBy(this.edges, (edge) => edge.ownerNode);
24
+ const owners = [...edgesByOwner.keys()];
25
+ const concurrency = this._getConcurrency(builder, owners);
26
+
27
+ return promiseUtils.map(
28
+ owners,
29
+ (owner) => this._patchOwner(builder, owner, edgesByOwner.get(owner)),
30
+ { concurrency },
31
+ );
32
+ }
33
+
34
+ _patchOwner(builder, ownerNode, edges) {
35
+ const { modelClass } = ownerNode;
36
+ const ownerIdProp = modelClass.getIdRelationProperty();
37
+ const patch = {};
38
+
39
+ for (const { relation } of edges) {
40
+ const { ownerProp } = relation;
41
+
42
+ ownerProp.forEach((i) => {
43
+ ownerProp.patch(patch, i, ownerProp.getProp(ownerNode.obj, i));
44
+ });
45
+ }
46
+
47
+ return modelClass
48
+ .query()
49
+ .childQueryOf(builder)
50
+ .patch(patch)
51
+ .whereInComposite(ownerIdProp.refs(builder), [ownerIdProp.getProps(ownerNode.obj)]);
52
+ }
53
+ }
54
+
55
+ module.exports = {
56
+ DeferredRelationGraphInsertAction,
57
+ };