objection 3.1.4 → 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 (34) hide show
  1. package/README.md +3 -3
  2. package/lib/model/AjvValidator.js +60 -25
  3. package/lib/model/Model.js +3 -4
  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/QueryBuilderOperationSupport.js +1 -1
  8. package/lib/queryBuilder/RelationExpression.js +8 -8
  9. package/lib/queryBuilder/graph/GraphFetcher.js +3 -1
  10. package/lib/queryBuilder/graph/GraphOptions.js +21 -2
  11. package/lib/queryBuilder/graph/GraphUpsert.js +30 -0
  12. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +20 -14
  13. package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -0
  14. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +2 -0
  15. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +15 -3
  16. package/lib/queryBuilder/join/RelationJoiner.js +17 -0
  17. package/lib/queryBuilder/operations/DelegateOperation.js +4 -0
  18. package/lib/queryBuilder/operations/NoneOperation.js +37 -0
  19. package/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js +5 -0
  20. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +20 -0
  21. package/lib/queryBuilder/operations/eager/EagerOperation.js +3 -5
  22. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +8 -2
  23. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +15 -3
  24. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +8 -2
  25. package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +9 -0
  26. package/lib/relations/RelationProperty.js +7 -3
  27. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +1 -0
  28. package/lib/relations/belongsToOne/BelongsToOneRelateOperation.js +5 -0
  29. package/lib/relations/hasMany/HasManyRelateOperation.js +1 -0
  30. package/lib/relations/manyToMany/relate/ManyToManyRelateOperation.js +6 -0
  31. package/lib/utils/identifierMapping.js +8 -2
  32. package/lib/utils/objectUtils.js +9 -5
  33. package/package.json +17 -13
  34. package/typings/objection/index.d.ts +77 -39
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,9 +35,14 @@ 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
- conf.onCreateAjv(ajv);
40
- // Only add AJV formats if they weren't added in user-space already
42
+ if (conf.onCreateAjv) {
43
+ conf.onCreateAjv(ajv);
44
+ }
45
+ // Only add Ajv formats if they weren't added in user-space already
41
46
  if (!ajv.formats['date-time']) {
42
47
  addFormats(ajv);
43
48
  }
@@ -77,7 +82,7 @@ class AjvValidator extends Validator {
77
82
  }
78
83
 
79
84
  validator.call(model, json);
80
- const error = parseValidationError(validator.errors, modelClass, options);
85
+ const error = parseValidationError(validator.errors, modelClass, options, this.ajvOptions);
81
86
 
