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
@@ -8,9 +8,10 @@ const { ValidationErrorType } = require('../model/ValidationError');
8
8
  const { isObject, isString, isFunction, last, flatten } = require('../utils/objectUtils');
9
9
  const { RelationExpression, DuplicateRelationError } = require('./RelationExpression');
10
10
  const { assertIdNotUndefined, assertHasIdColumn } = require('../utils/assert');
11
- const { deprecate } = require('../utils/deprecate');
11
+ const { warnOnce } = require('../utils/warnings');
12
+ const { isMsSql } = require('../utils/knexUtils');
12
13
 
13
- const { Selection } = require('./operations/select/Selection');
14
+ const { Selection, isSameIdentifier } = require('./operations/select/Selection');
14
15
 
15
16
  const { QueryBuilderContext } = require('./QueryBuilderContext');
16
17
  const { QueryBuilderBase } = require('./QueryBuilderBase');
@@ -45,9 +46,11 @@ const { EagerOperation } = require('./operations/eager/EagerOperation');
45
46
  const { RangeOperation } = require('./operations/RangeOperation');
46
47
  const { FirstOperation } = require('./operations/FirstOperation');
47
48
  const { NoneOperation } = require('./operations/NoneOperation');
49
+ const { LimitPerParentOperation } = require('./operations/LimitPerParentOperation');
48
50
  const { FromOperation } = require('./operations/FromOperation');
49
51
  const { KnexOperation } = require('./operations/KnexOperation');
50
- const { wrapWhereGroups } = require('./whereGroups');
52
+ const { clearUnsupportedOrder } = require('./withoutOrder');
53
+ const { addWhereGroup } = require('./whereGroups');
51
54
 
