objection 3.1.5 → 3.2.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 (33) hide show
  1. package/README.md +3 -3
  2. package/lib/model/AjvValidator.js +40 -15
  3. package/lib/model/Model.js +3 -1
  4. package/lib/model/graph/ModelGraphNode.js +12 -0
  5. package/lib/model/modelToJson.js +2 -1
  6. package/lib/queryBuilder/QueryBuilder.js +156 -45
  7. package/lib/queryBuilder/RelationExpression.js +8 -8
  8. package/lib/queryBuilder/graph/GraphFetcher.js +3 -1
  9. package/lib/queryBuilder/graph/GraphOptions.js +21 -2
  10. package/lib/queryBuilder/graph/GraphUpsert.js +30 -0
  11. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +20 -14
  12. package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -0
  13. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +2 -0
  14. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +15 -3
  15. package/lib/queryBuilder/join/RelationJoiner.js +17 -0
  16. package/lib/queryBuilder/operations/DelegateOperation.js +4 -0
  17. package/lib/queryBuilder/operations/NoneOperation.js +37 -0
  18. package/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js +5 -0
  19. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +20 -0
  20. package/lib/queryBuilder/operations/eager/EagerOperation.js +3 -5
  21. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +8 -2
  22. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +15 -3
  23. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +8 -2
  24. package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +9 -0
  25. package/lib/relations/RelationProperty.js +7 -3
  26. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +1 -0
  27. package/lib/relations/belongsToOne/BelongsToOneRelateOperation.js +5 -0
  28. package/lib/relations/hasMany/HasManyRelateOperation.js +1 -0
  29. package/lib/relations/manyToMany/relate/ManyToManyRelateOperation.js +6 -0
  30. package/lib/utils/identifierMapping.js +8 -2
  31. package/lib/utils/objectUtils.js +9 -5
  32. package/package.json +14 -10
  33. package/typings/objection/index.d.ts +59 -40
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua)
2
2
 
