objection 3.7.0 → 3.8.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 (43) hide show
  1. package/lib/model/AjvValidator.js +101 -44
  2. package/lib/queryBuilder/QueryBuilder.js +91 -33
  3. package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
  4. package/lib/queryBuilder/StaticHookArguments.js +4 -1
  5. package/lib/queryBuilder/graph/GraphOptions.js +11 -26
  6. package/lib/queryBuilder/graph/GraphUpsert.js +16 -10
  7. package/lib/queryBuilder/join/RelationJoiner.js +104 -12
  8. package/lib/queryBuilder/join/utils.js +2 -2
  9. package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
  10. package/lib/queryBuilder/operations/FirstOperation.js +5 -3
  11. package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
  12. package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
  13. package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
  14. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -1
  15. package/lib/queryBuilder/operations/LimitPerParentOperation.js +145 -0
  16. package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
  17. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
  18. package/lib/queryBuilder/operations/UpdateOperation.js +29 -3
  19. package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
  20. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
  21. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
  22. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +2 -2
  23. package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
  24. package/lib/queryBuilder/operations/select/Selection.js +21 -20
  25. package/lib/queryBuilder/parsers/relationExpressionParser.js +5 -3
  26. package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +6 -2
  27. package/lib/queryBuilder/whereGroups.js +35 -40
  28. package/lib/queryBuilder/withoutOrder.js +33 -0
  29. package/lib/relations/Relation.js +3 -0
  30. package/lib/relations/RelationFindOperation.js +9 -4
  31. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +31 -9
  32. package/lib/relations/manyToMany/ManyToManyRelation.js +7 -0
  33. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +7 -2
  34. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +16 -1
  35. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +87 -17
  36. package/lib/utils/identifierMapping.js +2 -2
  37. package/lib/utils/knexUtils.js +14 -0
  38. package/lib/utils/parseFieldExpression.js +12 -0
  39. package/lib/utils/warnings.js +26 -0
  40. package/lib/utils/wrapDbError.js +34 -20
  41. package/package.json +5 -4
  42. package/typings/objection/index.d.ts +10 -2
  43. package/lib/utils/deprecate.js +0 -21
@@ -14,16 +14,17 @@ 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
+ const { warnOnce } = require('../../utils/warnings');
18
18
 