52
55
  class QueryBuilder extends QueryBuilderBase {
53
56
  static init(self, modelClass) {
@@ -56,6 +59,7 @@ class QueryBuilder extends QueryBuilderBase {
56
59
  self._resultModelClass = null;
57
60
  self._explicitRejectValue = null;
58
61
  self._explicitResolveValue = null;
62
+ self._queryBatch = null;
59
63
  self._modifiers = {};
60
64
 
61
65
  self._allowedGraphExpression = null;
@@ -257,21 +261,29 @@ class QueryBuilder extends QueryBuilderBase {
257
261
  // The `joinOperation` option only applies to the relations of this call,
258
262
  // see `RelationJoiner`. All other options are shared by all calls.
259
263
  const { joinOperation, ...graphOptions } = options;
264
+ const isJoined = eagerOp instanceof JoinEagerOperation;
260
265
 
261
266
  eagerOp.expression = eagerOp.expression.merge(parsedExp);
262
267
  eagerOp.graphOptions = { ...eagerOp.graphOptions, ...graphOptions };
263
268
 
264
269
  if (joinOperation) {
265
- if (eagerOp instanceof JoinEagerOperation) {
270
+ if (isJoined) {
266
271
  eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
267
272
  } else {
268
- deprecate(
273
+ warnOnce(
269
274
  'The joinOperation option only applies to withGraphJoined() and is ignored for the ' +
270
275
  `fetched relations "${parsedExp.toString()}".`,
271
276
  );
272
277
  }
273
278
  }
274
279
 
280
+ if (graphOptions.limitPerParent && isJoined) {
281
+ warnOnce(
282
+ 'The limitPerParent option only applies to withGraphFetched() and is ignored for the ' +
283
+ `joined relations "${parsedExp.toString()}".`,
284
+ );
285
+ }
286
+
275
287
  checkMixedEager(this);
276
288
  checkEager(this);
277
289
  return this;
@@ -387,7 +399,7 @@ class QueryBuilder extends QueryBuilderBase {
387
399
  return this.everyOperation((op) => {
388
400
  if (op.constructor === SelectOperation) {
389
401
  // SelectOperations with zero selections are the ones that only have
390
- // raw items or other non-trivial selections.
402
+ // raw items or other non-trivial selections, or `distinctOn()`.
391
403
  return (
392
404
  op.selections.length > 0 &&
393
405
  op.selections.every((select) => {
@@ -501,39 +513,50 @@ class QueryBuilder extends QueryBuilderBase {
501
513
 
502
514
  async resultSize() {
503
515
  const knex = this.knex();
504
- 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/);
505
520
  const joinEagerOp = builder.findOperation(JoinEagerOperation);
506
521
 
507
522
  if (joinEagerOp) {
508
523
  // `withGraphJoined` needs the table metadata to build its selects.
509
- await joinEagerOp.onBefore3(builder);
524
+ await joinEagerOp.fetchTableMetadata(builder);
525
+
526
+ // To-many joins return one row per joined child, so the root models are
527
+ // counted by their distinct ids, see below.
528
+ if (joinEagerOp.joiner.hasToManyJoins(builder)) {
529
+ assertHasIdColumn(builder.modelClass());
530
+ }
510
531
  }
511
532
 
533
+ const idColumns = builder.modelClass().getIdColumnArray();
534
+
512
535
  // Also clear the order on the built knex query, since `orderBy` calls
513
536
  // can be added at build time, e.g. by a relation's `modify`.
514
- const buildQuery = (knexBuilder) => builder.toKnexQuery(knexBuilder).clearOrder();
537
+ const buildQuery = (knexBuilder) => {
538
+ const query = builder.toKnexQuery(knexBuilder);
539
+ return shouldKeepOrder ? query : query.clearOrder();
540
+ };
515
541
 
516
542
  const countQuery = knex.count('* as count').from((knexBuilder) => {
517
- if (!joinEagerOp) {
543
+ if (!joinEagerOp || idColumns.length === 0) {
544
+ // Without ids, the joins are all to-one and return one row per root
545
+ // model.
518
546
  buildQuery(knexBuilder);
547
+ } else if (refersToSelections(builder) || builder.has('distinctOn')) {
548
+ // `having`, and `groupBy` with an alias, may refer to aliased
549
+ // selections, and clearing the selections would also clear
550
+ // `distinctOn`, so keep them and select the distinct ids from the
551
+ // result instead.
552
+ knexBuilder.distinct(idColumns).from((knexBuilder) => {
553
+ buildQuery(knexBuilder).as('inner');
554
+ });
519
555
  } else {
520
- // The joins of `withGraphJoined` return one row per joined child.
521
- // Count the distinct root models instead.
522
- const idColumns = builder.modelClass().getIdColumnArray();
523
-
524
- if (refersToSelections(builder)) {
525
- // `having`, and `groupBy` with an alias, may refer to aliased
526
- // selections, so keep them and select the distinct ids from the
527
- // result instead.
528
- knexBuilder.distinct(idColumns).from((knexBuilder) => {
529
- buildQuery(knexBuilder).as('inner');
530
- });
531
- } else {
532
- const tableRef = builder.tableRef();
533
- buildQuery(knexBuilder)
534
- .clearSelect()
535
- .distinct(idColumns.map((col) => `${tableRef}.${col}`));
536
- }
556
+ const tableRef = builder.tableRef();
557
+ buildQuery(knexBuilder)
558
+ .clearSelect()
559
+ .distinct(idColumns.map((col) => `${tableRef}.${col}`));
537
560
  }
538
561
 
539
562
  knexBuilder.as('temp');
@@ -585,7 +608,7 @@ class QueryBuilder extends QueryBuilderBase {
585
608
  let selectionInstance = null;
586
609
 
587
610
  this.forEachOperation(true, (op) => {
588
- if (op.constructor === SelectOperation) {
611
+ if (op.constructor === SelectOperation && !op.isDistinctOn) {
589
612
  selectionInstance = op.findSelection(this, selection);
590
613
  noSelectStatements = false;
591
614
 
@@ -637,9 +660,9 @@ class QueryBuilder extends QueryBuilderBase {
637
660
  } else {
638
661
  if (foundSelection.column === '*') {
639
662
  // * selects the columns with their column names as aliases.
640
- return selection.column === alias;
663
+ return isSameIdentifier(this, selection.column, alias);
641
664
  } else {
642
- return foundSelection.name === alias;
665
+ return isSameIdentifier(this, foundSelection.name, alias);
643
666
  }
644
667
  }
645
668
  }
@@ -1000,7 +1023,9 @@ class QueryBuilder extends QueryBuilderBase {
1000
1023
  }
1001
1024
 
1002
1025
  updateById(id, modelOrObject) {
1003
- 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))
1004
1029
  .update(modelOrObject)
1005
1030
  .runBefore((_, builder) => {
1006
1031
  if (!builder.internalOptions().skipUndefined) {
@@ -1010,7 +1035,7 @@ class QueryBuilder extends QueryBuilderBase {
1010
1035
  }
1011
1036
 
1012
1037
  patchById(id, modelOrObject) {
1013
- return this.findById(id)
1038
+ return addWhereGroup(this, () => this.findById(id))
1014
1039
  .patch(modelOrObject)
1015
1040
  .runBefore((_, builder) => {
1016
1041
  if (!builder.internalOptions().skipUndefined) {
@@ -1020,7 +1045,7 @@ class QueryBuilder extends QueryBuilderBase {
1020
1045
  }
1021
1046
 
1022
1047
  deleteById(id) {
1023
- return this.findById(id)
1048
+ return addWhereGroup(this, () => this.findById(id))
1024
1049
  .delete()
1025
1050
  .runBefore((_, builder) => {
1026
1051
  if (!builder.internalOptions().skipUndefined) {
@@ -1325,14 +1350,14 @@ function checkMixedEager(builder) {
1325
1350
 
1326
1351
  if (joinNode.$allRecursive || fetchNode.$allRecursive) {
1327
1352
  throw new Error(
1328
- 'relation expression `*` cannot be used when mixing withGraphJoined and withGraphFetched',
1353
+ 'relation expression "*" cannot be used when mixing withGraphJoined and withGraphFetched',
1329
1354
  );
1330
1355
  }
1331
1356
 
1332
1357
  for (const childName of joinNode.$childNames) {
1333
1358
  if (fetchNode.$childNames.includes(childName)) {
1334
1359
  throw new Error(
1335
- `relation \`${childName}\` cannot be loaded with both withGraphJoined and withGraphFetched`,
1360
+ `relation "${childName}" cannot be loaded with both withGraphJoined and withGraphFetched`,
1336
1361
  );
1337
1362
  }
1338
1363
  }
@@ -1382,10 +1407,13 @@ function beforeExecute(builder) {
1382
1407
  return promise;
1383
1408
  }
1384
1409
 
1385
- function doExecute(builder) {
1410
+ async function doExecute(builder) {
1386
1411
  let promise = Promise.resolve();
1387
1412
 
1388
1413
  builder = callOnBuildHooks(builder);
1414
+ // Unlike `onBuild`, `onAfterBuild` can be async, as it only runs when the
1415
+ // query is executed, not when it's only built, e.g. with `toKnexQuery()`:
1416
+ await chainOperationHooks(promise, builder, 'onAfterBuild');
1389
1417
  const queryExecutorOperation = findQueryExecutorOperation(builder);
1390
1418
 
1391
1419
  const explicitRejectValue = builder._explicitRejectValue;
@@ -1398,7 +1426,7 @@ function doExecute(builder) {
1398
1426
  } else if (queryExecutorOperation !== null) {
1399
1427
  promise = Promise.resolve(queryExecutorOperation.queryExecutor(builder));
1400
1428
  } else {
1401
- promise = Promise.resolve(buildKnexQuery(builder));
1429
+ promise = executeKnexQuery(builder);
1402
1430
 
1403
1431
  promise = chainOperationHooks(promise, builder, 'onRawResult');
1404
1432
  promise = promise.then((result) => createModels(result, builder));
@@ -1407,6 +1435,30 @@ function doExecute(builder) {
1407
1435
  return promise;
1408
1436
  }
1409
1437
 
1438
+ // An operation can split the query into batches, e.g. to stay below the number
1439
+ // of values a database accepts in one statement. Build and execute the knex
1440
+ // query once per batch then, with the operation reading the current batch from
1441
+ // `builder._queryBatch`, and merge the counts or rows of all batches.
1442
+ async function executeKnexQuery(builder) {
1443
+ const operation = builder.findOperation((op) => op.hasQueryBatches());
1444
+ const batches = operation?.queryBatches(builder);
1445
+
1446
+ if (batches) {
1447
+ const results = [];
1448
+
1449
+ for (const batch of batches) {
1450
+ builder._queryBatch = batch;
1451
+ results.push(await buildKnexQuery(builder));
1452
+ }
1453
+
1454
+ return results.reduce((merged, result) =>
1455
+ Array.isArray(merged) ? merged.concat(result) : merged + result,
1456
+ );
1457
+ } else {
1458
+ return buildKnexQuery(builder);
1459
+ }
1460
+ }
1461
+
1410
1462
  function afterExecute(builder, result) {
1411
1463
  let promise = Promise.resolve(result);
1412
1464
 
@@ -1560,7 +1612,13 @@ function callOnBuildHooks(builder) {
1560
1612
  callOnBuildFuncs(builder, builder.internalContext().onBuild);
1561
1613
 
1562
1614
  builder.executeOnBuild();
1563
- return wrapWhereGroups(builder);
1615
+
1616
+ // Clear the order after all hooks, as any of them may add one.
1617
+ if (builder.isUpdate()) {
1618
+ clearUnsupportedOrder(builder);
1619
+ }
1620
+
1621
+ return builder;
1564
1622
  }
1565
1623
 
1566
1624
  function callOnBuildFuncs(builder, func) {
@@ -1595,6 +1653,12 @@ function buildKnexQuery(builder, knexBuilder = builder.knex().queryBuilder()) {
1595
1653
  }
1596
1654
  }
1597
1655
 
1656
+ const limitPerParentOperation = builder.findOperation(LimitPerParentOperation);
1657
+
1658
+ if (limitPerParentOperation) {
1659
+ knexBuilder = limitPerParentOperation.onAfterBuildKnex(knexBuilder, builder);
1660
+ }
1661
+
1598
1662
  return knexBuilder;
1599
1663
  }
1600
1664
 
@@ -1604,6 +1668,10 @@ function setDefaultTable(builder, knexBuilder) {
1604
1668
 
1605
1669
  if (table === tableRef) {
1606
1670
  return knexBuilder.table(table);
1671
+ } else if (isMsSql(builder.knex()) && (builder.isUpdate() || builder.isDelete())) {
1672
+ // SQL Server can't alias the table of updates and deletes, but it can modify
1673
+ // it through a common table expression named like the alias.
1674
+ return knexBuilder.with(tableRef, (query) => query.select('*').from(table)).table(tableRef);
1607
1675
  } else {
1608
1676
  return knexBuilder.table(`${table} as ${tableRef}`);
1609
1677
  }
@@ -3,7 +3,8 @@
3
3
  const { isString, isFunction, isRegExp, mergeMaps, last } = require('../utils/objectUtils');
4
4
  const { QueryBuilderContextBase } = require('./QueryBuilderContextBase');
5
5
  const { QueryBuilderUserContext } = require('./QueryBuilderUserContext');
6
- const { deprecate } = require('../utils/deprecate');
6
+ const { deprecate } = require('../utils/warnings');
7
+ const { getWhereGroupOperations } = require('./whereGroups');
7
8
 
8
9
  const AllSelector = () => true;
9
10
  const SelectSelector =
@@ -489,8 +490,19 @@ class QueryBuilderOperationSupport {
489
490
  }
490
491
 
491
492
  executeOnBuildKnex(knexBuilder) {
492
- this.forEachOperation(true, (op) => {
493
- if (op.hasOnBuildKnex()) {
493
+ const whereGroupOps = getWhereGroupOperations(this);
494
+
495
+ // Visit the operations while building, so that operations added by
496
+ // `onBuildKnex` hooks are built too. Build each where group in place of its
497
+ // first operation, and skip the operations that the groups build.
498
+ this.forEachOperation(true, (it) => {
499
+ const op = whereGroupOps.has(it)
500
+ ? whereGroupOps.get(it)
501
+ : it.isAncestorInSet(whereGroupOps)
502
+ ? null
503
+ : it;
504
+
505
+ if (op?.hasOnBuildKnex()) {
494
506
  const newKnexBuilder = this.callOperationMethod(op, 'onBuildKnex', [knexBuilder, this]);
495
507
  // Default to the input knex builder for backwards compatibility
496
508
  // with QueryBuilder.onBuildKnex hooks.
@@ -26,9 +26,12 @@ class StaticHookArguments {
26
26
  return () => {
27
27
  // The `runAfter()` callbacks of the query being executed (including the
28
28
  // ones installed by `throwIfNotFound()` and `traverse()`) are meant for
29
- // its result, not for the result of this find query.
29
+ // its result, not for the result of this find query. The same goes for
30
+ // the value it was resolved with, e.g. the 0 of an update that only
31
+ // changes the join rows of a many-to-many relation.
30
32
  return this[BUILDER_SYMBOL].toFindQuery()
31
33
  .clear('runAfter')
34
+ .clearResolve()
32
35
  .clearWithGraphFetched()
33
36
  .runAfter(toArray);
34
37
  };
@@ -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 { parseFieldExpression, isFieldExpressionKey } = require('../../utils/parseFieldExpression');
7
9
 
8
10
  class GraphFetcher {
9
11
  /**
@@ -106,8 +108,14 @@ 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 = isFieldExpressionKey(key, declaredColumns)
116
+ ? parseFieldExpression(key).column
117
+ : key;
109
118
 
110
- for (const column of Object.keys(databaseJson)) {
111
119
  if (!shouldSelectColumn(column, selects, node)) {
112
120
  continue;
113
121
  }
@@ -17,21 +17,6 @@ const FETCH_STRATEGY = 'fetchStrategy';
17
17
  const ALLOW_REFS = 'allowRefs';
18
18
  const ALLOW_MARKERS = 'allowMarkers';
19
19
 
20
- const OPTION_NAMES = [
21
- NO_RELATE,
22
- NO_UNRELATE,
23
- NO_INSERT,
24
- NO_UPDATE,
25
- NO_DELETE,
26
- UPDATE,
27
- RELATE,
28
- UNRELATE,
29
- INSERT_MISSING,
30
- FETCH_STRATEGY,
31
- ALLOW_REFS,
32
- ALLOW_MARKERS,
33
- ];
34
-
35
20
  // The options that are matched against the relation path of each node, see
36
21
  // `_hasOption()`.
37
22
  const NODE_OPTION_NAMES = [
@@ -46,6 +31,8 @@ const NODE_OPTION_NAMES = [
46
31
  INSERT_MISSING,
47
32
  ];
48
33
 
34
+ const OPTION_NAMES = [...NODE_OPTION_NAMES, FETCH_STRATEGY, ALLOW_REFS, ALLOW_MARKERS];
35
+
49
36
  const FetchStrategy = {
50
37
  OnlyIdentifiers: 'OnlyIdentifiers',
51
38
  Everything: 'Everything',
@@ -55,14 +42,17 @@ const FetchStrategy = {
55
42
  class GraphOptions {
56
43
  // `rootRelationPath` is the relation path of the root node of a recursive
57
44
  // upsert in the original graph, against which the relation paths and
58
- // expressions of the options match.
59
- constructor(options, rootRelationPath = []) {
45
+ // expressions of the options match. `insertOnly` is only set by
46
+ // `insertGraph`, which never fetches, updates or removes existing rows.
47
+ constructor(options, rootRelationPath = [], insertOnly = false) {
60
48
  if (options instanceof GraphOptions) {
61
49
  this.options = options.options;
62
50
  this.rootRelationPath = options.rootRelationPath;
51
+ this.insertOnly = options.insertOnly;
63
52
  } else {
64
53
  this.options = parseRelationExpressions(options);
65
54
  this.rootRelationPath = rootRelationPath;
55
+ this.insertOnly = insertOnly;
66
56
  }
67
57
  }
68
58
 
@@ -79,17 +69,12 @@ class GraphOptions {
79
69
  }
80
70
 
81
71
  isInsertOnly() {
82
- // NO_RELATE is not in the list, since the `insert only` mode does
83
- // relate things that can be related using inserts.
84
- // TODO: Use a special key for this.
85
- return [NO_DELETE, NO_UPDATE, NO_UNRELATE, INSERT_MISSING].every((opt) => {
86
- return this.options[opt] === true;
87
- });
72
+ return this.insertOnly;
88
73
  }
89
74
 
90
75
  // Like `shouldRelate` but ignores settings that explicitly disable relate operations.
91
76
  shouldRelateIgnoreDisable(node, graphData) {
92
- if (node.toBeUnrelated || node.toBeDeleted) {
77
+ if (node.toBeUnrelatedOrDeleted) {
93
78
  return false;
94
79
  }
95
80
 
@@ -113,7 +98,7 @@ class GraphOptions {
113
98
 
114
99
  // Like `shouldInsert` but ignores settings that explicitly disable insert operations.
115
100
  shouldInsertIgnoreDisable(node, graphData) {
116
- if (node.toBeUnrelated || node.toBeDeleted) {
101
+ if (node.toBeUnrelatedOrDeleted) {
117
102
  return false;
118
103
  }
119
104
 
@@ -135,7 +120,7 @@ class GraphOptions {
135
120
  // Like `shouldPatch() || shouldUpdate()` but ignores settings that explicitly disable
136
121
  // update or patch operations.
137
122
  shouldPatchOrUpdateIgnoreDisable(node, graphData) {
138
- if (node.toBeUnrelated || node.toBeDeleted) {
123
+ if (node.toBeUnrelatedOrDeleted) {
139
124
  return false;
140
125
  }
141
126
 
@@ -176,8 +161,11 @@ class GraphOptions {
176
161
 
177
162
  return (
178
163
  !node &&
164
+ !hasIgnoredOtherModel(currentNode, graphData) &&
179
165
  !this._hasOption(currentNode, NO_UNRELATE) &&
180
- this.shouldUnrelateIgnoreDisable(currentNode)
166
+ (this.shouldUnrelateIgnoreDisable(currentNode) ||
167
+ // The unrelate replaces a delete, so `noDelete` disables it too.
168
+ (!this._hasOption(currentNode, NO_DELETE) && shouldUnrelateKeptRow(currentNode, graphData)))
181
169
  );
182
170
  }
183
171
 
@@ -188,8 +176,11 @@ class GraphOptions {
188
176
 
189
177
  return (
190
178
  !node &&
179
+ !hasIgnoredOtherModel(currentNode, graphData) &&
191
180
  !this._hasOption(currentNode, NO_DELETE) &&
192
- !this.shouldUnrelateIgnoreDisable(currentNode)
181
+ !this.shouldUnrelateIgnoreDisable(currentNode) &&
182
+ // Rows that the graph keeps in another place must not be deleted (#214).
183
+ !graphData.keptNodeForRow(currentNode)
193
184
  );
194
185
  }
195
186
 
@@ -197,6 +188,18 @@ class GraphOptions {
197
188
  return this.shouldInsert(node, graphData) || this.shouldRelate(node, graphData);
198
189
  }
199
190
 
191
+ // The graph ignores models that it neither inserts, relates nor patches, like
192
+ // models marked with `#unrelate` or `#delete` and models skipped by `noInsert`
193
+ // or `noRelate`. Inserted models have ids and can look like models to relate
194
+ // afterwards, so we check that they were inserted too.
195
+ shouldIgnore(node, graphData) {
196
+ return (
197
+ !node.userData.inserted &&
198
+ !this.shouldInsertOrRelate(node, graphData) &&
199
+ !this.shouldPatchOrUpdateIgnoreDisable(node, graphData)
200
+ );
201
+ }
202
+
200
203
  shouldDeleteOrUnrelate(currentNode, graphData) {
201
204
  return this.shouldDelete(currentNode, graphData) || this.shouldUnrelate(currentNode, graphData);
202
205
  }
@@ -279,27 +282,30 @@ function parseRelationExpression(optionName, expression) {
279
282
  }
280
283
 
281
284
  // Relation paths are matched by relation name, so aliases would never match.
282
- const alias = findAlias(parsedExpression.node);
285
+ const alias = findNode(parsedExpression.node, (node) => node.$name !== node.$relation);
283
286
 
284
287
  if (alias) {
285
288
  throw createError(`aliases like "${alias.$relation} as ${alias.$name}" are not supported`);
286
289
  }
287
290
 
291
+ // `*` and `^` only have a meaning at the end of a relation path. Anywhere else
292
+ // they are parsed as relation names that would never match.
293
+ const token = findNode(parsedExpression.node, (node) => /^(\*|\^\d*)$/.test(node.$relation));
294
+
295
+ if (token) {
296
+ throw createError(`"${token.$relation}" is only supported at the end of a relation path`);
297
+ }
298
+
288
299
  return parsedExpression;
289
300
  }
290
301
 
291
- function findAlias(node) {
302
+ function findNode(node, predicate) {
292
303
  for (const childName of node.$childNames) {
293
304
  const child = node[childName];
305
+ const found = predicate(child) ? child : findNode(child, predicate);
294
306
 
295
- if (child.$name !== child.$relation) {
296
- return child;
297
- }
298
-
299
- const alias = findAlias(child);
300
-
301
- if (alias) {
302
- return alias;
307
+ if (found) {
308
+ return found;
303
309
  }
304
310
  }
305
311
 
@@ -359,6 +365,39 @@ function hasRelateProp(node) {
359
365
  }
360
366
  }
361
367
 
368
+ // An ignored model that isn't the current model of a BelongsToOneRelation
369
+ // doesn't replace it, so we keep the current one instead of removing it as
370
+ // missing.
371
+ function hasIgnoredOtherModel(currentNode, graphData) {
372
+ const { relation } = currentNode.parentEdge;
373
+ const parentNode = getNode(currentNode.parentNode, graphData.graph);
374
+
375
+ return (
376
+ relation.isObjectionBelongsToOneRelation &&
377
+ !!parentNode &&
378
+ parentNode.edges.some(
379
+ (edge) =>
380
+ edge.relation === relation &&
381
+ edge.isOwnerNode(parentNode) &&
382
+ graphData.graphOptions.shouldIgnore(edge.relatedNode, graphData),
383
+ )
384
+ );
385
+ }
386
+
387
+ // A row that the graph keeps in another place is unrelated here instead of
388
+ // being deleted, unless it's a has-many row that the graph relates through the
389
+ // same relation: That relate moves it by overwriting the foreign key, which
390
+ // may not be nullable.
391
+ function shouldUnrelateKeptRow(currentNode, graphData) {
392
+ const keptNode = graphData.keptNodeForRow(currentNode);
393
+ const { relation } = currentNode.parentEdge;
394
+
395
+ return (
396
+ !!keptNode &&
397
+ !(relation.isObjectionHasManyRelation && keptNode.parentEdge?.relation === relation)
398
+ );
399
+ }
400
+
362
401
  function getNode(currentNode, graph) {
363
402
  if (!graph || !currentNode) {
364
403
  return null;