3
- [![Tests](https://github.com/Vincit/objection.js/actions/workflows/test.yml/badge.svg)](https://github.com/Vincit/objection.js)
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
6
  # [Objection.js](https://vincit.github.io/objection.js)
@@ -9,7 +9,7 @@ Objection.js is an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping
9
9
 
10
10
  Even though ORM is the best commonly known acronym to describe objection, a more accurate description is to call it **a relational query builder**. You get all the benefits of an SQL query builder but also a powerful set of tools for working with relations.
11
11
 
12
- Objection.js is built on an SQL query builder called [knex](http://knexjs.org). All databases supported by knex are supported by objection.js. **SQLite3**, **Postgres** and **MySQL** are [thoroughly tested](https://github.com/Vincit/objection.js/actions).
12
+ Objection.js is built on an SQL query builder called [knex](http://knexjs.org). All databases supported by knex are supported by objection.js. **SQLite3**, **Postgres** and **MySQL** are [thoroughly tested](https://github.com/ditojs/objection/actions).
13
13
 
14
14
  What objection.js gives you:
15
15
 
@@ -17,7 +17,7 @@ What objection.js gives you:
17
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
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
19
  - **Easy to use [transactions](https://vincit.github.io/objection.js/guide/transactions.html)**
20
- - **Official [TypeScript](https://github.com/Vincit/objection.js/blob/main/typings/objection/index.d.ts) support**
20
+ - **Official [TypeScript](https://github.com/ditojs/objection/blob/main/typings/objection/index.d.ts) support**
21
21
  - **Optional [JSON schema](https://vincit.github.io/objection.js/guide/validation.html) validation**
22
22
  - **A way to [store complex documents](https://vincit.github.io/objection.js/guide/documents.html) as single rows**
23
23
 
@@ -35,6 +35,9 @@ class AjvValidator extends Validator {
35
35
  // A cache for the compiled validator functions.
36
36
  self.cache = new Map();
37
37
 
38
+ // The cache keys of the unmodified model class schemas.
39
+ self.modelCacheKeys = new WeakMap();
40
+
38
41
  const setupAjv = (ajv) => {
39
42
  if (conf.onCreateAjv) {
40
43
  conf.onCreateAjv(ajv);
@@ -92,12 +95,23 @@ class AjvValidator extends Validator {
92
95
  // Use the Ajv custom serializer if provided.
93
96
  const createCacheKey = this.ajvOptions.serialize || JSON.stringify;
94
97
 
95
- // Optimization for the common case where jsonSchema is never modified.
96
- // In that case we don't need to call the costly createCacheKey function.
97
- const cacheKey =
98
- jsonSchema === modelClass.getJsonSchema()
99
- ? modelClass.uniqueTag()
100
- : createCacheKey(jsonSchema);
98
+ let cacheKey;
99
+
100
+ if (jsonSchema === modelClass.getJsonSchema()) {
101
+ // Optimization for the common case where jsonSchema is never modified.
102
+ // The key is only computed once per model class. It includes the schema
103
+ // contents so that model classes with the same `uniqueTag()` but
104
+ // different schemas don't share validators, while bound model classes
105
+ // with equal schemas still do.
106
+ cacheKey = this.modelCacheKeys.get(modelClass);
107
+
108
+ if (cacheKey === undefined) {
109
+ cacheKey = `${modelClass.uniqueTag()}:${createCacheKey(jsonSchema)}`;
110
+ this.modelCacheKeys.set(modelClass, cacheKey);
111
+ }
112
+ } else {
113
+ cacheKey = createCacheKey(jsonSchema);
114
+ }
101
115
 
102
116
  let validators = this.cache.get(cacheKey);
103
117
  let validator = null;
@@ -263,24 +277,35 @@ function objectHasDefaults(obj) {
263
277
 
264
278
  function jsonSchemaWithoutRequired(jsonSchema) {
265
279
  const subSchemaProps = ['anyOf', 'oneOf', 'allOf', 'not', 'then', 'else', 'properties'];
280
+ // `definitions` was renamed to `$defs` in draft 2019-09. Support both.
281
+ const definitionProps = ['definitions', '$defs'];
266
282
  return Object.assign(
267
283
  omit(jsonSchema, ['required', ...subSchemaProps]),
268
284
  ...subSchemaProps.map((prop) => subSchemaWithoutRequired(jsonSchema, prop)),
269
- jsonSchema && jsonSchema.definitions && Object.keys(jsonSchema.definitions).length > 0
270
- ? {
271
- definitions: Object.assign(
272
- ...Object.keys(jsonSchema.definitions).map((prop) => ({
273
- [prop]: jsonSchemaWithoutRequired(jsonSchema.definitions[prop]),
274
- })),
275
- ),
276
- }
277
- : {},
285
+ ...definitionProps.map((prop) => definitionsWithoutRequired(jsonSchema, prop)),
278
286
  jsonSchema.discriminator && jsonSchema.discriminator.propertyName
279
287
  ? { required: [jsonSchema.discriminator.propertyName] }
280
288
  : {},
281
289
  );
282
290
  }
283
291
 
292
+ function definitionsWithoutRequired(jsonSchema, prop) {
293
+ const definitions = jsonSchema && jsonSchema[prop];
294
+
295
+ if (definitions && Object.keys(definitions).length > 0) {
296
+ return {
297
+ [prop]: Object.fromEntries(
298
+ Object.entries(definitions).map(([key, schema]) => [
299
+ key,
300
+ jsonSchemaWithoutRequired(schema),
301
+ ]),
302
+ ),
303
+ };
304
+ } else {
305
+ return {};
306
+ }
307
+ }
308
+
284
309
  function subSchemaWithoutRequired(jsonSchema, prop) {
285
310
  if (jsonSchema[prop]) {
286
311
  if (Array.isArray(jsonSchema[prop])) {
@@ -120,7 +120,7 @@ class Model {
120
120
  return json;
121
121
  }
122
122
 
123
- $formatJson(json) {
123
+ $formatJson(json, options) {
124
124
  return json;
125
125
  }
126
126
 
@@ -721,6 +721,8 @@ Model.uidProp = '#id';
721
721
  Model.uidRefProp = '#ref';
722
722
  Model.dbRefProp = '#dbRef';
723
723
  Model.propRefRegex = /#ref{([^\.]+)\.([^}]+)}/g;
724
+ Model.graphUnrelateProp = '#unrelate';
725
+ Model.graphDeleteProp = '#delete';
724
726
  Model.jsonAttributes = null;
725
727
  Model.cloneObjectAttributes = true;
726
728
  Model.virtualAttributes = null;
@@ -46,6 +46,18 @@ class ModelGraphNode {
46
46
  return this.obj[this.modelClass.dbRefProp];
47
47
  }
48
48
 
49
+ get toBeUnrelated() {
50
+ return this.obj[this.modelClass.graphUnrelateProp] === true;
51
+ }
52
+
53
+ get toBeDeleted() {
54
+ return this.obj[this.modelClass.graphDeleteProp] === true;
55
+ }
56
+
57
+ get toBeUnrelatedOrDeleted() {
58
+ return this.toBeUnrelated || this.toBeDeleted;
59
+ }
60
+
49
61
  get parentNode() {
50
62
  if (this.parentEdge) {
51
63
  return this.parentEdge.ownerNode;
@@ -15,10 +15,11 @@ function toJson(model, optIn) {
15
15
  pick: null,
16
16
  omitFromJson: model.$omitFromJson() || null,
17
17
  cloneObjects: modelClass.cloneObjectAttributes,
18
+ format: optIn && optIn.format,
18
19
  };
19
20
 
20
21
  let json = toExternalJsonImpl(model, opt);
21
- json = model.$formatJson(json);
22
+ json = model.$formatJson(json, opt.format);
22
23
 
23
24
  return json;
24
25
  }
@@ -43,6 +43,7 @@ const { SelectOperation } = require('./operations/select/SelectOperation');
43
43
  const { EagerOperation } = require('./operations/eager/EagerOperation');
44
44
  const { RangeOperation } = require('./operations/RangeOperation');
45
45
  const { FirstOperation } = require('./operations/FirstOperation');
46
+ const { NoneOperation } = require('./operations/NoneOperation');
46
47
  const { FromOperation } = require('./operations/FromOperation');
47
48
  const { KnexOperation } = require('./operations/KnexOperation');
48
49
 
@@ -56,6 +57,7 @@ class QueryBuilder extends QueryBuilderBase {
56
57
  self._modifiers = {};
57
58
 
58
59
  self._allowedGraphExpression = null;
60
+ self._graphModifiersAtPath = [];
59
61
  self._findOperationOptions = modelClass.defaultFindOptions;
60
62
  self._relatedQueryFor = null;
61
63
 
@@ -203,6 +205,7 @@ class QueryBuilder extends QueryBuilderBase {
203
205
  eagerOp.expression = eagerOp.expression.merge(parsedExp);
204
206
  eagerOp.graphOptions = { ...eagerOp.graphOptions, ...options };
205
207
 
208
+ checkMixedEager(this);
206
209
  checkEager(this);
207
210
  return this;
208
211
  }
@@ -223,28 +226,30 @@ class QueryBuilder extends QueryBuilderBase {
223
226
  }
224
227
 
225
228
  graphExpressionObject() {
226
- const eagerOp = this.findOperation(EagerOperation);
229
+ const expression = getGraphExpression(this);
227
230
 
228
- if (eagerOp && !eagerOp.expression.isEmpty) {
229
- return eagerOp.expression.toPojo();
231
+ if (expression && !expression.isEmpty) {
232
+ return expression.toPojo();
230
233
  } else {
231
234
  return null;
232
235
  }
233
236
  }
234
237
 
235
238
  graphModifiersAtPath() {
236
- const eagerOp = this.findOperation(EagerOperation);
239
+ const expression = getGraphExpression(this);
237
240
 
238
- if (eagerOp && !eagerOp.expression.isEmpty) {
239
- return eagerOp.modifiersAtPath.map((it) => Object.assign({}, it));
241
+ if (expression && !expression.isEmpty) {
242
+ return this._graphModifiersAtPath.map((it) => Object.assign({}, it));
240
243
  } else {
241
244
  return [];
242
245
  }
243
246
  }
244
247
 
245
248
  modifyGraph(path, modifier) {
246
- const eagerOp = ensureEagerOperation(this);
247
- eagerOp.modifiersAtPath.push({ path, modifier });
249
+ // The modifiers are shared by all eager operations. They only take effect
250
+ // for the paths that exist in each operation's expression.
251
+ ensureEagerOperation(this);
252
+ this._graphModifiersAtPath.push({ path, modifier });
248
253
  return this;
249
254
  }
250
255
 
@@ -301,8 +306,7 @@ class QueryBuilder extends QueryBuilderBase {
301
306
  }
302
307
 
303
308
  hasWithGraph() {
304
- const eagerOp = this.findOperation(EagerOperation);
305
- return !!eagerOp && !eagerOp.expression.isEmpty;
309
+ return findEagerOperations(this).some((eagerOp) => !eagerOp.expression.isEmpty);
306
310
  }
307
311
 
308
312
  isSelectAll() {
@@ -350,6 +354,7 @@ class QueryBuilder extends QueryBuilderBase {
350
354
  builder._modifiers = { ...this._modifiers };
351
355
 
352
356
  builder._allowedGraphExpression = this._allowedGraphExpression;
357
+ builder._graphModifiersAtPath = this._graphModifiersAtPath.slice();
353
358
  builder._findOperationOptions = this._findOperationOptions;
354
359
  builder._relatedQueryFor = this._relatedQueryFor;
355
360
 
@@ -421,7 +426,9 @@ class QueryBuilder extends QueryBuilderBase {
421
426
  const builder = this.clone().clear(/orderBy|offset|limit/);
422
427
 
423
428
  const countQuery = knex.count('* as count').from((knexBuilder) => {
424
- builder.toKnexQuery(knexBuilder).as('temp');
429
+ // Also clear the order on the built knex query, since `orderBy` calls
430
+ // can be added at build time, e.g. by a relation's `modify`.
431
+ builder.toKnexQuery(knexBuilder).clearOrder().as('temp');
425
432
  });
426
433
 
427
434
  if (this.internalOptions().debug) {
@@ -808,6 +815,10 @@ class QueryBuilder extends QueryBuilderBase {
808
815
  return this.addOperation(new FirstOperation('first'), args);
809
816
  }
810
817
 
818
+ none() {
819
+ return this.addOperation(new NoneOperation('none'), []);
820
+ }
821
+
811
822
  joinRelated(expression, options) {
812
823
  ensureJoinRelatedOperation(this, 'innerJoin').addCall({
813
824
  expression,
@@ -922,6 +933,10 @@ class QueryBuilder extends QueryBuilderBase {
922
933
  return this.addOperation(new FromOperation('from'), args);
923
934
  }
924
935
 
936
+ updateFrom(...args) {
937
+ return this.addOperation(new KnexOperation('updateFrom'), args);
938
+ }
939
+
925
940
  table(...args) {
926
941
  return this.addOperation(new FromOperation('table'), args);
927
942
  }
@@ -930,6 +945,15 @@ class QueryBuilder extends QueryBuilderBase {
930
945
  if (arguments.length === 0) {
931
946
  return this._relatedQueryFor;
932
947
  } else {
948
+ // The write operation factories installed by the static `relatedQuery()`
949
+ // read `for()` when the write method is called. Setting it afterwards
950
+ // would make the query silently target the wrong rows.
951
+ if (this._findOperationFactory !== findOperationFactory && !this.isFind()) {
952
+ throw new Error(
953
+ 'for() must be called before insert, update, patch, delete, relate or unrelate on queries created using the static relatedQuery method',
954
+ );
955
+ }
956
+
933
957
  this._relatedQueryFor = relatedQueryFor;
934
958
  return this;
935
959
  }
@@ -952,48 +976,83 @@ function getTableName(modelClassOrTableName) {
952
976
  }
953
977
  }
954
978
 
979
+ // A query builder can hold at most one eager operation of each kind: one
980
+ // `JoinEagerOperation` for `withGraphJoined` and one `WhereInEagerOperation`
981
+ // (or its subclass `NaiveEagerOperation`) for `withGraphFetched`.
955
982
  function ensureEagerOperation(builder, algorithm = null) {
956
983
  const modelClass = builder.modelClass();
957
984
  const defaultGraphOptions = modelClass.getDefaultGraphOptions();
958
- const eagerOp = builder.findOperation(EagerOperation);
959
985
 
960
- if (algorithm) {
961
- const EagerOperationClass = getOperationClassForEagerAlgorithm(builder, algorithm);
986
+ if (!algorithm) {
987
+ const eagerOp = builder.findOperation(EagerOperation);
962
988
 
963
- if (eagerOp instanceof EagerOperationClass) {
989
+ if (eagerOp) {
964
990
  return eagerOp;
965
- } else {
966
- const newEagerOp = new EagerOperationClass('eager', {
967
- defaultGraphOptions,
968
- });
991
+ }
969
992
 
970
- if (eagerOp) {
971
- newEagerOp.cloneFrom(eagerOp);
972
- }
993
+ algorithm = getWhereInEagerAlgorithm(builder);
994
+ }
973
995
 
974
- builder.clear(EagerOperation);
975
- builder.addOperation(newEagerOp);
996
+ const EagerOperationClass = getOperationClassForEagerAlgorithm(builder, algorithm);
997
+ const EagerOperationKind = getEagerOperationKind(EagerOperationClass);
998
+ const eagerOp = builder.findOperation(EagerOperationKind);
976
999
 
977
- return newEagerOp;
978
- }
979
- } else {
980
- if (eagerOp) {
981
- return eagerOp;
982
- } else {
983
- const EagerOperationClass = getOperationClassForEagerAlgorithm(
984
- builder,
985
- getWhereInEagerAlgorithm(builder),
986
- );
1000
+ if (eagerOp && eagerOp.constructor === EagerOperationClass) {
1001
+ return eagerOp;
1002
+ }
987
1003
 
988
- const newEagerOp = new EagerOperationClass('eager', {
989
- defaultGraphOptions,
990
- });
1004
+ const newEagerOp = new EagerOperationClass('eager', {
1005
+ defaultGraphOptions,
1006
+ });
1007
+
1008
+ if (eagerOp) {
1009
+ // Switching between algorithms of the same kind, e.g. from
1010
+ // `WhereInEagerOperation` to `NaiveEagerOperation`.
1011
+ newEagerOp.cloneFrom(eagerOp);
1012
+ builder.clear(EagerOperationKind);
1013
+ } else if (!builder.has(EagerOperation)) {
1014
+ // The `modifyGraph` modifiers only live as long as an eager operation.
1015
+ builder._graphModifiersAtPath = [];
1016
+ }
991
1017
 
992
- builder.addOperation(newEagerOp);
1018
+ builder.addOperation(newEagerOp);
993
1019
 
994
- return newEagerOp;
995
- }
1020
+ // Keep the joined operation before the fetched one (see `findEagerOperations`),
1021
+ // even if it is added after `addImplicitOperations`, e.g. in `runBefore`.
1022
+ const fetchOp =
1023
+ EagerOperationKind === JoinEagerOperation && builder.findOperation(WhereInEagerOperation);
1024
+
1025
+ if (fetchOp) {
1026
+ builder.removeOperation(fetchOp).addOperation(fetchOp);
996
1027
  }
1028
+
1029
+ return newEagerOp;
1030
+ }
1031
+
1032
+ function getEagerOperationKind(EagerOperationClass) {
1033
+ return EagerOperationClass === JoinEagerOperation ? JoinEagerOperation : WhereInEagerOperation;
1034
+ }
1035
+
1036
+ function findEagerOperations(builder) {
1037
+ // Joined operations first, so that their `onBuild` runs before the fetched
1038
+ // one's. See `WhereInEagerOperation.onBuild`.
1039
+ return [
1040
+ builder.findOperation(JoinEagerOperation),
1041
+ builder.findOperation(WhereInEagerOperation),
1042
+ ].filter(Boolean);
1043
+ }
1044
+
1045
+ function getGraphExpression(builder) {
1046
+ const eagerOps = findEagerOperations(builder);
1047
+
1048
+ if (eagerOps.length === 0) {
1049
+ return null;
1050
+ }
1051
+
1052
+ return eagerOps.reduce(
1053
+ (expression, eagerOp) => expression.merge(eagerOp.expression),
1054
+ RelationExpression.create(),
1055
+ );
997
1056
  }
998
1057
 
999
1058
  function getWhereInEagerAlgorithm(builder) {
@@ -1036,14 +1095,44 @@ function parseRelationExpression(modelClass, exp) {
1036
1095
  }
1037
1096
  }
1038
1097
 
1098
+ function checkMixedEager(builder) {
1099
+ const joinOp = builder.findOperation(JoinEagerOperation);
1100
+ const fetchOp = builder.findOperation(WhereInEagerOperation);
1101
+
1102
+ if (!joinOp || !fetchOp) {
1103
+ return;
1104
+ }
1105
+
1106
+ const joinNode = joinOp.expression.node;
1107
+ const fetchNode = fetchOp.expression.node;
1108
+ const isUsed = (node) => node.$allRecursive || node.$childNames.length > 0;
1109
+
1110
+ if (!isUsed(joinNode) || !isUsed(fetchNode)) {
1111
+ return;
1112
+ }
1113
+
1114
+ if (joinNode.$allRecursive || fetchNode.$allRecursive) {
1115
+ throw new Error(
1116
+ 'relation expression `*` cannot be used when mixing withGraphJoined and withGraphFetched',
1117
+ );
1118
+ }
1119
+
1120
+ for (const childName of joinNode.$childNames) {
1121
+ if (fetchNode.$childNames.includes(childName)) {
1122
+ throw new Error(
1123
+ `relation \`${childName}\` cannot be loaded with both withGraphJoined and withGraphFetched`,
1124
+ );
1125
+ }
1126
+ }
1127
+ }
1128
+
1039
1129
  function checkEager(builder) {
1040
- const eagerOp = builder.findOperation(EagerOperation);
1130
+ const expr = getGraphExpression(builder);
1041
1131
 
1042
- if (!eagerOp) {
1132
+ if (!expr) {
1043
1133
  return;
1044
1134
  }
1045
1135
 
1046
- const expr = eagerOp.expression;
1047
1136
  const allowedExpr = builder.allowedGraphExpression();
1048
1137
 
1049
1138
  if (expr.numChildren > 0 && allowedExpr && !allowedExpr.isSubExpression(expr)) {
@@ -1065,6 +1154,7 @@ function findQueryExecutorOperation(builder) {
1065
1154
  function beforeExecute(builder) {
1066
1155
  let promise = Promise.resolve();
1067
1156
 
1157
+ checkRelatedQueryFor(builder);
1068
1158
  builder = addImplicitOperations(builder);
1069
1159
 
1070
1160
  // Resolve all before hooks before building and executing the query
@@ -1181,6 +1271,7 @@ function ensureJoinRelatedOperation(builder, joinOperation) {
1181
1271
  }
1182
1272
 
1183
1273
  function prebuildQuery(builder) {
1274
+ checkRelatedQueryFor(builder);
1184
1275
  builder = addImplicitOperations(builder);
1185
1276
  builder = callOnBuildHooks(builder);
1186
1277
 
@@ -1193,6 +1284,17 @@ function prebuildQuery(builder) {
1193
1284
  }
1194
1285
  }
1195
1286
 
1287
+ function checkRelatedQueryFor(builder) {
1288
+ // `for()` is only consumed by the operation factories installed by the static
1289
+ // `relatedQuery()`. The default ones ignore it, which would silently make the
1290
+ // query target all rows of the table.
1291
+ if (builder.for() !== null && builder._findOperationFactory === findOperationFactory) {
1292
+ throw new Error(
1293
+ 'for() can only be used with queries created using the static relatedQuery method',
1294
+ );
1295
+ }
1296
+ }
1297
+
1196
1298
  function addImplicitOperations(builder) {
1197
1299
  if (builder.isFind()) {
1198
1300
  // If no write operations have been called at this point this query is a
@@ -1215,10 +1317,13 @@ function addFindOperation(builder) {
1215
1317
  }
1216
1318
 
1217
1319
  function moveEagerOperationToEnd(builder) {
1218
- const eagerOp = builder.findOperation(EagerOperation);
1320
+ const eagerOps = findEagerOperations(builder);
1219
1321
 
1220
1322
  builder.clear(EagerOperation);
1221
- builder.addOperation(eagerOp);
1323
+
1324
+ for (const eagerOp of eagerOps) {
1325
+ builder.addOperation(eagerOp);
1326
+ }
1222
1327
  }
1223
1328
 
1224
1329
  function callOnBuildHooks(builder) {
@@ -1240,6 +1345,12 @@ function callOnBuildFuncs(builder, func) {
1240
1345
  function buildKnexQuery(builder, knexBuilder = builder.knex().queryBuilder()) {
1241
1346
  knexBuilder = builder.executeOnBuildKnex(knexBuilder);
1242
1347
 
1348
+ const noneOperation = builder.findOperation(NoneOperation);
1349
+
1350
+ if (noneOperation) {
1351
+ knexBuilder = noneOperation.onAfterBuildKnex(knexBuilder);
1352
+ }
1353
+
1243
1354
  const fromOperation = builder.findLastOperation(QueryBuilderBase.FromSelector);
1244
1355
 
1245
1356
  if (!builder.isPartial()) {
@@ -229,6 +229,14 @@ function toString(node) {
229
229
  let childExpr = childNames.map((childName) => node[childName]).map(toString);
230
230
  let str = node.$relation;
231
231
 
232
+ if (node.$modify.length) {
233
+ str += `(${node.$modify.join(', ')})`;
234
+ }
235
+
236
+ if (node.$name !== node.$relation) {
237
+ str += ` as ${node.$name}`;
238
+ }
239
+
232
240
  if (node.$recursive) {
233
241
  if (isNumber(node.$recursive)) {
234
242
  str += '.^' + node.$recursive;
@@ -245,14 +253,6 @@ function toString(node) {
245
253
  childExpr = childExpr[0];
246
254
  }
247
255
 
248
- if (node.$modify.length) {
249
- str += `(${node.$modify.join(', ')})`;
250
- }
251
-
252
- if (node.$name !== node.$relation) {
253
- str += ` as ${node.$name}`;
254
- }
255
-
256
256
  if (childExpr) {
257
257
  if (str) {
258
258
  return `${str}.${childExpr}`;
@@ -141,7 +141,9 @@ function shouldSelectColumn(column, selects, node) {
141
141
  !selects.has(column) &&
142
142
  column !== modelClass.propertyNameToColumnName(modelClass.dbRefProp) &&
143
143
  column !== modelClass.propertyNameToColumnName(modelClass.uidRefProp) &&
144
- column !== modelClass.propertyNameToColumnName(modelClass.uidProp)
144
+ column !== modelClass.propertyNameToColumnName(modelClass.uidProp) &&
145
+ column !== modelClass.propertyNameToColumnName(modelClass.graphUnrelateProp) &&
146
+ column !== modelClass.propertyNameToColumnName(modelClass.graphDeleteProp)
145
147
  );
146
148
  }
147
149
 
@@ -51,6 +51,10 @@ class GraphOptions {
51
51
 
52
52
  // Like `shouldRelate` but ignores settings that explicitly disable relate operations.
53
53
  shouldRelateIgnoreDisable(node, graphData) {
54
+ if (node.toBeUnrelated || node.toBeDeleted) {
55
+ return false;
56
+ }
57
+
54
58
  if (node.isReference || node.isDbReference) {
55
59
  return true;
56
60
  }
@@ -71,6 +75,10 @@ class GraphOptions {
71
75
 
72
76
  // Like `shouldInsert` but ignores settings that explicitly disable insert operations.
73
77
  shouldInsertIgnoreDisable(node, graphData) {
78
+ if (node.toBeUnrelated || node.toBeDeleted) {
79
+ return false;
80
+ }
81
+
74
82
  return (
75
83
  !getCurrentNode(node, graphData) &&
76
84
  !this.shouldRelateIgnoreDisable(node, graphData) &&
@@ -89,6 +97,10 @@ class GraphOptions {
89
97
  // Like `shouldPatch() || shouldUpdate()` but ignores settings that explicitly disable
90
98
  // update or patch operations.
91
99
  shouldPatchOrUpdateIgnoreDisable(node, graphData) {
100
+ if (node.toBeUnrelated || node.toBeDeleted) {
101
+ return false;
102
+ }
103
+
92
104
  if (this.shouldRelate(node, graphData)) {
93
105
  // We should update all nodes that are going to be related. Note that
94
106
  // we don't actually update anything unless there is something to update
@@ -121,16 +133,23 @@ class GraphOptions {
121
133
  }
122
134
 
123
135
  shouldUnrelate(currentNode, graphData) {
136
+ const node = getNode(currentNode, graphData.graph);
137
+ if (node && node.toBeUnrelated) return true;
138
+
124
139
  return (
125
- !getNode(currentNode, graphData.graph) &&
140
+ !node &&
126
141
  !this._hasOption(currentNode, NO_UNRELATE) &&
127
142
  this.shouldUnrelateIgnoreDisable(currentNode)
128
143
  );
129
144
  }
130
145
 
131
146
  shouldDelete(currentNode, graphData) {
147
+ const node = getNode(currentNode, graphData.graph);
148
+ // `#unrelate` takes precedence over `#delete` if both are set.
149
+ if (node && node.toBeDeleted && !node.toBeUnrelated) return true;
150
+
132
151
  return (
133
- !getNode(currentNode, graphData.graph) &&
152
+ !node &&
134
153
  !this._hasOption(currentNode, NO_DELETE) &&
135
154
  !this.shouldUnrelateIgnoreDisable(currentNode)
136
155
  );
@@ -91,6 +91,7 @@ function pruneGraphs() {
91
91
 
92
92
  if (!graphData.graphOptions.isInsertOnly()) {
93
93
  pruneDeletedBranches(graphData);
94
+ pruneUnrelatedOrDeletedBranches(graphData);
94
95
  }
95
96
 
96
97
  return graphData;
@@ -136,6 +137,19 @@ function pruneDeletedBranches(graphData) {
136
137
  removeNodesFromGraph(new Set(rootsNotInRelation), currentGraph);
137
138
  }
138
139
 
140
+ // Models marked with `#unrelate` or `#delete` are unrelated or deleted
141
+ // as a whole. Their relations are ignored, both in the upserted graph
142
+ // and in the current graph.
143
+ function pruneUnrelatedOrDeletedBranches(graphData) {
144
+ const { graph, currentGraph } = graphData;
145
+
146
+ const roots = findRoots(graph.nodes.filter((node) => node.toBeUnrelatedOrDeleted));
147
+ const currentRoots = roots.map((node) => currentGraph.nodeForNode(node)).filter(Boolean);
148
+
149
+ removeBranchesFromGraph(roots, graph);
150
+ removeBranchesFromGraph(currentRoots, currentGraph);
151
+ }
152
+
139
153
  function findRoots(nodes) {
140
154
  const nodeSet = new Set(nodes);
141
155
 
@@ -192,6 +206,7 @@ function checkForErrors(builder) {
192
206
  checkForNotFoundErrors(graphData, builder);
193
207
  checkForUnallowedRelationErrors(graphData, builder);
194
208
  checkForUnallowedReferenceErrors(graphData, builder);
209
+ checkForRootUnrelateOrDeleteErrors(graphData, builder);
195
210
 
196
211
  if (graphData.graphOptions.isInsertOnly()) {
197
212
  checkForHasManyRelateErrors(graphData);
@@ -207,6 +222,8 @@ function checkForNotFoundErrors(graphData, builder) {
207
222
  for (const node of graph.nodes) {
208
223
  if (
209
224
  node.obj.$hasId() &&
225
+ !node.toBeUnrelated &&
226
+ !node.toBeDeleted &&
210
227
  !graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
211
228
  !graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
212
229
  !currentGraph.nodeForNode(node)
@@ -259,6 +276,19 @@ function checkForUnallowedReferenceErrors(graphData, builder) {
259
276
  }
260
277
  }
261
278
 
279
+ function checkForRootUnrelateOrDeleteErrors(graphData, builder) {
280
+ for (const node of graphData.graph.nodes) {
281
+ if (!node.parentEdge && node.toBeUnrelatedOrDeleted) {
282
+ const { graphUnrelateProp, graphDeleteProp } = node.modelClass;
283
+
284
+ throw builder.modelClass().createValidationError({
285
+ type: ValidationErrorType.InvalidGraph,
286
+ message: `${graphUnrelateProp} and ${graphDeleteProp} can only be used for related models, not for root models`,
287
+ });
288
+ }
289
+ }
290
+ }
291
+
262
292
  function checkForHasManyRelateErrors(graphData) {
263
293
  const { graph, graphOptions } = graphData;
264
294