objection 3.3.0 → 3.4.1
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 +87 -10
- package/lib/model/Model.js +3 -1
- package/lib/model/modelId.js +10 -2
- package/lib/model/modelQueryProps.js +7 -0
- package/lib/queryBuilder/QueryBuilder.js +5 -3
- package/lib/queryBuilder/QueryBuilderBase.js +44 -9
- package/lib/queryBuilder/ReferenceBuilder.js +8 -1
- package/lib/queryBuilder/graph/GraphFetcher.js +1 -3
- package/lib/queryBuilder/graph/GraphOptions.js +30 -1
- package/lib/queryBuilder/graph/GraphUpsert.js +16 -1
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +8 -2
- package/lib/queryBuilder/join/RelationJoiner.js +62 -9
- package/lib/queryBuilder/join/utils.js +20 -2
- package/lib/queryBuilder/operations/InsertOperation.js +11 -4
- package/lib/queryBuilder/operations/UpdateOperation.js +2 -0
- package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +1 -0
- package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -0
- package/lib/queryBuilder/operations/jsonApi/WhereJsonPostgresOperation.js +33 -0
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +16 -1
- package/lib/queryBuilder/operations/select/SelectOperation.js +39 -1
- package/lib/relations/RelationFindOperation.js +66 -2
- package/lib/relations/RelationProperty.js +3 -2
- package/lib/relations/manyToMany/ManyToManyRelation.js +34 -3
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +34 -4
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +4 -0
- package/lib/utils/assert.js +9 -0
- package/lib/utils/wrapDbError.js +36 -0
- package/package.json +2 -5
- package/typings/objection/index.d.ts +1 -1
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
const { uniqBy } = require('../../utils/objectUtils');
|
|
4
4
|
const { Selection } = require('../operations/select/Selection');
|
|
5
|
+
const { SelectOperation } = require('../operations/select/SelectOperation');
|
|
5
6
|
const { createModifier } = require('../../utils/createModifier');
|
|
6
7
|
const { map: mapPromise } = require('../../utils/promiseUtils');
|
|
7
8
|
const { ValidationErrorType } = require('../../model/ValidationError');
|
|
8
9
|
|
|
9
10
|
const { TableTree } = require('./TableTree');
|
|
10
11
|
const { JoinResultParser } = require('./JoinResultParser');
|
|
11
|
-
const {
|
|
12
|
+
const { getIdentifierLengthLimit } = require('./utils');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Given a relation expression, builds a query using joins to fetch it.
|
|
@@ -124,7 +125,7 @@ class RelationJoiner {
|
|
|
124
125
|
// also contain selects.
|
|
125
126
|
userSelectQuery.modify(tableNode.relation.modify);
|
|
126
127
|
|
|
127
|
-
if (selectsOnlyAliasesKeepingSelectAll(userSelectQuery
|
|
128
|
+
if (selectsOnlyAliasesKeepingSelectAll(findJoinSelections(userSelectQuery))) {
|
|
128
129
|
// All columns are selected from the joined subquery in the main query,
|
|
129
130
|
// so the subquery needs to select them too.
|
|
130
131
|
subqueryToJoin.select(`${subqueryToJoin.tableRef()}.*`);
|
|
@@ -179,8 +180,10 @@ class RelationJoiner {
|
|
|
179
180
|
userSelectQueries,
|
|
180
181
|
});
|
|
181
182
|
|
|
183
|
+
const lengthLimit = getIdentifierLengthLimit(builder.knex());
|
|
184
|
+
|
|
182
185
|
for (const selection of selections) {
|
|
183
|
-
checkAliasLength(tableNode.modelClass, selection.name);
|
|
186
|
+
checkAliasLength(tableNode.modelClass, selection.name, lengthLimit);
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
// Save the selections that were added internally (not by the user)
|
|
@@ -266,7 +269,7 @@ function ensureIdAndRelationPropsAreSelected({ builder, tableNode }) {
|
|
|
266
269
|
];
|
|
267
270
|
|
|
268
271
|
const selectStrings = uniqBy(cols)
|
|
269
|
-
.filter((col) => !
|
|
272
|
+
.filter((col) => !hasSelectionAs(builder, col))
|
|
270
273
|
.map((col) => `${tableRef}.${col}`);
|
|
271
274
|
|
|
272
275
|
return builder.select(selectStrings);
|
|
@@ -274,7 +277,7 @@ function ensureIdAndRelationPropsAreSelected({ builder, tableNode }) {
|
|
|
274
277
|
|
|
275
278
|
function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
|
|
276
279
|
const userSelectQuery = userSelectQueries.get(tableNode);
|
|
277
|
-
const userSelections = userSelectQuery
|
|
280
|
+
const userSelections = findUserSelections(userSelectQuery, tableNode);
|
|
278
281
|
const userSelectedAllColumns = isSelectAllSelectionSet(userSelections);
|
|
279
282
|
|
|
280
283
|
let selections = [];
|
|
@@ -296,7 +299,7 @@ function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
|
|
|
296
299
|
const idSelections = getIdSelectionsForNode({ tableNode });
|
|
297
300
|
|
|
298
301
|
for (const idSelection of idSelections) {
|
|
299
|
-
if (!userSelectQuery
|
|
302
|
+
if (!hasUserSelectionAs(userSelectQuery, tableNode, idSelection.column)) {
|
|
300
303
|
selections.push(idSelection);
|
|
301
304
|
internalSelections.push(idSelection);
|
|
302
305
|
}
|
|
@@ -316,6 +319,54 @@ function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
|
|
|
316
319
|
};
|
|
317
320
|
}
|
|
318
321
|
|
|
322
|
+
// Like `builder.findAllSelections()`, but aggregates select their aliases
|
|
323
|
+
// instead of the columns they're passed. Aggregates without an alias are left
|
|
324
|
+
// out, as their column name differs between databases.
|
|
325
|
+
function findJoinSelections(builder) {
|
|
326
|
+
let selections = [];
|
|
327
|
+
|
|
328
|
+
builder.forEachOperation(true, (op) => {
|
|
329
|
+
if (op.constructor === SelectOperation) {
|
|
330
|
+
selections = selections.concat(op.aggregateSelections || op.selections);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return selections;
|
|
335
|
+
}
|
|
336
|
+
|
|
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) {
|
|
340
|
+
if (builder.has(isAggregateOperation)) {
|
|
341
|
+
// Explicit, as a query with only aggregates doesn't select all columns.
|
|
342
|
+
return builder.clone().clear(isAggregateOperation).hasSelectionAs(column, column, true);
|
|
343
|
+
} else {
|
|
344
|
+
return builder.hasSelectionAs(column, column);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Only the joined subqueries select aggregates by their aliases. The root
|
|
349
|
+
// query keeps treating the columns passed to them as selected.
|
|
350
|
+
function findUserSelections(builder, tableNode) {
|
|
351
|
+
if (tableNode.parentNode) {
|
|
352
|
+
return findJoinSelections(builder);
|
|
353
|
+
} else {
|
|
354
|
+
return builder.findAllSelections();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function hasUserSelectionAs(builder, tableNode, column) {
|
|
359
|
+
if (tableNode.parentNode) {
|
|
360
|
+
return hasSelectionAs(builder, column);
|
|
361
|
+
} else {
|
|
362
|
+
return builder.hasSelectionAs(column, column);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isAggregateOperation(op) {
|
|
367
|
+
return op.constructor === SelectOperation && op.aggregateSelections !== null;
|
|
368
|
+
}
|
|
369
|
+
|
|
319
370
|
function mapUserSelectionsFromSubqueryToMainQuery({ userSelections, tableNode }) {
|
|
320
371
|
return userSelections.filter(isNotSelectAll).map((selection) => {
|
|
321
372
|
return new Selection(
|
|
@@ -393,11 +444,13 @@ function selectsOnlyAliasesKeepingSelectAll(selections) {
|
|
|
393
444
|
return selections.length > 0 && selections.every((it) => it.keepsSelectAll);
|
|
394
445
|
}
|
|
395
446
|
|
|
396
|
-
function checkAliasLength(modelClass, alias) {
|
|
397
|
-
if (alias.length >
|
|
447
|
+
function checkAliasLength(modelClass, alias, lengthLimit) {
|
|
448
|
+
if (alias.length > lengthLimit) {
|
|
398
449
|
throw modelClass.createValidationError({
|
|
399
450
|
type: ValidationErrorType.RelationExpression,
|
|
400
|
-
message:
|
|
451
|
+
message:
|
|
452
|
+
`identifier ${alias} is over ${lengthLimit} characters long and would be truncated by the database engine. ` +
|
|
453
|
+
'Use the `minimize` option of withGraphJoined() to shorten the aliases.',
|
|
401
454
|
});
|
|
402
455
|
}
|
|
403
456
|
}
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { ValidationErrorType } = require('../../model/ValidationError');
|
|
4
|
+
const { isPostgres, isMySql, isMsSql, isSqlite } = require('../../utils/knexUtils');
|
|
4
5
|
|
|
5
|
-
const ID_LENGTH_LIMIT = 63;
|
|
6
6
|
const RELATION_RECURSION_LIMIT = 64;
|
|
7
7
|
|
|
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.
|
|
11
|
+
function getIdentifierLengthLimit(knex) {
|
|
12
|
+
if (isPostgres(knex)) {
|
|
13
|
+
return 63;
|
|
14
|
+
} else if (isMySql(knex)) {
|
|
15
|
+
return 256;
|
|
16
|
+
} else if (isMsSql(knex)) {
|
|
17
|
+
return 128;
|
|
18
|
+
} else if (isSqlite(knex)) {
|
|
19
|
+
return Infinity;
|
|
20
|
+
} else {
|
|
21
|
+
return 63;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
// Given a relation expression, goes through all first level children.
|
|
9
26
|
function forEachChildExpression(expr, modelClass, callback) {
|
|
10
27
|
if (expr.node.$allRecursive || expr.maxRecursionDepth > RELATION_RECURSION_LIMIT) {
|
|
@@ -18,8 +35,9 @@ function forEachChildExpression(expr, modelClass, callback) {
|
|
|
18
35
|
}
|
|
19
36
|
|
|
20
37
|
module.exports = {
|
|
21
|
-
ID_LENGTH_LIMIT,
|
|
22
38
|
RELATION_RECURSION_LIMIT,
|
|
23
39
|
|
|
40
|
+
getIdentifierLengthLimit,
|
|
41
|
+
|
|
24
42
|
forEachChildExpression,
|
|
25
43
|
};
|
|
@@ -38,18 +38,20 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
38
38
|
onBuildKnex(knexBuilder, builder) {
|
|
39
39
|
const knex = builder.knex();
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
const defaultReturning = getDefaultReturning(builder);
|
|
42
|
+
|
|
43
|
+
if (!builder.has(/returning/) && defaultReturning !== null) {
|
|
42
44
|
if (!isSqlite(knex) && !isMySql(knex)) {
|
|
43
45
|
// If the user hasn't specified a `returning` clause, we make sure
|
|
44
46
|
// that at least the identifier is returned. With `onConflict()` the
|
|
45
47
|
// conflict columns are returned too, so that the returned rows can
|
|
46
48
|
// be matched to the models when some of the rows are ignored.
|
|
47
|
-
knexBuilder = knexBuilder.returning(
|
|
49
|
+
knexBuilder = knexBuilder.returning(defaultReturning);
|
|
48
50
|
} else if (isSqlite(knex) && builder.has('onConflict')) {
|
|
49
51
|
// Without a `returning` clause, SQLite returns the id of the last
|
|
50
52
|
// inserted row, which belongs to a previous insert if the row was
|
|
51
53
|
// ignored. Use `returning` to find out what was actually inserted.
|
|
52
|
-
knexBuilder = knexBuilder.returning(
|
|
54
|
+
knexBuilder = knexBuilder.returning(defaultReturning);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -77,7 +79,7 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
77
79
|
this.models[i].$setDatabaseJson(ret[i]);
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
|
-
} else {
|
|
82
|
+
} else if (builder.modelClass().getIdColumnArray().length > 0) {
|
|
81
83
|
// If the return value is not an array of objects, we assume it is an array of identifiers.
|
|
82
84
|
for (let i = 0, l = this.models.length; i < l; ++i) {
|
|
83
85
|
const model = this.models[i];
|
|
@@ -113,10 +115,15 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
// Returns `null` if there's nothing to return, e.g. for models without an `idColumn`.
|
|
116
119
|
function getDefaultReturning(builder) {
|
|
117
120
|
const idColumns = builder.modelClass().getIdColumnArray();
|
|
118
121
|
const columns = Array.from(new Set(idColumns.concat(getConflictColumns(builder))));
|
|
119
122
|
|
|
123
|
+
if (columns.length === 0) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
return columns.length === 1 ? columns[0] : columns;
|
|
121
128
|
}
|
|
122
129
|
|
|
@@ -5,6 +5,7 @@ const { isEmpty } = require('../../utils/objectUtils');
|
|
|
5
5
|
const { toPostgresJsonPathLiteral } = require('../../utils/parseFieldExpression');
|
|
6
6
|
const { isKnexRaw, isKnexQueryBuilder } = require('../../utils/knexUtils');
|
|
7
7
|
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
8
|
+
const { warnIfNotPostgres } = require('./jsonApi/postgresJsonApi');
|
|
8
9
|
const { StaticHookArguments } = require('../StaticHookArguments');
|
|
9
10
|
|
|
10
11
|
class UpdateOperation extends QueryBuilderOperation {
|
|
@@ -103,6 +104,7 @@ function convertFieldExpressionsToRaw(builder, model, json) {
|
|
|
103
104
|
if (key.indexOf(':') > -1) {
|
|
104
105
|
// 'col:attr' : ref('other:lol') is transformed to
|
|
105
106
|
// "col" : raw(`jsonb_set("col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
|
|
107
|
+
warnIfNotPostgres(knex);
|
|
106
108
|
|
|
107
109
|
let parsed = ref(key);
|
|
108
110
|
let jsonPath = toPostgresJsonPathLiteral(parsed.parsedExpr.access);
|
|
@@ -5,6 +5,7 @@ const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConver
|
|
|
5
5
|
|
|
6
6
|
class WhereJsonHasPostgresOperation extends ObjectionToKnexConvertingOperation {
|
|
7
7
|
onBuildKnex(knexBuilder, builder) {
|
|
8
|
+
jsonApi.warnIfNotPostgres(builder.knex());
|
|
8
9
|
const args = this.getKnexArgs(builder);
|
|
9
10
|
|
|
10
11
|
const rawArgs = jsonApi.whereJsonFieldRightStringArrayOnLeftQuery(
|
|
@@ -5,6 +5,7 @@ const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConver
|
|
|
5
5
|
|
|
6
6
|
class WhereJsonNotObjectPostgresOperation extends ObjectionToKnexConvertingOperation {
|
|
7
7
|
onBuildKnex(knexBuilder, builder) {
|
|
8
|
+
jsonApi.warnIfNotPostgres(builder.knex());
|
|
8
9
|
return this.whereJsonNotObject(knexBuilder, builder.knex(), this.getKnexArgs(builder)[0]);
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -2,9 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
const jsonApi = require('./postgresJsonApi');
|
|
4
4
|
const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConvertingOperation');
|
|
5
|
+
const { parseFieldExpression } = require('../../../utils/parseFieldExpression');
|
|
6
|
+
const { isPlainObject, isString, isFunction } = require('../../../utils/objectUtils');
|
|
7
|
+
const { isMySql } = require('../../../utils/knexUtils');
|
|
5
8
|
|
|
6
9
|
class WhereJsonPostgresOperation extends ObjectionToKnexConvertingOperation {
|
|
7
10
|
onBuildKnex(knexBuilder, builder) {
|
|
11
|
+
const knex = builder.knex();
|
|
12
|
+
|
|
13
|
+
if (isMySql(knex) && this.canDelegateToKnex(knexBuilder)) {
|
|
14
|
+
// On MySQL, knex's version of the method works (ditojs#113). The `or`
|
|
15
|
+
// variants are wrapped in `orWhere()`, as knex < 3 ignores their `or`.
|
|
16
|
+
const [column, value] = this.getKnexArgs(builder);
|
|
17
|
+
const { knexMethod } = this.opt;
|
|
18
|
+
|
|
19
|
+
if (this.opt.bool === 'or') {
|
|
20
|
+
return knexBuilder.orWhere((qb) => qb[knexMethod](column, value));
|
|
21
|
+
} else {
|
|
22
|
+
return knexBuilder[knexMethod](column, value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
jsonApi.warnIfNotPostgres(knex);
|
|
8
27
|
const args = this.getKnexArgs(builder);
|
|
9
28
|
|
|
10
29
|
const rawArgs = jsonApi.whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams(
|
|
@@ -22,6 +41,20 @@ class WhereJsonPostgresOperation extends ObjectionToKnexConvertingOperation {
|
|
|
22
41
|
|
|
23
42
|
return knexBuilder;
|
|
24
43
|
}
|
|
44
|
+
|
|
45
|
+
// Only the simple form with a plain column and a plain object or array value
|
|
46
|
+
// can be passed on to knex, as knex doesn't understand field expressions.
|
|
47
|
+
canDelegateToKnex(knexBuilder) {
|
|
48
|
+
const [column, value] = this.args;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
!!this.opt.knexMethod &&
|
|
52
|
+
isFunction(knexBuilder[this.opt.knexMethod]) &&
|
|
53
|
+
isString(column) &&
|
|
54
|
+
parseFieldExpression(column).access.length === 0 &&
|
|
55
|
+
(isPlainObject(value) || Array.isArray(value))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
25
58
|
}
|
|
26
59
|
|
|
27
60
|
module.exports = {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const parser = require('../../../utils/parseFieldExpression');
|
|
4
4
|
const { asArray, isObject, isString } = require('../../../utils/objectUtils');
|
|
5
|
-
const { isKnexRaw, isKnexQueryBuilder } = require('../../../utils/knexUtils');
|
|
5
|
+
const { isKnexRaw, isKnexQueryBuilder, isPostgres } = require('../../../utils/knexUtils');
|
|
6
|
+
const { deprecate } = require('../../../utils/deprecate');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @typedef {String} FieldExpression
|
|
@@ -44,6 +45,19 @@ const { isKnexRaw, isKnexQueryBuilder } = require('../../../utils/knexUtils');
|
|
|
44
45
|
* not currently supported
|
|
45
46
|
*/
|
|
46
47
|
|
|
48
|
+
// JSON field expressions and the `whereJson*()` methods generate Postgres-only
|
|
49
|
+
// SQL (ditojs#113). As some of it happens to run on other databases, only warn.
|
|
50
|
+
// TODO: Consider throwing an error instead in objection 4.0.
|
|
51
|
+
function warnIfNotPostgres(knex) {
|
|
52
|
+
if (!isPostgres(knex)) {
|
|
53
|
+
deprecate(
|
|
54
|
+
'JSON field expressions and the whereJson*() methods of objection are only supported ' +
|
|
55
|
+
'on PostgreSQL. Use the JSON methods of knex like whereJsonPath() or jsonExtract() ' +
|
|
56
|
+
'with other databases.',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
// Returns the SQL and bindings of a field expression. The column is passed as an
|
|
48
62
|
// identifier binding (`??`), so that knex's `wrapIdentifier` (e.g. used by
|
|
49
63
|
// `knexSnakeCaseMappers()`) is applied to it.
|
|
@@ -168,6 +182,7 @@ function normalizeOperator(knex, operator) {
|
|
|
168
182
|
}
|
|
169
183
|
|
|
170
184
|
module.exports = {
|
|
185
|
+
warnIfNotPostgres,
|
|
171
186
|
parseFieldExpression: parseFieldExpression,
|
|
172
187
|
whereJsonbRefOnLeftJsonbValOrRefOnRight: whereJsonbRefOnLeftJsonbValOrRefOnRight,
|
|
173
188
|
whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams:
|
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { flatten } = require('../../../utils/objectUtils');
|
|
3
|
+
const { flatten, isString, isPlainObject } = require('../../../utils/objectUtils');
|
|
4
4
|
const { Selection } = require('./Selection');
|
|
5
5
|
const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConvertingOperation');
|
|
6
6
|
|
|
7
7
|
const COUNT_REGEX = /count/i;
|
|
8
|
+
const ALIAS_REGEX = /\s+as\s+/i;
|
|
9
|
+
|
|
10
|
+
const AGGREGATE_METHODS = new Set([
|
|
11
|
+
'count',
|
|
12
|
+
'countDistinct',
|
|
13
|
+
'min',
|
|
14
|
+
'max',
|
|
15
|
+
'sum',
|
|
16
|
+
'sumDistinct',
|
|
17
|
+
'avg',
|
|
18
|
+
'avgDistinct',
|
|
19
|
+
]);
|
|
8
20
|
|
|
9
21
|
class SelectOperation extends ObjectionToKnexConvertingOperation {
|
|
10
22
|
constructor(name, opt) {
|
|
11
23
|
super(name, opt);
|
|
12
24
|
this.selections = [];
|
|
25
|
+
// The selections that aggregates select, by their aliases. `null` for
|
|
26
|
+
// non-aggregate operations.
|
|
27
|
+
this.aggregateSelections = AGGREGATE_METHODS.has(name) ? [] : null;
|
|
13
28
|
}
|
|
14
29
|
|
|
15
30
|
onAdd(builder, args) {
|
|
@@ -31,6 +46,12 @@ class SelectOperation extends ObjectionToKnexConvertingOperation {
|
|
|
31
46
|
}
|
|
32
47
|
}
|
|
33
48
|
|
|
49
|
+
if (this.aggregateSelections) {
|
|
50
|
+
this.aggregateSelections = getAggregateAliases(selections).map(
|
|
51
|
+
(alias) => new Selection(null, null, alias),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
return ret;
|
|
35
56
|
}
|
|
36
57
|
|
|
@@ -57,10 +78,27 @@ class SelectOperation extends ObjectionToKnexConvertingOperation {
|
|
|
57
78
|
clone() {
|
|
58
79
|
const clone = super.clone();
|
|
59
80
|
clone.selections = this.selections.slice();
|
|
81
|
+
clone.aggregateSelections = this.aggregateSelections && this.aggregateSelections.slice();
|
|
60
82
|
return clone;
|
|
61
83
|
}
|
|
62
84
|
}
|
|
63
85
|
|
|
86
|
+
// Returns the aliases of the columns an aggregate selects, supporting the same
|
|
87
|
+
// argument forms as knex: `count('col as alias')`, `count('col', { as: 'alias' })`
|
|
88
|
+
// and `count({ alias: 'col' })`. Aggregates without an alias are skipped, as
|
|
89
|
+
// their column name differs between databases.
|
|
90
|
+
function getAggregateAliases([column, options]) {
|
|
91
|
+
if (isString(column) && ALIAS_REGEX.test(column)) {
|
|
92
|
+
return [column.split(ALIAS_REGEX)[1].trim()];
|
|
93
|
+
} else if (isPlainObject(options) && isString(options.as)) {
|
|
94
|
+
return [options.as];
|
|
95
|
+
} else if (isPlainObject(column)) {
|
|
96
|
+
return Object.keys(column);
|
|
97
|
+
} else {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
64
102
|
module.exports = {
|
|
65
103
|
SelectOperation,
|
|
66
104
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { FindOperation } = require('../queryBuilder/operations/FindOperation');
|
|
4
|
+
const { deprecate } = require('../utils/deprecate');
|
|
4
5
|
|
|
5
6
|
class RelationFindOperation extends FindOperation {
|
|
6
7
|
constructor(name, opt) {
|
|
@@ -19,8 +20,13 @@ class RelationFindOperation extends FindOperation {
|
|
|
19
20
|
this.maybeApplyAlias(builder);
|
|
20
21
|
this.relation.findQuery(builder, this.owner);
|
|
21
22
|
|
|
22
|
-
if (this.
|
|
23
|
-
this.
|
|
23
|
+
if (this.owner.isModels) {
|
|
24
|
+
this.warnOnMissingOwnerProps();
|
|
25
|
+
|
|
26
|
+
if (this.assignResultToOwner) {
|
|
27
|
+
this.warnOnArrayOwnerProps();
|
|
28
|
+
this.selectMissingJoinColumns(builder);
|
|
29
|
+
}
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
|
|
@@ -44,6 +50,8 @@ class RelationFindOperation extends FindOperation {
|
|
|
44
50
|
arr.push(rel);
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
this.warnOnMissingRelatedProps(related);
|
|
54
|
+
|
|
47
55
|
for (let i = 0, l = owners.length; i < l; ++i) {
|
|
48
56
|
const own = owners[i];
|
|
49
57
|
const key = this.relation.ownerProp.propKey(own);
|
|
@@ -75,6 +83,62 @@ class RelationFindOperation extends FindOperation {
|
|
|
75
83
|
return super.onAfter3(builder, related);
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
// Owner models without the join columns, e.g. loaded with a partial
|
|
87
|
+
// `select()`, silently get no related models (#1832). Only `undefined`
|
|
88
|
+
// is reported, as `null` is a legit value for a missing relation.
|
|
89
|
+
// TODO: Consider throwing an error instead in objection 4.0.
|
|
90
|
+
warnOnMissingOwnerProps() {
|
|
91
|
+
const { ownerProp } = this.relation;
|
|
92
|
+
|
|
93
|
+
for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
|
|
94
|
+
if (this.owner.modelArray.some((owner) => ownerProp.getProp(owner, c) === undefined)) {
|
|
95
|
+
deprecate(
|
|
96
|
+
`Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
|
|
97
|
+
`some owner models are missing the join property "${ownerProp.propDescription(c)}", ` +
|
|
98
|
+
'so no related models are found for them. Select the column in the owner query.',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Owner join properties holding an array of keys, e.g. a JSON array column,
|
|
105
|
+
// aren't supported: The query finds the related models, but they can't be
|
|
106
|
+
// assigned to their owners, so all owners silently get no related models
|
|
107
|
+
// (#2667). `$relatedQuery()` without assigning the results isn't affected.
|
|
108
|
+
warnOnArrayOwnerProps() {
|
|
109
|
+
const { ownerProp } = this.relation;
|
|
110
|
+
|
|
111
|
+
for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
|
|
112
|
+
if (this.owner.modelArray.some((owner) => Array.isArray(ownerProp.getProp(owner, c)))) {
|
|
113
|
+
deprecate(
|
|
114
|
+
`Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
|
|
115
|
+
`the join property "${ownerProp.propDescription(c)}" of some owner models holds ` +
|
|
116
|
+
'an array, which is not supported, so no related models are found for them. ' +
|
|
117
|
+
'Use a ManyToManyRelation through a join table instead.',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Related models without the join properties can't be assigned to their
|
|
124
|
+
// owners and are silently dropped (#2258). This happens for example when the
|
|
125
|
+
// relation mapping uses column names but the rows come back with different
|
|
126
|
+
// property names, e.g. snake_case mappings with `knexSnakeCaseMappers`.
|
|
127
|
+
warnOnMissingRelatedProps(related) {
|
|
128
|
+
const { relatedProp } = this.relation;
|
|
129
|
+
|
|
130
|
+
for (let c = 0, lc = relatedProp.size; c < lc; ++c) {
|
|
131
|
+
if (related.some((rel) => relatedProp.getProp(rel, c) === undefined)) {
|
|
132
|
+
deprecate(
|
|
133
|
+
`Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
|
|
134
|
+
`some related models are missing the join property "${relatedProp.propDescription(c)}", ` +
|
|
135
|
+
'so they are not assigned to any owner. Check that the relation mapping uses the ' +
|
|
136
|
+
'property names of the models, e.g. camelCase names when using knexSnakeCaseMappers.',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
78
142
|
selectMissingJoinColumns(builder) {
|
|
79
143
|
const relatedProp = this.relation.relatedProp;
|
|
80
144
|
const addedSelects = [];
|
|
@@ -31,10 +31,11 @@ class RelationProperty {
|
|
|
31
31
|
//
|
|
32
32
|
// modelClassResolver must be a function that takes a table name
|
|
33
33
|
// and returns a model class.
|
|
34
|
-
constructor(references, modelClassResolver) {
|
|
34
|
+
constructor(references, modelClassResolver, { allowEmpty = false } = {}) {
|
|
35
35
|
const refs = createRefs(asArray(references));
|
|
36
36
|
const paths = createPaths(refs, modelClassResolver);
|
|
37
|
-
const modelClass =
|
|
37
|
+
const modelClass =
|
|
38
|
+
allowEmpty && refs.length === 0 ? modelClassResolver(null) : resolveModelClass(paths);
|
|
38
39
|
|
|
39
40
|
this._refs = refs.map((ref) => ref.model(modelClass));
|
|
40
41
|
this._modelClass = modelClass;
|
|
@@ -75,10 +75,39 @@ class ManyToManyRelation extends Relation {
|
|
|
75
75
|
return [];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
isJoinTableRelatedTable(builder) {
|
|
79
|
+
return builder.tableNameFor(this.joinTable) === builder.tableNameFor(this.relatedModelClass);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Returns the alias of the join table in `findQuery`. If the join table is
|
|
83
|
+
// also the related table, it needs an alias of its own (#1803).
|
|
84
|
+
joinTableAliasFor(builder) {
|
|
85
|
+
const joinTableRef = builder.tableRefFor(this.joinTable);
|
|
86
|
+
|
|
87
|
+
if (this.isJoinTableRelatedTable(builder)) {
|
|
88
|
+
return this.ownerModelClass.joinTableAlias(joinTableRef);
|
|
89
|
+
} else {
|
|
90
|
+
return joinTableRef;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The modify operations can't tell the join rows apart from the related rows
|
|
95
|
+
// if the join table is also the related table, so they would modify the
|
|
96
|
+
// wrong rows (#1803).
|
|
97
|
+
assertCanModifyJoinTable(builder, operationName) {
|
|
98
|
+
if (this.isJoinTableRelatedTable(builder)) {
|
|
99
|
+
throw this.createError(
|
|
100
|
+
`${operationName} is not supported when the join table is also the related table`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
findQuery(builder, owner) {
|
|
79
|
-
const joinTableOwnerRefs = this.joinTableOwnerProp.refs(builder);
|
|
80
106
|
const joinTable = builder.tableNameFor(this.joinTable);
|
|
81
|
-
const joinTableAlias =
|
|
107
|
+
const joinTableAlias = this.joinTableAliasFor(builder);
|
|
108
|
+
const joinTableOwnerRefs = this.joinTableOwnerProp
|
|
109
|
+
.refs(builder)
|
|
110
|
+
.map((ref) => ref.table(joinTableAlias));
|
|
82
111
|
|
|
83
112
|
let joinTableSelect = this.joinTableModelClass
|
|
84
113
|
.query()
|
|
@@ -92,7 +121,7 @@ class ManyToManyRelation extends Relation {
|
|
|
92
121
|
builder.join(joinTableSelect, (join) => {
|
|
93
122
|
this.relatedProp.forEach((i) => {
|
|
94
123
|
const relatedRef = this.relatedProp.ref(builder, i);
|
|
95
|
-
const joinTableRelatedRef = this.joinTableRelatedProp.ref(builder, i);
|
|
124
|
+
const joinTableRelatedRef = this.joinTableRelatedProp.ref(builder, i).table(joinTableAlias);
|
|
96
125
|
|
|
97
126
|
join.on(relatedRef, joinTableRelatedRef);
|
|
98
127
|
});
|
|
@@ -196,6 +225,8 @@ class ManyToManyRelation extends Relation {
|
|
|
196
225
|
}
|
|
197
226
|
|
|
198
227
|
unrelate(builder, owner) {
|
|
228
|
+
this.assertCanModifyJoinTable(builder, 'unrelate');
|
|
229
|
+
|
|
199
230
|
return new (getUnrelateOperationClass(builder))('unrelate', {
|
|
200
231
|
relation: this,
|
|
201
232
|
owner,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { RelationFindOperation } = require('../../RelationFindOperation');
|
|
4
4
|
const { getTempColumn } = require('../../../utils/tmpColumnUtils');
|
|
5
|
+
const { deprecate } = require('../../../utils/deprecate');
|
|
5
6
|
|
|
6
7
|
class ManyToManyFindOperation extends RelationFindOperation {
|
|
7
8
|
constructor(name, opt) {
|
|
@@ -29,13 +30,17 @@ class ManyToManyFindOperation extends RelationFindOperation {
|
|
|
29
30
|
|
|
30
31
|
// Also select all extra columns.
|
|
31
32
|
for (const extra of this.relation.joinTableExtras) {
|
|
32
|
-
const joinTable =
|
|
33
|
+
const joinTable = this.relation.joinTableAliasFor(builder);
|
|
33
34
|
builder.select(`${joinTable}.${extra.joinTableCol} as ${extra.aliasCol}`);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
if (this.
|
|
38
|
-
this.
|
|
38
|
+
if (this.owner.isModels) {
|
|
39
|
+
this.warnOnMissingOwnerProps();
|
|
40
|
+
|
|
41
|
+
if (this.assignResultToOwner) {
|
|
42
|
+
this.selectMissingJoinColumns(builder);
|
|
43
|
+
}
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
|
|
@@ -60,6 +65,8 @@ class ManyToManyFindOperation extends RelationFindOperation {
|
|
|
60
65
|
arr.push(rel);
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
this.warnOnMissingRelatedProps(related);
|
|
69
|
+
|
|
63
70
|
for (let i = 0, l = owners.length; i < l; ++i) {
|
|
64
71
|
const own = owners[i];
|
|
65
72
|
const key = ownerProp.propKey(own);
|
|
@@ -76,6 +83,27 @@ class ManyToManyFindOperation extends RelationFindOperation {
|
|
|
76
83
|
return related;
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
// The related models are assigned to their owners by the owner join columns
|
|
87
|
+
// selected from the join table. If they get lost, e.g. in a custom
|
|
88
|
+
// `$parseDatabaseJson()`, the related models are silently dropped (#2258).
|
|
89
|
+
warnOnMissingRelatedProps(related) {
|
|
90
|
+
const { joinTableOwnerProp } = this.relation;
|
|
91
|
+
|
|
92
|
+
for (let c = 0, lc = this.ownerJoinColumnAlias.length; c < lc; ++c) {
|
|
93
|
+
const alias = this.ownerJoinColumnAlias[c];
|
|
94
|
+
|
|
95
|
+
if (related.some((rel) => rel[alias] === undefined)) {
|
|
96
|
+
deprecate(
|
|
97
|
+
`Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
|
|
98
|
+
`some related models are missing the join table column ` +
|
|
99
|
+
`"${joinTableOwnerProp.propDescription(c)}" selected as "${alias}", so they are not ` +
|
|
100
|
+
'assigned to any owner. Check that $parseDatabaseJson() and the column name mappers ' +
|
|
101
|
+
'of the related model keep it.',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
79
107
|
clone() {
|
|
80
108
|
const clone = super.clone();
|
|
81
109
|
clone.ownerJoinColumnAlias = this.ownerJoinColumnAlias.slice();
|
|
@@ -88,7 +116,9 @@ class ManyToManyFindOperation extends RelationFindOperation {
|
|
|
88
116
|
// We must select the owner join columns so that we know for which owner model the related
|
|
89
117
|
// models belong to after the requests.
|
|
90
118
|
joinTableOwnerProp.forEach((i) => {
|
|
91
|
-
const joinTableOwnerRef = joinTableOwnerProp
|
|
119
|
+
const joinTableOwnerRef = joinTableOwnerProp
|
|
120
|
+
.ref(builder, i)
|
|
121
|
+
.table(this.relation.joinTableAliasFor(builder));
|
|
92
122
|
const propName = relatedModelClass.columnNameToPropertyName(this.ownerJoinColumnAlias[i]);
|
|
93
123
|
|
|
94
124
|
builder.select(joinTableOwnerRef.as(this.ownerJoinColumnAlias[i]));
|
|
@@ -26,6 +26,10 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
if (this.hasExtraProps) {
|
|
30
|
+
this.relation.assertCanModifyJoinTable(builder, `${this.name} with extra properties`);
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
const res = super.onAdd(builder, args);
|
|
30
34
|
|
|
31
35
|
if (this.hasExtraProps) {
|