objection 3.7.0 → 3.9.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 +123 -50
- package/lib/model/Model.js +2 -0
- package/lib/model/graph/ModelGraphNode.js +2 -1
- package/lib/model/modelJsonAttributes.js +20 -11
- package/lib/model/modelQueryProps.js +49 -5
- package/lib/model/modelSet.js +6 -8
- package/lib/model/modelToJson.js +17 -2
- package/lib/model/modelValidate.js +2 -1
- package/lib/model/modelValues.js +43 -50
- package/lib/queryBuilder/QueryBuilder.js +106 -38
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
- package/lib/queryBuilder/StaticHookArguments.js +4 -1
- package/lib/queryBuilder/graph/GraphData.js +39 -0
- package/lib/queryBuilder/graph/GraphFetcher.js +9 -1
- package/lib/queryBuilder/graph/GraphOptions.js +77 -38
- package/lib/queryBuilder/graph/GraphUpsert.js +98 -14
- package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
- package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
- package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -1
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +3 -2
- package/lib/queryBuilder/join/JoinResultParser.js +13 -8
- package/lib/queryBuilder/join/RelationJoiner.js +105 -12
- package/lib/queryBuilder/join/TableNode.js +6 -68
- package/lib/queryBuilder/join/utils.js +2 -2
- package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
- package/lib/queryBuilder/operations/DeleteOperation.js +13 -0
- package/lib/queryBuilder/operations/FirstOperation.js +5 -3
- package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
- package/lib/queryBuilder/operations/InsertOperation.js +2 -2
- package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +4 -1
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +152 -0
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
- package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
- package/lib/queryBuilder/operations/UpdateOperation.js +54 -5
- 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 +3 -3
- package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
- package/lib/queryBuilder/operations/select/Selection.js +39 -16
- 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 +13 -5
- package/lib/relations/RelationOwner.js +2 -5
- package/lib/relations/RelationProperty.js +3 -14
- package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
- 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/insert/ManyToManyInsertOperation.js +19 -12
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +123 -17
- package/lib/utils/identifierMapping.js +2 -2
- package/lib/utils/knexUtils.js +21 -0
- package/lib/utils/objectUtils.js +5 -0
- package/lib/utils/parseFieldExpression.js +9 -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 +31 -13
- package/lib/utils/deprecate.js +0 -21
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { propKey, hasProps } = require('../../model/modelValues');
|
|
4
|
+
|
|
3
5
|
class TableNode {
|
|
4
6
|
constructor({ tableTree, modelClass, expression, parentNode = null, relation = null }) {
|
|
5
7
|
this.tableTree = tableTree;
|
|
@@ -86,78 +88,14 @@ class TableNode {
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
_createIdGetter() {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (idColumns.length === 1) {
|
|
93
|
-
return createIdGetter(columnAliases);
|
|
94
|
-
} else {
|
|
95
|
-
return createCompositeIdGetter(columnAliases);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function createIdGetter(columnAliases) {
|
|
101
|
-
const columnAlias = columnAliases[0];
|
|
91
|
+
const columnAliases = this.modelClass
|
|
92
|
+
.getIdColumnArray()
|
|
93
|
+
.map((column) => this.getColumnAliasForColumn(column));
|
|
102
94
|
|
|
103
|
-
|
|
104
|
-
const id = flatRow[columnAlias];
|
|
105
|
-
|
|
106
|
-
if (id === null) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return `${id}`;
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function createCompositeIdGetter(columnAliases) {
|
|
115
|
-
if (columnAliases.length === 2) {
|
|
116
|
-
return createTwoIdGetter(columnAliases);
|
|
117
|
-
} else {
|
|
118
|
-
return createMultiIdGetter(columnAliases);
|
|
95
|
+
return (flatRow) => (hasProps(flatRow, columnAliases) ? propKey(flatRow, columnAliases) : null);
|
|
119
96
|
}
|
|
120
97
|
}
|
|
121
98
|
|
|
122
|
-
function createTwoIdGetter(columnAliases) {
|
|
123
|
-
const columnAlias1 = columnAliases[0];
|
|
124
|
-
const columnAlias2 = columnAliases[1];
|
|
125
|
-
|
|
126
|
-
return (flatRow) => {
|
|
127
|
-
const id1 = flatRow[columnAlias1];
|
|
128
|
-
const id2 = flatRow[columnAlias2];
|
|
129
|
-
|
|
130
|
-
if (id1 === null || id2 === null) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return `${id1},${id2}`;
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function createMultiIdGetter(columnAliases) {
|
|
139
|
-
return (flatRow) => {
|
|
140
|
-
let idStr = '';
|
|
141
|
-
|
|
142
|
-
for (let i = 0, l = columnAliases.length; i < l; ++i) {
|
|
143
|
-
const columnAlias = columnAliases[i];
|
|
144
|
-
const id = flatRow[columnAlias];
|
|
145
|
-
|
|
146
|
-
if (id === null) {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
idStr += id;
|
|
151
|
-
|
|
152
|
-
if (i !== l - 1) {
|
|
153
|
-
idStr += ',';
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return idStr;
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
99
|
module.exports = {
|
|
162
100
|
TableNode,
|
|
163
101
|
};
|
|
@@ -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
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
4
4
|
const { StaticHookArguments } = require('../StaticHookArguments');
|
|
5
|
+
const { isMySql } = require('../../utils/knexUtils');
|
|
6
|
+
const { warnOnce } = require('../../utils/warnings');
|
|
5
7
|
|
|
6
8
|
class DeleteOperation extends QueryBuilderOperation {
|
|
7
9
|
async onBefore2(builder, result) {
|
|
@@ -9,6 +11,17 @@ class DeleteOperation extends QueryBuilderOperation {
|
|
|
9
11
|
return result;
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
// knex leaves the order out of deletes on MySQL, but keeps their limit, so
|
|
15
|
+
// the order doesn't pick the deleted rows. Warn when the query is executed.
|
|
16
|
+
onAfterBuild(builder) {
|
|
17
|
+
if (isMySql(builder.knex()) && builder.has('limit') && builder.has(/^orderBy/)) {
|
|
18
|
+
warnOnce(
|
|
19
|
+
'orderBy() has no effect in delete queries with limit() on MySQL, so the limit ' +
|
|
20
|
+
'deletes rows in any order. This will throw an error in objection 4.0.',
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
onBuildKnex(knexBuilder) {
|
|
13
26
|
return knexBuilder.delete();
|
|
14
27
|
}
|
|
@@ -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,7 +3,7 @@
|
|
|
3
3
|
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
4
4
|
const { StaticHookArguments } = require('../StaticHookArguments');
|
|
5
5
|
const { after, mapAfterAllReturn } = require('../../utils/promiseUtils');
|
|
6
|
-
const {
|
|
6
|
+
const { isSqlite, isMySql, hasBatchInsert } = require('../../utils/knexUtils');
|
|
7
7
|
const { isObject, isString } = require('../../utils/objectUtils');
|
|
8
8
|
|
|
9
9
|
// Base class for all insert operations.
|
|
@@ -27,7 +27,7 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async onBefore2(builder, result) {
|
|
30
|
-
if (this.models.length > 1 && !
|
|
30
|
+
if (this.models.length > 1 && !hasBatchInsert(builder.knex())) {
|
|
31
31
|
throw new Error('batch insert only works with Postgresql and SQL Server');
|
|
32
32
|
} else {
|
|
33
33
|
await callBeforeInsert(builder, this.models);
|
|
@@ -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,8 @@ 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');
|
|
8
|
+
const { removeOverriddenQueryProps } = require('../../model/modelQueryProps');
|
|
7
9
|
|
|
8
10
|
class InstanceUpdateOperation extends UpdateOperation {
|
|
9
11
|
constructor(name, opt) {
|
|
@@ -18,6 +20,7 @@ class InstanceUpdateOperation extends UpdateOperation {
|
|
|
18
20
|
|
|
19
21
|
if (!this.model) {
|
|
20
22
|
this.model = this.instance;
|
|
23
|
+
removeOverriddenQueryProps(this.model);
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
return retVal;
|
|
@@ -27,7 +30,7 @@ class InstanceUpdateOperation extends UpdateOperation {
|
|
|
27
30
|
super.onBuild(builder);
|
|
28
31
|
|
|
29
32
|
assertHasId(this.instance);
|
|
30
|
-
builder.findById(this.instance.$id());
|
|
33
|
+
addWhereGroup(builder, () => builder.findById(this.instance.$id()));
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
async onAfter2(builder, result) {
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
const { Selection, isSameTable, withoutSchema } = require('./select/Selection');
|
|
9
|
+
|
|
10
|
+
// Without underscores, so that column name mappers don't change it.
|
|
11
|
+
const rowNumberColumn = 'objectiontmprownum';
|
|
12
|
+
|
|
13
|
+
// Applies the `limit()`, `offset()` and `first()` of a relation query that
|
|
14
|
+
// fetches the related models of many parents to each parent separately.
|
|
15
|
+
class LimitPerParentOperation extends QueryBuilderOperation {
|
|
16
|
+
constructor(name, opt) {
|
|
17
|
+
super(name, opt);
|
|
18
|
+
this.findOperation = opt.findOperation;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
onBuild(builder) {
|
|
22
|
+
builder.addInternalSelection(rowNumberColumn);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Return the limit and offset of the query, built into `knexBuilder` by any
|
|
26
|
+
// of its operations, or `null` if it has none:
|
|
27
|
+
getRange(knexBuilder, builder) {
|
|
28
|
+
// `page()` and `range()` keep limiting all parents, as their total does.
|
|
29
|
+
if (!builder.has(RangeOperation)) {
|
|
30
|
+
const { limit = null, offset = null } = knexBuilder._single;
|
|
31
|
+
// `first()` only limits the query with `useLimitInFirst`:
|
|
32
|
+
const isFirst = builder.has(FirstOperation);
|
|
33
|
+
|
|
34
|
+
if (limit !== null || offset !== null || isFirst) {
|
|
35
|
+
return { limit: isFirst ? 1 : limit, offset: offset ?? 0 };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Called by `buildKnexQuery()` after the default table and select are set.
|
|
43
|
+
onAfterBuildKnex(knexBuilder, builder) {
|
|
44
|
+
const knex = builder.knex();
|
|
45
|
+
const range = this.getRange(knexBuilder, builder);
|
|
46
|
+
|
|
47
|
+
if (!range) {
|
|
48
|
+
return knexBuilder;
|
|
49
|
+
} else if (!hasWindowFunctions(knex)) {
|
|
50
|
+
// `WhereInEagerOperation` fetches the related models of each parent with
|
|
51
|
+
// a separate query then, so only `first()` still needs to be applied:
|
|
52
|
+
return range.limit === null ? knexBuilder : knexBuilder.limit(range.limit);
|
|
53
|
+
} else {
|
|
54
|
+
const { limit, offset } = range;
|
|
55
|
+
// Number the rows of each parent one level up, aliased as the related
|
|
56
|
+
// table, so that the order can refer to the selected columns and their
|
|
57
|
+
// aliases as in the query itself, e.g. to `id` when a joined table has
|
|
58
|
+
// an `id` too. The order moves into the window function, as SQL Server
|
|
59
|
+
// doesn't allow it in subqueries. An alias can't have a schema.
|
|
60
|
+
const table = withoutSchema(builder.tableRef());
|
|
61
|
+
// knex only accepts raw partitions wrapped in objects:
|
|
62
|
+
const partition = this.findOperation.ownerKeys(builder, table).map((column) => ({ column }));
|
|
63
|
+
const numberedQuery = knex
|
|
64
|
+
.queryBuilder()
|
|
65
|
+
.select('*')
|
|
66
|
+
.rowNumber(rowNumberColumn, (window) =>
|
|
67
|
+
window.orderBy(getWindowOrder(builder, knex, table)).partitionBy(partition),
|
|
68
|
+
)
|
|
69
|
+
.from(
|
|
70
|
+
knexBuilder.clone().clear('with').clear('limit').clear('offset').clear('order').as(table),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Select the rows in range with the query itself, to keep its settings
|
|
74
|
+
// like `timeout()`, and its common table expressions.
|
|
75
|
+
knexBuilder
|
|
76
|
+
.clear('select')
|
|
77
|
+
.clear('where')
|
|
78
|
+
.clear('join')
|
|
79
|
+
.clear('union')
|
|
80
|
+
.clear('group')
|
|
81
|
+
.clear('having')
|
|
82
|
+
.clear('order')
|
|
83
|
+
.clear('limit')
|
|
84
|
+
.clear('offset')
|
|
85
|
+
.select('*')
|
|
86
|
+
.from(numberedQuery.as(rowNumberColumn))
|
|
87
|
+
.where(rowNumberColumn, '>', offset)
|
|
88
|
+
.orderBy(rowNumberColumn);
|
|
89
|
+
|
|
90
|
+
if (limit !== null) {
|
|
91
|
+
// knex also accepts raw offsets:
|
|
92
|
+
const end = isNumber(offset) ? offset + limit : knex.raw('? + ?', [offset, limit]);
|
|
93
|
+
knexBuilder.where(rowNumberColumn, '<=', end);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return knexBuilder;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Convert the `orderBy()` and `orderByRaw()` calls of the query to the order
|
|
102
|
+
// of knex's window functions, which can only refer to the related table by its
|
|
103
|
+
// alias `table`.
|
|
104
|
+
function getWindowOrder(builder, knex, table) {
|
|
105
|
+
const order = [];
|
|
106
|
+
const tableRef = builder.tableRef();
|
|
107
|
+
|
|
108
|
+
const getAliasedColumn = (column) => {
|
|
109
|
+
const selection = isString(column) && Selection.create(column);
|
|
110
|
+
return selection?.table && isSameTable(builder, selection.table, tableRef)
|
|
111
|
+
? `${table}.${selection.column}`
|
|
112
|
+
: column;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
builder.forEachOperation(builder.constructor.OrderBySelector, (op) => {
|
|
116
|
+
const args = op.getKnexArgs(builder);
|
|
117
|
+
|
|
118
|
+
if (op.name === 'orderByRaw') {
|
|
119
|
+
const [sql, bindings] = args;
|
|
120
|
+
order.push({ column: isKnexRaw(sql) ? sql : knex.raw(sql, bindings) });
|
|
121
|
+
} else {
|
|
122
|
+
const [column, direction, nulls] = args;
|
|
123
|
+
const items = Array.isArray(column)
|
|
124
|
+
? column.map((item) => (isString(item) ? { column: item } : item))
|
|
125
|
+
: [{ column, order: direction, nulls }];
|
|
126
|
+
|
|
127
|
+
for (const item of items) {
|
|
128
|
+
const aliasedColumn = getAliasedColumn(item.column);
|
|
129
|
+
|
|
130
|
+
if (item.nulls) {
|
|
131
|
+
// Not all databases support `nulls first` and `nulls last`:
|
|
132
|
+
order.push({
|
|
133
|
+
column: knex.raw('(case when ? is null then 0 else 1 end)', [
|
|
134
|
+
isString(aliasedColumn) ? knex.ref(aliasedColumn) : aliasedColumn,
|
|
135
|
+
]),
|
|
136
|
+
order: item.nulls === 'first' ? 'asc' : 'desc',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Unlike in `orderBy()`, knex adds the direction to the window as it is:
|
|
141
|
+
order.push({ column: aliasedColumn, order: /^desc$/i.test(item.order) ? 'desc' : 'asc' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// SQL Server requires an order in the window function:
|
|
147
|
+
return order.length ? order : [{ column: knex.raw('(select null)') }];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
LimitPerParentOperation,
|
|
152
|
+
};
|
|
@@ -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
|
};
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { ref } = require('../../queryBuilder/ReferenceBuilder');
|
|
4
|
-
const { isEmpty } = require('../../utils/objectUtils');
|
|
5
|
-
const {
|
|
4
|
+
const { isEmpty, isObject } = require('../../utils/objectUtils');
|
|
5
|
+
const {
|
|
6
|
+
isFieldExpressionKey,
|
|
7
|
+
toPostgresJsonPathLiteral,
|
|
8
|
+
} = require('../../utils/parseFieldExpression');
|
|
6
9
|
const { isKnexRaw, isKnexQueryBuilder } = require('../../utils/knexUtils');
|
|
7
10
|
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
8
11
|
const { warnIfNotPostgres } = require('./jsonApi/postgresJsonApi');
|
|
9
12
|
const { StaticHookArguments } = require('../StaticHookArguments');
|
|
13
|
+
const { ValidationErrorType } = require('../../model/ValidationError');
|
|
10
14
|
|
|
11
15
|
class UpdateOperation extends QueryBuilderOperation {
|
|
12
16
|
constructor(name, opt) {
|
|
@@ -107,17 +111,39 @@ async function callStaticAfterUpdate(builder, result) {
|
|
|
107
111
|
|
|
108
112
|
function convertFieldExpressionsToRaw(builder, model, json) {
|
|
109
113
|
const knex = builder.knex();
|
|
114
|
+
const modelClass = builder.modelClass();
|
|
115
|
+
const declaredColumns = getDeclaredColumns(modelClass);
|
|
110
116
|
const convertedJson = {};
|
|
111
117
|
|
|
112
118
|
for (const key of Object.keys(json)) {
|
|
113
119
|
let val = json[key];
|
|
114
120
|
|
|
115
|
-
if (key
|
|
121
|
+
if (isFieldExpressionKey(key, declaredColumns)) {
|
|
116
122
|
// 'col:attr' : ref('other:lol') is transformed to
|
|
117
|
-
// "col" : raw(`jsonb_set("col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
|
|
123
|
+
// "col" : raw(`jsonb_set("table"."col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
|
|
124
|
+
// with the column qualified, as it is ambiguous with `excluded` in merge().
|
|
118
125
|
warnIfNotPostgres(knex);
|
|
119
126
|
|
|
120
127
|
let parsed = ref(key);
|
|
128
|
+
|
|
129
|
+
// The field expression would be lost or produce invalid SQL next to a
|
|
130
|
+
// value for the whole json column. Report it with the property names:
|
|
131
|
+
if (Object.prototype.hasOwnProperty.call(json, parsed.column)) {
|
|
132
|
+
const property = modelClass.columnNameToPropertyName(parsed.column);
|
|
133
|
+
throw modelClass.createValidationError({
|
|
134
|
+
type: ValidationErrorType.ModelValidation,
|
|
135
|
+
data: {
|
|
136
|
+
[modelClass.columnNameToPropertyName(key)]: [
|
|
137
|
+
{
|
|
138
|
+
message: `can't be combined with a value for the json column '${property}'`,
|
|
139
|
+
keyword: null,
|
|
140
|
+
params: { property },
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
121
147
|
let jsonPath = toPostgresJsonPathLiteral(parsed.parsedExpr.access);
|
|
122
148
|
let valuePlaceholder = '?';
|
|
123
149
|
|
|
@@ -129,7 +155,7 @@ function convertFieldExpressionsToRaw(builder, model, json) {
|
|
|
129
155
|
|
|
130
156
|
convertedJson[parsed.column] = knex.raw(
|
|
131
157
|
`jsonb_set(??, ${jsonPath}, ${valuePlaceholder}, true)`,
|
|
132
|
-
[convertedJson[parsed.column] || parsed.column
|
|
158
|
+
[convertedJson[parsed.column] || `${builder.tableRef()}.${parsed.column}`, val],
|
|
133
159
|
);
|
|
134
160
|
|
|
135
161
|
delete model[key];
|
|
@@ -141,7 +167,30 @@ function convertFieldExpressionsToRaw(builder, model, json) {
|
|
|
141
167
|
return convertedJson;
|
|
142
168
|
}
|
|
143
169
|
|
|
170
|
+
const declaredColumnsByModelClass = new WeakMap();
|
|
171
|
+
|
|
172
|
+
// The keys of `json` are column names, so map the properties with a ':' that the
|
|
173
|
+
// schema declares to their columns, once per model class. Properties that only
|
|
174
|
+
// the schema returned by `$beforeValidate()` declares aren't known here.
|
|
175
|
+
function getDeclaredColumns(modelClass) {
|
|
176
|
+
let declaredColumns = declaredColumnsByModelClass.get(modelClass);
|
|
177
|
+
|
|
178
|
+
if (!declaredColumns) {
|
|
179
|
+
const jsonSchema = modelClass.getJsonSchema();
|
|
180
|
+
const properties = jsonSchema && jsonSchema.properties;
|
|
181
|
+
declaredColumns = Object.fromEntries(
|
|
182
|
+
(isObject(properties) ? Object.keys(properties) : [])
|
|
183
|
+
.filter((key) => key.includes(':'))
|
|
184
|
+
.map((key) => [modelClass.propertyNameToColumnName(key), true]),
|
|
185
|
+
);
|
|
186
|
+
declaredColumnsByModelClass.set(modelClass, declaredColumns);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return declaredColumns;
|
|
190
|
+
}
|
|
191
|
+
|
|
144
192
|
module.exports = {
|
|
145
193
|
UpdateOperation,
|
|
146
194
|
convertFieldExpressionsToRaw,
|
|
195
|
+
getDeclaredColumns,
|
|
147
196
|
};
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
4
4
|
|
|
5
5
|
// Builds a set of where operations inside a single knex `where(callback)`
|
|
6
|
-
// group, i.e. wraps them in parentheses. See `
|
|
6
|
+
// group, i.e. wraps them in parentheses. See `getWhereGroupOperations()`.
|
|
7
7
|
class WhereGroupOperation extends QueryBuilderOperation {
|
|
8
8
|
constructor(name, opt) {
|
|
9
9
|
super(name, opt);
|
|
10
|
-
this.operations =
|
|
10
|
+
this.operations = opt.operations;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
onBuildKnex(knexBuilder, builder) {
|
|
@@ -28,12 +28,6 @@ class WhereGroupOperation extends QueryBuilderOperation {
|
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
clone() {
|
|
33
|
-
const clone = super.clone();
|
|
34
|
-
clone.operations = this.operations.map((op) => op.clone());
|
|
35
|
-
return clone;
|
|
36
|
-
}
|
|
37
31
|
}
|
|
38
32
|
|
|
39
33
|
module.exports = {
|
|
@@ -20,6 +20,11 @@ class JoinEagerOperation extends EagerOperation {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
onBefore3(builder) {
|
|
23
|
+
return this.fetchTableMetadata(builder);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Also called by `resultSize()`, which builds the query without executing it.
|
|
27
|
+
fetchTableMetadata(builder) {
|
|
23
28
|
return this.joiner
|
|
24
29
|
.setExpression(this.buildFinalExpression(builder))
|
|
25
30
|
.setModifiers(this.buildFinalModifiers(builder))
|