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.
- package/lib/model/AjvValidator.js +101 -44
- package/lib/queryBuilder/QueryBuilder.js +91 -33
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
- package/lib/queryBuilder/StaticHookArguments.js +4 -1
- package/lib/queryBuilder/graph/GraphOptions.js +11 -26
- package/lib/queryBuilder/graph/GraphUpsert.js +16 -10
- package/lib/queryBuilder/join/RelationJoiner.js +104 -12
- package/lib/queryBuilder/join/utils.js +2 -2
- package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
- package/lib/queryBuilder/operations/FirstOperation.js +5 -3
- package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
- package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -1
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +145 -0
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
- package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
- package/lib/queryBuilder/operations/UpdateOperation.js +29 -3
- package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +2 -2
- package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
- package/lib/queryBuilder/operations/select/Selection.js +21 -20
- package/lib/queryBuilder/parsers/relationExpressionParser.js +5 -3
- package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +6 -2
- package/lib/queryBuilder/whereGroups.js +35 -40
- package/lib/queryBuilder/withoutOrder.js +33 -0
- package/lib/relations/Relation.js +3 -0
- package/lib/relations/RelationFindOperation.js +9 -4
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +31 -9
- package/lib/relations/manyToMany/ManyToManyRelation.js +7 -0
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +7 -2
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +16 -1
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +87 -17
- package/lib/utils/identifierMapping.js +2 -2
- package/lib/utils/knexUtils.js +14 -0
- package/lib/utils/parseFieldExpression.js +12 -0
- package/lib/utils/warnings.js +26 -0
- package/lib/utils/wrapDbError.js +34 -20
- package/package.json +5 -4
- package/typings/objection/index.d.ts +10 -2
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
338
|
-
// passed to aggregates, as they don't select them.
|
|
339
|
-
|
|
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
|
|
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
|
|
422
|
+
function createHasUserSelectionAs(builder, tableNode) {
|
|
359
423
|
if (tableNode.parentNode) {
|
|
360
|
-
return
|
|
424
|
+
return createHasSelectionAs(builder);
|
|
361
425
|
} else {
|
|
362
|
-
|
|
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.
|
|
10
|
-
//
|
|
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
|
-
|
|
7
|
-
|
|
6
|
+
shouldLimit(builder) {
|
|
7
|
+
return builder.isFind() && builder.modelClass().useLimitInFirst;
|
|
8
|
+
}
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
onBuildKnex(knexBuilder, builder) {
|
|
11
|
+
if (this.shouldLimit(builder)) {
|
|
10
12
|
knexBuilder = knexBuilder.limit(1);
|
|
11
13
|
}
|
|
12
14
|
|
|
@@ -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
|
-
|
|
83
|
-
|
|
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
|
};
|