82
87
  if (error) {
83
88
  throw error;
@@ -87,15 +92,26 @@ class AjvValidator extends Validator {
87
92
  }
88
93
 
89
94
  getValidator(modelClass, jsonSchema, isPatchObject) {
90
- // Use the AJV custom serializer if provided.
95
+ // Use the Ajv custom serializer if provided.
91
96
  const createCacheKey = this.ajvOptions.serialize || JSON.stringify;
92
97
 
93
- // Optimization for the common case where jsonSchema is never modified.
94
- // In that case we don't need to call the costly createCacheKey function.
95
- const cacheKey =
96
- jsonSchema === modelClass.getJsonSchema()
97
- ? modelClass.uniqueTag()
98
- : 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
+ }
99
115
 
100
116
  let validators = this.cache.get(cacheKey);
101
117
  let validator = null;
@@ -103,7 +119,7 @@ class AjvValidator extends Validator {
103
119
  if (!validators) {
104
120
  validators = {
105
121
  // Validator created for the schema object without `required` properties
106
- // using the AJV instance that doesn't set default values.
122
+ // using the Ajv instance that doesn't set default values.
107
123
  patchValidator: null,
108
124
 
109
125
  // Validator created for the unmodified schema.
@@ -143,7 +159,7 @@ class AjvValidator extends Validator {
143
159
  }
144
160
  }
145
161
 
146
- function parseValidationError(errors, modelClass, options) {
162
+ function parseValidationError(errors, modelClass, options, ajvOptions) {
147
163
  if (!errors) {
148
164
  return null;
149
165
  }
@@ -178,13 +194,21 @@ function parseValidationError(errors, modelClass, options) {
178
194
  // More than one error can occur for the same key in Ajv, merge them in the array:
179
195
  const array = errorHash[key] || (errorHash[key] = []);
180
196
 
181
- // Use unshift instead of push so that the last error ends up at [0],
182
- // preserving previous behavior where only the last error was stored.
183
- array.unshift({
197
+ // Prepare error object
198
+ const errorObj = {
184
199
  message: error.message,
185
200
  keyword: error.keyword,
186
201
  params: error.params,
187
- });
202
+ };
203
+
204
+ // Add data if verbose enabled
205
+ if (ajvOptions.verbose) {
206
+ errorObj.data = error.data;
207
+ }
208
+
209
+ // Use unshift instead of push so that the last error ends up at [0],
210
+ // preserving previous behavior where only the last error was stored.
211
+ array.unshift(errorObj);
188
212
 
189
213
  ++numErrors;
190
214
  }
@@ -253,24 +277,35 @@ function objectHasDefaults(obj) {
253
277
 
254
278
  function jsonSchemaWithoutRequired(jsonSchema) {
255
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'];
256
282
  return Object.assign(
257
283
  omit(jsonSchema, ['required', ...subSchemaProps]),
258
284
  ...subSchemaProps.map((prop) => subSchemaWithoutRequired(jsonSchema, prop)),
259
- jsonSchema && jsonSchema.definitions && Object.keys(jsonSchema.definitions).length > 0
260
- ? {
261
- definitions: Object.assign(
262
- ...Object.keys(jsonSchema.definitions).map((prop) => ({
263
- [prop]: jsonSchemaWithoutRequired(jsonSchema.definitions[prop]),
264
- })),
265
- ),
266
- }
267
- : {},
285
+ ...definitionProps.map((prop) => definitionsWithoutRequired(jsonSchema, prop)),
268
286
  jsonSchema.discriminator && jsonSchema.discriminator.propertyName
269
287
  ? { required: [jsonSchema.discriminator.propertyName] }
270
288
  : {},
271
289
  );
272
290
  }
273
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
+
274
309
  function subSchemaWithoutRequired(jsonSchema, prop) {
275
310
  if (jsonSchema[prop]) {
276
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
 
@@ -317,9 +317,6 @@ class Model {
317
317
 
318
318
  static createValidator() {
319
319
  return new AjvValidator({
320
- onCreateAjv: (ajv) => {
321
- /* Do Nothing by default */
322
- },
323
320
  options: {
324
321
  allErrors: true,
325
322
  validateSchema: false,
@@ -724,6 +721,8 @@ Model.uidProp = '#id';
724
721
  Model.uidRefProp = '#ref';
725
722
  Model.dbRefProp = '#dbRef';
726
723
  Model.propRefRegex = /#ref{([^\.]+)\.([^}]+)}/g;
724
+ Model.graphUnrelateProp = '#unrelate';
725
+ Model.graphDeleteProp = '#delete';
727
726
  Model.jsonAttributes = null;
728
727
  Model.cloneObjectAttributes = true;
729
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()) {
@@ -12,7 +12,7 @@ const WhereSelector = /^(where|orWhere|andWhere|find\w+)/;
12
12
  const OnSelector = /^(on|orOn|andOn)/;
13
13
  const OrderBySelector = /orderBy/;
14
14
  const JoinSelector = /(join|joinRaw|joinRelated)$/i;
15
- const FromSelector = /^(from|into|table)$/;
15
+ const FromSelector = /^(from|fromRaw|into|table)$/;
16
16
 
17
17
  class QueryBuilderOperationSupport {
18
18
  constructor(...args) {
@@ -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