19
19
  class GraphUpsert {
20
- constructor({ rootModelClass, objects, upsertOptions }) {
20
+ constructor({ rootModelClass, objects, upsertOptions, insertOnly = false }) {
21
21
  checkCanBeConvertedToModels(rootModelClass, objects);
22
22
  warnOnUnknownOptions(upsertOptions);
23
23
 
24
24
  this.objects = rootModelClass.ensureModelArray(objects, GraphUpsert.modelOptions);
25
25
  this.isArray = Array.isArray(objects);
26
26
  this.upsertOpt = upsertOptions;
27
+ this.insertOnly = insertOnly;
27
28
  }
28
29
 
29
30
  static get modelOptions() {
@@ -39,7 +40,7 @@ class GraphUpsert {
39
40
  // TODO: Throw an error instead in objection 4.0.
40
41
  static warnOnConflict(builder, method) {
41
42
  if (builder.has(isOnConflictOperation)) {
42
- deprecate(
43
+ warnOnce(
43
44
  `onConflict(), ignore() and merge() are not supported by ${method}(). ` +
44
45
  'Insert the conflicting rows with a separate insert() query instead. ' +
45
46
  'This will throw in objection 4.0.',
@@ -49,7 +50,7 @@ class GraphUpsert {
49
50
 
50
51
  run(builder) {
51
52
  const modelClass = builder.modelClass();
52
- const graphOptions = new GraphOptions(this.upsertOpt);
53
+ const graphOptions = new GraphOptions(this.upsertOpt, [], this.insertOnly);
53
54
 
54
55
  const graph = ModelGraph.create(modelClass, this.objects);
55
56
  assignDbRefsAsRelateProps(graph);
@@ -74,7 +75,7 @@ function warnOnUnknownOptions(upsertOptions) {
74
75
 
75
76
  for (const name of Object.keys(upsertOptions)) {
76
77
  if (!OPTION_NAMES.includes(name)) {
77
- deprecate(
78
+ warnOnce(
78
79
  `Unknown graph option "${name}" is ignored. ` +
79
80
  `Valid options are: ${OPTION_NAMES.join(', ')}. This will throw in objection 4.0.`,
80
81
  );
@@ -245,8 +246,10 @@ function checkForErrors(builder) {
245
246
  checkForNotFoundErrors(graphData, builder);
246
247
  checkForUnallowedRelationErrors(graphData, builder);
247
248
  checkForUnallowedReferenceErrors(graphData, builder);
248
- checkForUnallowedMarkerErrors(graphData, builder);
249
+ // Root models can't be marked even with `allowMarkers`, so report that
250
+ // first instead of pointing to the option.
249
251
  checkForRootUnrelateOrDeleteErrors(graphData, builder);
252
+ checkForUnallowedMarkerErrors(graphData, builder);
250
253
 
251
254
  if (graphData.graphOptions.isInsertOnly()) {
252
255
  checkForHasManyRelateErrors(graphData);
@@ -291,8 +294,7 @@ function checkForNotFoundErrors(graphData, builder) {
291
294
  for (const node of graph.nodes) {
292
295
  if (
293
296
  node.obj.$hasId() &&
294
- !node.toBeUnrelated &&
295
- !node.toBeDeleted &&
297
+ !node.toBeUnrelatedOrDeleted &&
296
298
  !graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
297
299
  !graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
298
300
  !currentGraph.nodeForNode(node)
@@ -347,10 +349,12 @@ function checkForUnallowedReferenceErrors(graphData, builder) {
347
349
 
348
350
  // `#unrelate` and `#delete` markers come from the graph data, which may be
349
351
  // untrusted input. Like `#ref`, they have to be allowed explicitly (#2410).
352
+ // An insert has nothing to unrelate or delete, so `insertGraph` rejects them.
350
353
  function checkForUnallowedMarkerErrors(graphData, builder) {
351
354
  const { graph, graphOptions } = graphData;
355
+ const isInsertOnly = graphOptions.isInsertOnly();
352
356
 
353
- if (graphOptions.allowMarkers()) {
357
+ if (graphOptions.allowMarkers() && !isInsertOnly) {
354
358
  return;
355
359
  }
356
360
 
@@ -361,7 +365,9 @@ function checkForUnallowedMarkerErrors(graphData, builder) {
361
365
 
362
366
  throw builder.modelClass().createValidationError({
363
367
  type: ValidationErrorType.InvalidGraph,
364
- message: `${graphUnrelateProp} and ${graphDeleteProp} markers are not allowed in a graph by default. see the allowMarkers insert/upsert graph option`,
368
+ message: isInsertOnly
369
+ ? `${graphUnrelateProp} and ${graphDeleteProp} can only be used in upsertGraph, not in insertGraph`
370
+ : `${graphUnrelateProp} and ${graphDeleteProp} markers are not allowed in a graph by default. see the allowMarkers upsertGraph option`,
365
371
  });
366
372
  }
367
373
  }
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { uniqBy } = require('../../utils/objectUtils');
4
- const { Selection } = require('../operations/select/Selection');
4
+ const { Selection, mapIdentifier } = require('../operations/select/Selection');
5
5
  const { SelectOperation } = require('../operations/select/SelectOperation');
6
6
  const { createModifier } = require('../../utils/createModifier');
7
7
  const { map: mapPromise } = require('../../utils/promiseUtils');
@@ -89,6 +89,13 @@ class RelationJoiner {
89
89
  }
90
90
  }
91
91
 
92
+ // Whether the joins can return more than one row per root model.
93
+ hasToManyJoins(builder) {
94
+ return this._getTableTree(builder).nodes.some(
95
+ (tableNode) => tableNode.relation && !tableNode.relation.isOneToOne(),
96
+ );
97
+ }
98
+
92
99
  parseResult(builder, flatRows) {
93
100
  const parser = JoinResultParser.create({
94
101
  tableTree: this._getTableTree(builder),
@@ -203,7 +210,60 @@ class RelationJoiner {
203
210
  }
204
211
  }
205
212
 
206
- builder.select(selectionsToStrings(selections));
213
+ // Each name is selected only once, as SQL Server returns duplicate names as
214
+ // arrays of values, and the other databases silently return one of them.
215
+ removeRootSelectAll(builder);
216
+ const tableTree = this._getTableTree(builder);
217
+ const userSelectionsByName = new Map();
218
+
219
+ for (const selection of findJoinSelections(builder)) {
220
+ // Only the columns of the joined tables are known for `table.*`:
221
+ const node = isSelectAll(selection) ? tableTree.nodesByAlias.get(selection.table) : null;
222
+ const names = node
223
+ ? getAllColumnSelectionsForNode({ builder, tableNode: node }).map((it) => it.column)
224
+ : [selection.name];
225
+
226
+ for (const name of names) {
227
+ userSelectionsByName.set(mapIdentifier(builder, name), { selection, name });
228
+ }
229
+ }
230
+
231
+ const idAliases = new Set(
232
+ tableTree.nodes.flatMap((node) =>
233
+ getIdSelectionsForNode({ tableNode: node }).map((it) => it.alias),
234
+ ),
235
+ );
236
+
237
+ builder.select(
238
+ selectionsToStrings(
239
+ selections.filter(({ table, column, alias }) => {
240
+ const { selection, name } = userSelectionsByName.get(mapIdentifier(builder, alias)) || {};
241
+
242
+ if (!selection) {
243
+ return true;
244
+ } else if (selection.column) {
245
+ if (!Selection.doesSelect(builder, selection, `${table}.${column}`)) {
246
+ throw new Error(
247
+ `withGraphJoined: the selection '${selectionToString(selection)}' selects '${name}', ` +
248
+ `the name under which the column '${table}.${column}' is selected. ` +
249
+ 'Select it under a different name.',
250
+ );
251
+ }
252
+ } else if (idAliases.has(alias)) {
253
+ // Raws, subqueries and aggregates select values that we can't see
254
+ // into. They replace our columns, but not the ids that we need to
255
+ // parse the result.
256
+ throw new Error(
257
+ `withGraphJoined: a raw, subquery or aggregate selection selects '${name}', ` +
258
+ `the name under which the id column '${table}.${column}' is selected. ` +
259
+ 'Select it under a different name, or select the id column itself.',
260
+ );
261
+ }
262
+
263
+ return false;
264
+ }),
265
+ ),
266
+ );
207
267
  }
208
268
  }
209
269
 
@@ -268,8 +328,9 @@ function ensureIdAndRelationPropsAreSelected({ builder, tableNode }) {
268
328
  ),
269
329
  ];
270
330
 
331
+ const hasSelectionAs = createHasSelectionAs(builder);
271
332
  const selectStrings = uniqBy(cols)
272
- .filter((col) => !hasSelectionAs(builder, col))
333
+ .filter((col) => !hasSelectionAs(col))
273
334
  .map((col) => `${tableRef}.${col}`);
274
335
 
275
336
  return builder.select(selectStrings);
@@ -297,9 +358,10 @@ function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
297
358
  selections = allColumnSelections.concat(selections);
298
359
  } else {
299
360
  const idSelections = getIdSelectionsForNode({ tableNode });
361
+ const hasUserSelectionAs = createHasUserSelectionAs(userSelectQuery, tableNode);
300
362
 
301
363
  for (const idSelection of idSelections) {
302
- if (!hasUserSelectionAs(userSelectQuery, tableNode, idSelection.column)) {
364
+ if (!hasUserSelectionAs(idSelection.column)) {
303
365
  selections.push(idSelection);
304
366
  internalSelections.push(idSelection);
305
367
  }
@@ -334,14 +396,16 @@ function findJoinSelections(builder) {
334
396
  return selections;
335
397
  }
336
398
 
337
- // Like `builder.hasSelectionAs(column, column)`, but ignoring the columns
338
- // passed to aggregates, as they don't select them.
339
- function hasSelectionAs(builder, column) {
399
+ // Create a `(column) => builder.hasSelectionAs(column, column)` function that
400
+ // ignores the columns passed to aggregates, as they don't select them. Clone
401
+ // the builder once, not once per column.
402
+ function createHasSelectionAs(builder) {
340
403
  if (builder.has(isAggregateOperation)) {
404
+ const selectionQuery = builder.clone().clear(isAggregateOperation);
341
405
  // Explicit, as a query with only aggregates doesn't select all columns.
342
- return builder.clone().clear(isAggregateOperation).hasSelectionAs(column, column, true);
406
+ return (column) => selectionQuery.hasSelectionAs(column, column, true);
343
407
  } else {
344
- return builder.hasSelectionAs(column, column);
408
+ return (column) => builder.hasSelectionAs(column, column);
345
409
  }
346
410
  }
347
411
 
@@ -355,11 +419,12 @@ function findUserSelections(builder, tableNode) {
355
419
  }
356
420
  }
357
421
 
358
- function hasUserSelectionAs(builder, tableNode, column) {
422
+ function createHasUserSelectionAs(builder, tableNode) {
359
423
  if (tableNode.parentNode) {
360
- return hasSelectionAs(builder, column);
424
+ return createHasSelectionAs(builder);
361
425
  } else {
362
- return builder.hasSelectionAs(column, column);
426
+ // Qualified, as the root query can also select the columns of the joined tables.
427
+ return (column) => builder.hasSelectionAs(`${tableNode.alias}.${column}`, column);
363
428
  }
364
429
  }
365
430
 
@@ -416,6 +481,28 @@ function getIdSelectionsForNode({ tableNode }) {
416
481
  });
417
482
  }
418
483
 
484
+ // The selections of all columns of the root table replace the user's `*`, which
485
+ // would select them a second time, and a bare `*` also the columns of the
486
+ // joined tables under the same names.
487
+ function removeRootSelectAll(builder) {
488
+ const tableRef = builder.tableRef();
489
+ const isRootSelectAll = (selection) =>
490
+ selection.column === '*' && (!selection.table || selection.table === tableRef);
491
+ const isSelect = (op) =>
492
+ op.constructor === SelectOperation && !isAggregateOperation(op) && !op.isDistinctOn;
493
+
494
+ builder.forEachOperation(isSelect, (op) => {
495
+ op.args = op.args.filter((arg) => {
496
+ const selection = Selection.create(arg);
497
+ return !selection || !isRootSelectAll(selection);
498
+ });
499
+ op.selections = op.selections.filter((selection) => !isRootSelectAll(selection));
500
+ });
501
+
502
+ // An empty `distinct()` still makes the whole select distinct.
503
+ builder.clear((op) => isSelect(op) && op.name !== 'distinct' && op.args.length === 0);
504
+ }
505
+
419
506
  function selectionsToStrings(selections) {
420
507
  return selections.map((selection) => {
421
508
  const selectStr = `${selection.table}.${selection.column}`;
@@ -423,6 +510,11 @@ function selectionsToStrings(selections) {
423
510
  });
424
511
  }
425
512
 
513
+ function selectionToString({ table, column, alias }) {
514
+ const selection = table ? `${table}.${column}` : column;
515
+ return alias ? `${selection} as ${alias}` : selection;
516
+ }
517
+
426
518
  function isSelectAll(selection) {
427
519
  return selection.column === '*';
428
520
  }
@@ -6,8 +6,8 @@ const { isPostgres, isMySql, isMsSql, isSqlite } = require('../../utils/knexUtil
6
6
  const RELATION_RECURSION_LIMIT = 64;
7
7
 
8
8
  // The maximum length of the column aliases of the joined relations, after
9
- // which the database would truncate them. Unknown databases use the strictest
10
- // of the known limits, the one of postgres.
9
+ // which the database would truncate them. Other databases keep the limit of
10
+ // postgres, which isn't the strictest one: Oracle before 12.2 only allows 30.
11
11
  function getIdentifierLengthLimit(knex) {
12
12
  if (isPostgres(knex)) {
13
13
  return 63;
@@ -55,6 +55,17 @@ class DelegateOperation extends QueryBuilderOperation {
55
55
  return this.onBuild !== DelegateOperation.prototype.onBuild || this.delegate.hasOnBuild();
56
56
  }
57
57
 
58
+ onAfterBuild(builder) {
59
+ return this.delegate.onAfterBuild(builder);
60
+ }
61
+
62
+ hasOnAfterBuild() {
63
+ return (
64
+ this.onAfterBuild !== DelegateOperation.prototype.onAfterBuild ||
65
+ this.delegate.hasOnAfterBuild()
66
+ );
67
+ }
68
+
58
69
  onBuildKnex(knexBuilder, builder) {
59
70
  return this.delegate.onBuildKnex(knexBuilder, builder);
60
71
  }
@@ -110,6 +121,17 @@ class DelegateOperation extends QueryBuilderOperation {
110
121
  );
111
122
  }
112
123
 
124
+ queryBatches(builder) {
125
+ return this.delegate.queryBatches(builder);
126
+ }
127
+
128
+ hasQueryBatches() {
129
+ return (
130
+ this.queryBatches !== DelegateOperation.prototype.queryBatches ||
131
+ this.delegate.hasQueryBatches()
132
+ );
133
+ }
134
+
113
135
  onError(builder, error) {
114
136
  return this.delegate.onError(builder, error);
115
137
  }
@@ -3,10 +3,12 @@
3
3
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
4
 
5
5
  class FirstOperation extends QueryBuilderOperation {
6
- onBuildKnex(knexBuilder, builder) {
7
- const modelClass = builder.modelClass();
6
+ shouldLimit(builder) {
7
+ return builder.isFind() && builder.modelClass().useLimitInFirst;
8
+ }
8
9
 
9
- if (builder.isFind() && modelClass.useLimitInFirst) {
10
+ onBuildKnex(knexBuilder, builder) {
11
+ if (this.shouldLimit(builder)) {
10
12
  knexBuilder = knexBuilder.limit(1);
11
13
  }
12
14
 
@@ -42,6 +42,7 @@ class InsertGraphOperation extends DelegateOperation {
42
42
  noUnrelate: true,
43
43
  insertMissing: true,
44
44
  }),
45
+ insertOnly: true,
45
46
  });
46
47
 
47
48
  // We resolve this query here and will not execute it. This is because the root
@@ -3,6 +3,7 @@
3
3
  const { DeleteOperation } = require('./DeleteOperation');
4
4
  const { InstanceFindOperation } = require('./InstanceFindOperation');
5
5
  const { assertHasId } = require('../../utils/assert');
6
+ const { addWhereGroup } = require('../whereGroups');
6
7
 
7
8
  class InstanceDeleteOperation extends DeleteOperation {
8
9
  constructor(name, opt) {
@@ -20,7 +21,7 @@ class InstanceDeleteOperation extends DeleteOperation {
20
21
  super.onBuild(builder);
21
22
 
22
23
  assertHasId(this.instance);
23
- builder.findById(this.instance.$id());
24
+ addWhereGroup(builder, () => builder.findById(this.instance.$id()));
24
25
  }
25
26
 
26
27
  async onAfter2(builder, result) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { FindOperation } = require('./FindOperation');
4
4
  const { assertHasId } = require('../../utils/assert');
5
+ const { addWhereGroup } = require('../whereGroups');
5
6
 
6
7
  class InstanceFindOperation extends FindOperation {
7
8
  constructor(name, opt) {
@@ -11,7 +12,7 @@ class InstanceFindOperation extends FindOperation {
11
12
 
12
13
  onBuild(builder) {
13
14
  assertHasId(this.instance);
14
- builder.findById(this.instance.$id());
15
+ addWhereGroup(builder, () => builder.findById(this.instance.$id()));
15
16
  }
16
17
  }
17
18
 
@@ -4,6 +4,7 @@ const { UpdateOperation } = require('./UpdateOperation');
4
4
  const { InstanceFindOperation } = require('./InstanceFindOperation');
5
5
  const { assertHasId } = require('../../utils/assert');
6
6
  const { isObject } = require('../../utils/objectUtils');
7
+ const { addWhereGroup } = require('../whereGroups');
7
8
 
8
9
  class InstanceUpdateOperation extends UpdateOperation {
9
10
  constructor(name, opt) {
@@ -27,7 +28,7 @@ class InstanceUpdateOperation extends UpdateOperation {
27
28
  super.onBuild(builder);
28
29
 
29
30
  assertHasId(this.instance);
30
- builder.findById(this.instance.$id());
31
+ addWhereGroup(builder, () => builder.findById(this.instance.$id()));
31
32
  }
32
33
 
33
34
  async onAfter2(builder, result) {
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
+ const { RangeOperation } = require('./RangeOperation');
5
+ const { FirstOperation } = require('./FirstOperation');
6
+ const { isString, isNumber } = require('../../utils/objectUtils');
7
+ const { hasWindowFunctions, isKnexRaw } = require('../../utils/knexUtils');
8
+
9
+ // Without underscores, so that column name mappers don't change it.
10
+ const rowNumberColumn = 'objectiontmprownum';
11
+
12
+ // Applies the `limit()`, `offset()` and `first()` of a relation query that
13
+ // fetches the related models of many parents to each parent separately.
14
+ class LimitPerParentOperation extends QueryBuilderOperation {
15
+ constructor(name, opt) {
16
+ super(name, opt);
17
+ this.findOperation = opt.findOperation;
18
+ }
19
+
20
+ onBuild(builder) {
21
+ builder.addInternalSelection(rowNumberColumn);
22
+ }
23
+
24
+ // Return the limit and offset of the query, built into `knexBuilder` by any
25
+ // of its operations, or `null` if it has none:
26
+ getRange(knexBuilder, builder) {
27
+ // `page()` and `range()` keep limiting all parents, as their total does.
28
+ if (!builder.has(RangeOperation)) {
29
+ const { limit = null, offset = null } = knexBuilder._single;
30
+ // `first()` only limits the query with `useLimitInFirst`:
31
+ const isFirst = builder.has(FirstOperation);
32
+
33
+ if (limit !== null || offset !== null || isFirst) {
34
+ return { limit: isFirst ? 1 : limit, offset: offset ?? 0 };
35
+ }
36
+ }
37
+
38
+ return null;
39
+ }
40
+
41
+ // Called by `buildKnexQuery()` after the default table and select are set.
42
+ onAfterBuildKnex(knexBuilder, builder) {
43
+ const knex = builder.knex();
44
+ const range = this.getRange(knexBuilder, builder);
45
+
46
+ if (!range) {
47
+ return knexBuilder;
48
+ } else if (!hasWindowFunctions(knex)) {
49
+ // `WhereInEagerOperation` fetches the related models of each parent with
50
+ // a separate query then, so only `first()` still needs to be applied:
51
+ return range.limit === null ? knexBuilder : knexBuilder.limit(range.limit);
52
+ } else {
53
+ const { limit, offset } = range;
54
+ // Number the rows of each parent one level up, aliased as the related
55
+ // table, so that the order can refer to the selected columns and their
56
+ // aliases as in the query itself, e.g. to `id` when a joined table has
57
+ // an `id` too. The order moves into the window function, as SQL Server
58
+ // doesn't allow it in subqueries.
59
+ const numberedQuery = knex
60
+ .queryBuilder()
61
+ .select('*')
62
+ .rowNumber(rowNumberColumn, (window) =>
63
+ window
64
+ .orderBy(getWindowOrder(builder, knex))
65
+ .partitionBy(this.findOperation.ownerKeyColumns()),
66
+ )
67
+ .from(
68
+ knexBuilder
69
+ .clone()
70
+ .clear('with')
71
+ .clear('limit')
72
+ .clear('offset')
73
+ .clear('order')
74
+ .as(builder.tableRef()),
75
+ );
76
+
77
+ // Select the rows in range with the query itself, to keep its settings
78
+ // like `timeout()`, and its common table expressions.
79
+ knexBuilder
80
+ .clear('select')
81
+ .clear('where')
82
+ .clear('join')
83
+ .clear('union')
84
+ .clear('group')
85
+ .clear('having')
86
+ .clear('order')
87
+ .clear('limit')
88
+ .clear('offset')
89
+ .select('*')
90
+ .from(numberedQuery.as(rowNumberColumn))
91
+ .where(rowNumberColumn, '>', offset)
92
+ .orderBy(rowNumberColumn);
93
+
94
+ if (limit !== null) {
95
+ // knex also accepts raw offsets:
96
+ const end = isNumber(offset) ? offset + limit : knex.raw('? + ?', [offset, limit]);
97
+ knexBuilder.where(rowNumberColumn, '<=', end);
98
+ }
99
+
100
+ return knexBuilder;
101
+ }
102
+ }
103
+ }
104
+
105
+ // Convert the `orderBy()` and `orderByRaw()` calls of the query to the order
106
+ // of knex's window functions.
107
+ function getWindowOrder(builder, knex) {
108
+ const order = [];
109
+
110
+ builder.forEachOperation(builder.constructor.OrderBySelector, (op) => {
111
+ const args = op.getKnexArgs(builder);
112
+
113
+ if (op.name === 'orderByRaw') {
114
+ const [sql, bindings] = args;
115
+ order.push({ column: isKnexRaw(sql) ? sql : knex.raw(sql, bindings) });
116
+ } else {
117
+ const [column, direction, nulls] = args;
118
+ const items = Array.isArray(column)
119
+ ? column.map((item) => (isString(item) ? { column: item } : item))
120
+ : [{ column, order: direction, nulls }];
121
+
122
+ for (const item of items) {
123
+ if (item.nulls) {
124
+ // Not all databases support `nulls first` and `nulls last`:
125
+ order.push({
126
+ column: knex.raw('(case when ? is null then 0 else 1 end)', [
127
+ isString(item.column) ? knex.ref(item.column) : item.column,
128
+ ]),
129
+ order: item.nulls === 'first' ? 'asc' : 'desc',
130
+ });
131
+ }
132
+
133
+ // Unlike in `orderBy()`, knex adds the direction to the window as it is:
134
+ order.push({ column: item.column, order: /^desc$/i.test(item.order) ? 'desc' : 'asc' });
135
+ }
136
+ }
137
+ });
138
+
139
+ // SQL Server requires an order in the window function:
140
+ return order.length ? order : [{ column: knex.raw('(select null)') }];
141
+ }
142
+
143
+ module.exports = {
144
+ LimitPerParentOperation,
145
+ };
@@ -6,9 +6,11 @@ const hookNameToHasMethodName = {
6
6
  onBefore2: 'hasOnBefore2',
7
7
  onBefore3: 'hasOnBefore3',
8
8
  onBuild: 'hasOnBuild',
9
+ onAfterBuild: 'hasOnAfterBuild',
9
10
  onBuildKnex: 'hasOnBuildKnex',
10
11
  onRawResult: 'hasOnRawResult',
11
12
  queryExecutor: 'hasQueryExecutor',
13
+ queryBatches: 'hasQueryBatches',
12
14
  onAfter1: 'hasOnAfter1',
13
15
  onAfter2: 'hasOnAfter2',
14
16
  onAfter3: 'hasOnAfter3',
@@ -107,6 +109,20 @@ class QueryBuilderOperation {
107
109
  return this.onBuild !== QueryBuilderOperation.prototype.onBuild;
108
110
  }
109
111
 
112
+ // This is called after `onBuild` when the query is executed, right before the
113
+ // knex query is built and executed, but not when the query is only built,
114
+ // e.g. with `toKnexQuery()` or `resultSize()`. Use it for work that needs the
115
+ // built query, like queries that have to run before it. It's also called for
116
+ // queries that are resolved or rejected without executing, e.g. with `none()`
117
+ // or `resolve()`, so check `builder.isExplicitlyResolvedOrRejected()` first.
118
+ //
119
+ // This method can be asynchronous.
120
+ // This method should never call any methods that add operations to the builder.
121
+ onAfterBuild(builder) {}
122
+ hasOnAfterBuild() {
123
+ return this.onAfterBuild !== QueryBuilderOperation.prototype.onAfterBuild;
124
+ }
125
+
110
126
  // This is called when the knex query is built. Here you should only call knex
111
127
  // methods. You may call getters and other immutable methods of the `builder`
112
128
  // but you should never call methods that add QueryBuilderOperations.
@@ -177,6 +193,21 @@ class QueryBuilderOperation {
177
193
  return this.queryExecutor !== QueryBuilderOperation.prototype.queryExecutor;
178
194
  }
179
195
 
196
+ // This is called after `onAfterBuild` when the query is executed. It can
197
+ // return an array of batches to build and execute the knex query once per
198
+ // batch, e.g. when a filter has more values than the database accepts in one
199
+ // statement. The batch being built is in `builder._queryBatch`, and the
200
+ // results of all batches are merged before `onRawResult` is called. Return
201
+ // null to execute the query once.
202
+ //
203
+ // This method must be synchronous.
204
+ queryBatches(builder) {
205
+ return null;
206
+ }
207
+ hasQueryBatches() {
208
+ return this.queryBatches !== QueryBuilderOperation.prototype.queryBatches;
209
+ }
210
+
180
211
  // This is called if an error occurs in the query execution.
181
212
  //
182
213
  // This method must return a QueryBuilder instance.
@@ -3,6 +3,7 @@
3
3
  const { DelegateOperation } = require('./DelegateOperation');
4
4
  const { FindByIdOperation } = require('./FindByIdOperation');
5
5
  const { UpdateOperation } = require('./UpdateOperation');
6
+ const { addWhereGroup, assignWhereGroup } = require('../whereGroups');
6
7
 
7
8
  class UpdateAndFetchOperation extends DelegateOperation {
8
9
  constructor(name, opt) {
@@ -41,7 +42,7 @@ class UpdateAndFetchOperation extends DelegateOperation {
41
42
 
42
43
  onBuild(builder) {
43
44
  if (!this.skipIdWhere) {
44
- builder.findById(this.id);
45
+ addWhereGroup(builder, () => builder.findById(this.id));
45
46
  }
46
47
 
47
48
  super.onBuild(builder);
@@ -78,10 +79,17 @@ class UpdateAndFetchOperation extends DelegateOperation {
78
79
  return fetched ? this.model : undefined;
79
80
  }
80
81
 
81
- toFindOperation() {
82
- return new FindByIdOperation('findById', {
83
- id: this.id,
84
- });
82
+ toFindOperation(builder) {
83
+ // The find operation of the delegate keeps its filters, e.g. the ones that
84
+ // many-to-many updates move to a subquery when the query is built.
85
+ const delegate = this.delegate.toFindOperation(builder);
86
+
87
+ // Keep the id condition in its own where group, like `onBuild()` does. The
88
+ // delegate isn't named `findById`, as that would put all the wheres that it
89
+ // adds in the user's group.
90
+ return delegate
91
+ ? new FindByIdDelegateOperation('find', { delegate, id: this.id })
92
+ : assignWhereGroup(new FindByIdOperation('findById', { id: this.id }));
85
93
  }
86
94
 
87
95
  clone() {
@@ -94,6 +102,15 @@ class UpdateAndFetchOperation extends DelegateOperation {
94
102
  }
95
103
  }
96
104
 
105
+ // Finds the rows of a delegate find operation by id, like `UpdateAndFetchOperation`
106
+ // updates them.
107
+ class FindByIdDelegateOperation extends DelegateOperation {
108
+ onBuild(builder) {
109
+ super.onBuild(builder);
110
+ addWhereGroup(builder, () => builder.findById(this.opt.id));
111
+ }
112
+ }
113
+
97
114
  module.exports = {
98
115
  UpdateAndFetchOperation,
99
116
  };