objection 3.4.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/queryBuilder/QueryBuilderBase.js +44 -9
- package/lib/queryBuilder/ReferenceBuilder.js +8 -1
- package/lib/queryBuilder/join/RelationJoiner.js +53 -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 +20 -0
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +30 -2
- package/package.json +1 -1
|
@@ -616,9 +616,14 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
616
616
|
// whereJson(Not)SupersetOf / whereJson(Not)SubsetOf are now supported by knex >= 1.0, but for now
|
|
617
617
|
// objection handles them differently and only for postgres.
|
|
618
618
|
// Changing them to utilize knex methods directly may require a major version bump and upgrade guide.
|
|
619
|
+
// On MySQL, the simple `(column, object | array)` form is passed on to knex (ditojs#113).
|
|
619
620
|
whereJsonSupersetOf(...args) {
|
|
620
621
|
return this.addOperation(
|
|
621
|
-
new WhereJsonPostgresOperation('whereJsonSupersetOf', {
|
|
622
|
+
new WhereJsonPostgresOperation('whereJsonSupersetOf', {
|
|
623
|
+
operator: '@>',
|
|
624
|
+
bool: 'and',
|
|
625
|
+
knexMethod: 'whereJsonSupersetOf',
|
|
626
|
+
}),
|
|
622
627
|
args,
|
|
623
628
|
);
|
|
624
629
|
}
|
|
@@ -629,7 +634,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
629
634
|
|
|
630
635
|
orWhereJsonSupersetOf(...args) {
|
|
631
636
|
return this.addOperation(
|
|
632
|
-
new WhereJsonPostgresOperation('orWhereJsonSupersetOf', {
|
|
637
|
+
new WhereJsonPostgresOperation('orWhereJsonSupersetOf', {
|
|
638
|
+
operator: '@>',
|
|
639
|
+
bool: 'or',
|
|
640
|
+
knexMethod: 'whereJsonSupersetOf',
|
|
641
|
+
}),
|
|
633
642
|
args,
|
|
634
643
|
);
|
|
635
644
|
}
|
|
@@ -640,13 +649,14 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
640
649
|
operator: '@>',
|
|
641
650
|
bool: 'and',
|
|
642
651
|
prefix: 'not',
|
|
652
|
+
knexMethod: 'whereJsonNotSupersetOf',
|
|
643
653
|
}),
|
|
644
654
|
args,
|
|
645
655
|
);
|
|
646
656
|
}
|
|
647
657
|
|
|
648
658
|
andWhereJsonNotSupersetOf(...args) {
|
|
649
|
-
return this.
|
|
659
|
+
return this.whereJsonNotSupersetOf(...args);
|
|
650
660
|
}
|
|
651
661
|
|
|
652
662
|
orWhereJsonNotSupersetOf(...args) {
|
|
@@ -655,6 +665,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
655
665
|
operator: '@>',
|
|
656
666
|
bool: 'or',
|
|
657
667
|
prefix: 'not',
|
|
668
|
+
knexMethod: 'whereJsonNotSupersetOf',
|
|
658
669
|
}),
|
|
659
670
|
args,
|
|
660
671
|
);
|
|
@@ -662,7 +673,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
662
673
|
|
|
663
674
|
whereJsonSubsetOf(...args) {
|
|
664
675
|
return this.addOperation(
|
|
665
|
-
new WhereJsonPostgresOperation('whereJsonSubsetOf', {
|
|
676
|
+
new WhereJsonPostgresOperation('whereJsonSubsetOf', {
|
|
677
|
+
operator: '<@',
|
|
678
|
+
bool: 'and',
|
|
679
|
+
knexMethod: 'whereJsonSubsetOf',
|
|
680
|
+
}),
|
|
666
681
|
args,
|
|
667
682
|
);
|
|
668
683
|
}
|
|
@@ -673,7 +688,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
673
688
|
|
|
674
689
|
orWhereJsonSubsetOf(...args) {
|
|
675
690
|
return this.addOperation(
|
|
676
|
-
new WhereJsonPostgresOperation('orWhereJsonSubsetOf', {
|
|
691
|
+
new WhereJsonPostgresOperation('orWhereJsonSubsetOf', {
|
|
692
|
+
operator: '<@',
|
|
693
|
+
bool: 'or',
|
|
694
|
+
knexMethod: 'whereJsonSubsetOf',
|
|
695
|
+
}),
|
|
677
696
|
args,
|
|
678
697
|
);
|
|
679
698
|
}
|
|
@@ -684,6 +703,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
684
703
|
operator: '<@',
|
|
685
704
|
bool: 'and',
|
|
686
705
|
prefix: 'not',
|
|
706
|
+
knexMethod: 'whereJsonNotSubsetOf',
|
|
687
707
|
}),
|
|
688
708
|
args,
|
|
689
709
|
);
|
|
@@ -699,6 +719,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
699
719
|
operator: '<@',
|
|
700
720
|
bool: 'or',
|
|
701
721
|
prefix: 'not',
|
|
722
|
+
knexMethod: 'whereJsonNotSubsetOf',
|
|
702
723
|
}),
|
|
703
724
|
args,
|
|
704
725
|
);
|
|
@@ -772,20 +793,34 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
|
|
|
772
793
|
);
|
|
773
794
|
}
|
|
774
795
|
|
|
796
|
+
// Not passed on to knex like `whereJsonSupersetOf()`, as MySQL's `json_contains()`
|
|
797
|
+
// also matches `{}` in arrays of objects.
|
|
775
798
|
whereJsonIsArray(fieldExpression) {
|
|
776
|
-
return this.
|
|
799
|
+
return this.addOperation(
|
|
800
|
+
new WhereJsonPostgresOperation('whereJsonSupersetOf', { operator: '@>', bool: 'and' }),
|
|
801
|
+
[fieldExpression, []],
|
|
802
|
+
);
|
|
777
803
|
}
|
|
778
804
|
|
|
779
805
|
orWhereJsonIsArray(fieldExpression) {
|
|
780
|
-
return this.
|
|
806
|
+
return this.addOperation(
|
|
807
|
+
new WhereJsonPostgresOperation('orWhereJsonSupersetOf', { operator: '@>', bool: 'or' }),
|
|
808
|
+
[fieldExpression, []],
|
|
809
|
+
);
|
|
781
810
|
}
|
|
782
811
|
|
|
783
812
|
whereJsonIsObject(fieldExpression) {
|
|
784
|
-
return this.
|
|
813
|
+
return this.addOperation(
|
|
814
|
+
new WhereJsonPostgresOperation('whereJsonSupersetOf', { operator: '@>', bool: 'and' }),
|
|
815
|
+
[fieldExpression, {}],
|
|
816
|
+
);
|
|
785
817
|
}
|
|
786
818
|
|
|
787
819
|
orWhereJsonIsObject(fieldExpression) {
|
|
788
|
-
return this.
|
|
820
|
+
return this.addOperation(
|
|
821
|
+
new WhereJsonPostgresOperation('orWhereJsonSupersetOf', { operator: '@>', bool: 'or' }),
|
|
822
|
+
[fieldExpression, {}],
|
|
823
|
+
);
|
|
789
824
|
}
|
|
790
825
|
|
|
791
826
|
whereColumn(...args) {
|
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
toPostgresJsonPathLiteral,
|
|
6
6
|
} = require('../utils/parseFieldExpression');
|
|
7
7
|
const { isObject } = require('../utils/objectUtils');
|
|
8
|
+
const { warnIfNotPostgres } = require('./operations/jsonApi/postgresJsonApi');
|
|
8
9
|
|
|
9
10
|
class ReferenceBuilder {
|
|
10
11
|
constructor(expr) {
|
|
@@ -163,7 +164,13 @@ class ReferenceBuilder {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
toKnexRaw(builder) {
|
|
166
|
-
|
|
167
|
+
const knex = builder.knex();
|
|
168
|
+
|
|
169
|
+
if (this._parsedExpr.access.length > 0) {
|
|
170
|
+
warnIfNotPostgres(knex);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return knex.raw(...this._createRawArgs(builder));
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
_parseExpression(expr) {
|
|
@@ -2,6 +2,7 @@
|
|
|
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');
|
|
@@ -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()}.*`);
|
|
@@ -268,7 +269,7 @@ function ensureIdAndRelationPropsAreSelected({ builder, tableNode }) {
|
|
|
268
269
|
];
|
|
269
270
|
|
|
270
271
|
const selectStrings = uniqBy(cols)
|
|
271
|
-
.filter((col) => !
|
|
272
|
+
.filter((col) => !hasSelectionAs(builder, col))
|
|
272
273
|
.map((col) => `${tableRef}.${col}`);
|
|
273
274
|
|
|
274
275
|
return builder.select(selectStrings);
|
|
@@ -276,7 +277,7 @@ function ensureIdAndRelationPropsAreSelected({ builder, tableNode }) {
|
|
|
276
277
|
|
|
277
278
|
function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
|
|
278
279
|
const userSelectQuery = userSelectQueries.get(tableNode);
|
|
279
|
-
const userSelections = userSelectQuery
|
|
280
|
+
const userSelections = findUserSelections(userSelectQuery, tableNode);
|
|
280
281
|
const userSelectedAllColumns = isSelectAllSelectionSet(userSelections);
|
|
281
282
|
|
|
282
283
|
let selections = [];
|
|
@@ -298,7 +299,7 @@ function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
|
|
|
298
299
|
const idSelections = getIdSelectionsForNode({ tableNode });
|
|
299
300
|
|
|
300
301
|
for (const idSelection of idSelections) {
|
|
301
|
-
if (!userSelectQuery
|
|
302
|
+
if (!hasUserSelectionAs(userSelectQuery, tableNode, idSelection.column)) {
|
|
302
303
|
selections.push(idSelection);
|
|
303
304
|
internalSelections.push(idSelection);
|
|
304
305
|
}
|
|
@@ -318,6 +319,54 @@ function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
|
|
|
318
319
|
};
|
|
319
320
|
}
|
|
320
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
|
+
|
|
321
370
|
function mapUserSelectionsFromSubqueryToMainQuery({ userSelections, tableNode }) {
|
|
322
371
|
return userSelections.filter(isNotSelectAll).map((selection) => {
|
|
323
372
|
return new Selection(
|
|
@@ -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
|
};
|
|
@@ -24,6 +24,7 @@ class RelationFindOperation extends FindOperation {
|
|
|
24
24
|
this.warnOnMissingOwnerProps();
|
|
25
25
|
|
|
26
26
|
if (this.assignResultToOwner) {
|
|
27
|
+
this.warnOnArrayOwnerProps();
|
|
27
28
|
this.selectMissingJoinColumns(builder);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -100,6 +101,25 @@ class RelationFindOperation extends FindOperation {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
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
|
+
|
|
103
123
|
// Related models without the join properties can't be assigned to their
|
|
104
124
|
// owners and are silently dropped (#2258). This happens for example when the
|
|
105
125
|
// relation mapping uses column names but the rows come back with different
|
|
@@ -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) {
|
|
@@ -34,8 +35,12 @@ class ManyToManyFindOperation extends RelationFindOperation {
|
|
|
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();
|