objection 3.2.0 → 3.4.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/README.md +13 -13
- package/lib/model/AjvValidator.js +220 -13
- 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 +191 -8
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
- package/lib/queryBuilder/ReferenceBuilder.js +5 -3
- package/lib/queryBuilder/RelationExpression.js +13 -1
- package/lib/queryBuilder/StaticHookArguments.js +12 -1
- package/lib/queryBuilder/graph/GraphFetcher.js +1 -3
- package/lib/queryBuilder/graph/GraphOptions.js +30 -1
- package/lib/queryBuilder/graph/GraphUpsert.js +38 -1
- package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
- package/lib/queryBuilder/graph/insert/GraphInsert.js +107 -8
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -1
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +8 -2
- package/lib/queryBuilder/join/JoinResultParser.js +22 -0
- package/lib/queryBuilder/join/RelationJoiner.js +80 -27
- package/lib/queryBuilder/join/utils.js +20 -2
- package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
- package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
- package/lib/queryBuilder/operations/InsertOperation.js +129 -13
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
- package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
- package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
- package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
- package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
- package/lib/queryBuilder/operations/eager/EagerOperation.js +4 -0
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +2 -6
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +10 -33
- package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
- package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +41 -34
- package/lib/queryBuilder/operations/select/Selection.js +95 -4
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
- package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
- package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
- package/lib/queryBuilder/whereGroups.js +125 -0
- package/lib/relations/Relation.js +5 -2
- package/lib/relations/RelationFindOperation.js +46 -2
- package/lib/relations/RelationProperty.js +3 -2
- package/lib/relations/hasMany/HasManyRelateOperation.js +5 -2
- package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
- package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
- package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
- package/lib/relations/manyToMany/ManyToManyRelation.js +104 -51
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +4 -2
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +12 -2
- package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
- package/lib/utils/assert.js +9 -0
- package/lib/utils/deprecate.js +6 -0
- package/lib/utils/identifierMapping.js +15 -1
- package/lib/utils/parseFieldExpression.js +28 -0
- package/lib/utils/wrapDbError.js +36 -0
- package/package.json +8 -7
- package/typings/objection/index.d.ts +281 -29
|
@@ -4,7 +4,7 @@ const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
|
4
4
|
const { StaticHookArguments } = require('../StaticHookArguments');
|
|
5
5
|
const { after, mapAfterAllReturn } = require('../../utils/promiseUtils');
|
|
6
6
|
const { isPostgres, isSqlite, isMySql, isMsSql } = require('../../utils/knexUtils');
|
|
7
|
-
const { isObject } = require('../../utils/objectUtils');
|
|
7
|
+
const { isObject, isString } = require('../../utils/objectUtils');
|
|
8
8
|
|
|
9
9
|
// Base class for all insert operations.
|
|
10
10
|
class InsertOperation extends QueryBuilderOperation {
|
|
@@ -36,35 +36,58 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
onBuildKnex(knexBuilder, builder) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
const knex = builder.knex();
|
|
40
|
+
|
|
41
|
+
const defaultReturning = getDefaultReturning(builder);
|
|
42
|
+
|
|
43
|
+
if (!builder.has(/returning/) && defaultReturning !== null) {
|
|
44
|
+
if (!isSqlite(knex) && !isMySql(knex)) {
|
|
45
|
+
// If the user hasn't specified a `returning` clause, we make sure
|
|
46
|
+
// that at least the identifier is returned. With `onConflict()` the
|
|
47
|
+
// conflict columns are returned too, so that the returned rows can
|
|
48
|
+
// be matched to the models when some of the rows are ignored.
|
|
49
|
+
knexBuilder = knexBuilder.returning(defaultReturning);
|
|
50
|
+
} else if (isSqlite(knex) && builder.has('onConflict')) {
|
|
51
|
+
// Without a `returning` clause, SQLite returns the id of the last
|
|
52
|
+
// inserted row, which belongs to a previous insert if the row was
|
|
53
|
+
// ignored. Use `returning` to find out what was actually inserted.
|
|
54
|
+
knexBuilder = knexBuilder.returning(defaultReturning);
|
|
55
|
+
}
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
return knexBuilder.insert(this.models.map((model) => model.$toDatabaseJson(builder)));
|
|
46
59
|
}
|
|
47
60
|
|
|
48
|
-
onAfter1(
|
|
61
|
+
onAfter1(builder, ret) {
|
|
49
62
|
if (!Array.isArray(ret) || !ret.length || ret === this.models) {
|
|
50
63
|
// Early exit if there is nothing to do.
|
|
51
64
|
return this.models;
|
|
52
65
|
}
|
|
53
66
|
|
|
67
|
+
const isOnConflict = builder.has('onConflict');
|
|
68
|
+
|
|
54
69
|
if (isObject(ret[0])) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
if (isOnConflict && ret.length !== this.models.length) {
|
|
71
|
+
// With `onConflict()`, ignored rows (and rows not updated because of a
|
|
72
|
+
// `where` clause after `merge()`) are not returned, so the returned rows
|
|
73
|
+
// can't be merged to the models based on their position.
|
|
74
|
+
mergeMatchingRows(builder, this.models, ret);
|
|
75
|
+
} else {
|
|
76
|
+
// If the user specified a `returning` clause the result may be an array of objects.
|
|
77
|
+
// Merge all values of the objects to our models.
|
|
78
|
+
for (let i = 0, l = this.models.length; i < l; ++i) {
|
|
79
|
+
this.models[i].$setDatabaseJson(ret[i]);
|
|
80
|
+
}
|
|
59
81
|
}
|
|
60
|
-
} else {
|
|
82
|
+
} else if (builder.modelClass().getIdColumnArray().length > 0) {
|
|
61
83
|
// If the return value is not an array of objects, we assume it is an array of identifiers.
|
|
62
84
|
for (let i = 0, l = this.models.length; i < l; ++i) {
|
|
63
85
|
const model = this.models[i];
|
|
64
86
|
|
|
65
87
|
// Don't set the id if the model already has one. MySQL and Sqlite don't return the correct
|
|
66
|
-
// primary key value if the id is not generated in db, but given explicitly.
|
|
67
|
-
|
|
88
|
+
// primary key value if the id is not generated in db, but given explicitly. With
|
|
89
|
+
// `onConflict()`, MySQL returns `0` for ignored rows, which is not a valid id.
|
|
90
|
+
if (!model.$id() && !(isOnConflict && !ret[i])) {
|
|
68
91
|
model.$id(ret[i]);
|
|
69
92
|
}
|
|
70
93
|
}
|
|
@@ -92,6 +115,99 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
92
115
|
}
|
|
93
116
|
}
|
|
94
117
|
|
|
118
|
+
// Returns `null` if there's nothing to return, e.g. for models without an `idColumn`.
|
|
119
|
+
function getDefaultReturning(builder) {
|
|
120
|
+
const idColumns = builder.modelClass().getIdColumnArray();
|
|
121
|
+
const columns = Array.from(new Set(idColumns.concat(getConflictColumns(builder))));
|
|
122
|
+
|
|
123
|
+
if (columns.length === 0) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return columns.length === 1 ? columns[0] : columns;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getConflictColumns(builder) {
|
|
131
|
+
if (!builder.has('onConflict')) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const op = builder.findOperation('onConflict');
|
|
136
|
+
const arg = op.args ? op.args[0] : undefined;
|
|
137
|
+
const columns = Array.isArray(arg) ? arg : [arg];
|
|
138
|
+
|
|
139
|
+
// Raw conflict targets (or no target at all) can't be used for matching.
|
|
140
|
+
return columns.every(isString) ? columns : [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Merges the rows returned by an insert with `onConflict()` into the models with
|
|
144
|
+
// matching key values. Models whose rows were not returned are left untouched.
|
|
145
|
+
function mergeMatchingRows(builder, models, rows) {
|
|
146
|
+
const jsons = models.map((model) => model.$toDatabaseJson(builder));
|
|
147
|
+
const idColumns = builder.modelClass().getIdColumnArray();
|
|
148
|
+
|
|
149
|
+
// Use the conflict columns, or else the identifiers, to match the rows to
|
|
150
|
+
// the models. The columns must be present in the input and the result.
|
|
151
|
+
const keyColumns = [getConflictColumns(builder), idColumns].find((columns) => {
|
|
152
|
+
return (
|
|
153
|
+
columns.length > 0 &&
|
|
154
|
+
columns.every((col) => {
|
|
155
|
+
return (
|
|
156
|
+
rows.every((row) => row[col] !== undefined) &&
|
|
157
|
+
jsons.every((json) => json[col] !== undefined)
|
|
158
|
+
);
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!keyColumns) {
|
|
164
|
+
throw createMatchError();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const createKey = (obj) => JSON.stringify(keyColumns.map((col) => normalizeKey(obj[col])));
|
|
168
|
+
const indicesByKey = new Map();
|
|
169
|
+
|
|
170
|
+
jsons.forEach((json, index) => {
|
|
171
|
+
const key = createKey(json);
|
|
172
|
+
const indices = indicesByKey.get(key);
|
|
173
|
+
|
|
174
|
+
if (indices) {
|
|
175
|
+
indices.push(index);
|
|
176
|
+
} else {
|
|
177
|
+
indicesByKey.set(key, [index]);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
const indices = indicesByKey.get(createKey(row));
|
|
183
|
+
const index = indices ? indices.shift() : undefined;
|
|
184
|
+
|
|
185
|
+
if (index === undefined) {
|
|
186
|
+
throw createMatchError();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
models[index].$setDatabaseJson(row);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeKey(value) {
|
|
194
|
+
if (value === null || value === undefined) {
|
|
195
|
+
return null;
|
|
196
|
+
} else if (isObject(value)) {
|
|
197
|
+
return JSON.stringify(value);
|
|
198
|
+
} else {
|
|
199
|
+
return String(value);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function createMatchError() {
|
|
204
|
+
return new Error(
|
|
205
|
+
'Could not match the rows returned by an insert with `onConflict()` to the inserted models. ' +
|
|
206
|
+
'Pass the conflict columns to `onConflict()` and make sure they are returned, or provide ' +
|
|
207
|
+
'the identifiers of the inserted models.',
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
95
211
|
function callBeforeInsert(builder, models) {
|
|
96
212
|
const maybePromise = callInstanceBeforeInsert(builder, models);
|
|
97
213
|
return after(maybePromise, () => callStaticBeforeInsert(builder));
|
|
@@ -41,6 +41,9 @@ class QueryBuilderOperation {
|
|
|
41
41
|
|
|
42
42
|
// Operations this operation added in any of its hooks.
|
|
43
43
|
this.childOperations = [];
|
|
44
|
+
|
|
45
|
+
// The where group this operation belongs to. See `whereGroups.js`.
|
|
46
|
+
this.whereGroup = null;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
is(OperationClass) {
|
|
@@ -217,6 +220,7 @@ class QueryBuilderOperation {
|
|
|
217
220
|
|
|
218
221
|
clone.adderHookName = this.adderHookName;
|
|
219
222
|
clone.parentOperation = this.parentOperation;
|
|
223
|
+
clone.whereGroup = this.whereGroup;
|
|
220
224
|
|
|
221
225
|
clone.childOperations = this.childOperations.map((childOp) => {
|
|
222
226
|
const childOpClone = childOp.clone();
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { flatten } = require('../../utils/objectUtils');
|
|
3
|
+
const { flatten, isPlainObject } = require('../../utils/objectUtils');
|
|
4
4
|
const { ObjectionToKnexConvertingOperation } = require('./ObjectionToKnexConvertingOperation');
|
|
5
5
|
|
|
6
|
-
// This class's
|
|
6
|
+
// This class's purpose is to normalize the arguments into an array and to
|
|
7
|
+
// pass an optional trailing options object (e.g. `includeTriggerModifications`)
|
|
8
|
+
// on to knex.
|
|
7
9
|
//
|
|
8
10
|
// In knex, if a single column is given to `returning` it returns an array with the that column's value
|
|
9
11
|
// in it. If an array is given with a one item inside, the return value is an object.
|
|
10
12
|
class ReturningOperation extends ObjectionToKnexConvertingOperation {
|
|
13
|
+
constructor(name, opt) {
|
|
14
|
+
super(name, opt);
|
|
15
|
+
this.options = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
onAdd(builder, args) {
|
|
19
|
+
args = Array.from(args);
|
|
20
|
+
|
|
21
|
+
if (args.length > 1 && isPlainObject(args[args.length - 1])) {
|
|
22
|
+
this.options = args.pop();
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
args = flatten(args);
|
|
13
26
|
|
|
14
27
|
// Don't add an empty returning list.
|
|
@@ -21,7 +34,19 @@ class ReturningOperation extends ObjectionToKnexConvertingOperation {
|
|
|
21
34
|
|
|
22
35
|
onBuildKnex(knexBuilder, builder) {
|
|
23
36
|
// Always pass an array of columns to knex.returning.
|
|
24
|
-
|
|
37
|
+
const columns = this.getKnexArgs(builder);
|
|
38
|
+
|
|
39
|
+
if (this.options) {
|
|
40
|
+
return knexBuilder.returning(columns, this.options);
|
|
41
|
+
} else {
|
|
42
|
+
return knexBuilder.returning(columns);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
clone() {
|
|
47
|
+
const clone = super.clone();
|
|
48
|
+
clone.options = this.options;
|
|
49
|
+
return clone;
|
|
25
50
|
}
|
|
26
51
|
}
|
|
27
52
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { ref } = require('../../queryBuilder/ReferenceBuilder');
|
|
4
4
|
const { isEmpty } = require('../../utils/objectUtils');
|
|
5
|
+
const { toPostgresJsonPathLiteral } = require('../../utils/parseFieldExpression');
|
|
5
6
|
const { isKnexRaw, isKnexQueryBuilder } = require('../../utils/knexUtils');
|
|
6
7
|
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
7
8
|
const { StaticHookArguments } = require('../StaticHookArguments');
|
|
@@ -104,7 +105,7 @@ function convertFieldExpressionsToRaw(builder, model, json) {
|
|
|
104
105
|
// "col" : raw(`jsonb_set("col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
|
|
105
106
|
|
|
106
107
|
let parsed = ref(key);
|
|
107
|
-
let
|
|
108
|
+
let jsonPath = toPostgresJsonPathLiteral(parsed.parsedExpr.access);
|
|
108
109
|
let valuePlaceholder = '?';
|
|
109
110
|
|
|
110
111
|
if (isKnexQueryBuilder(val) || isKnexRaw(val)) {
|
|
@@ -114,7 +115,7 @@ function convertFieldExpressionsToRaw(builder, model, json) {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
convertedJson[parsed.column] = knex.raw(
|
|
117
|
-
`jsonb_set(??,
|
|
118
|
+
`jsonb_set(??, ${jsonPath}, ${valuePlaceholder}, true)`,
|
|
118
119
|
[convertedJson[parsed.column] || parsed.column, val],
|
|
119
120
|
);
|
|
120
121
|
|
|
@@ -40,6 +40,11 @@ class UpsertGraphOperation extends QueryBuilderOperation {
|
|
|
40
40
|
return true;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
onBefore1(builder, result) {
|
|
44
|
+
GraphUpsert.warnOnConflict(builder, this.name);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
onAfter1(builder) {
|
|
44
49
|
if (hasOtherSqlModifyingQueryBuilderCalls(builder)) {
|
|
45
50
|
throw new Error(
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
4
|
+
|
|
5
|
+
// Builds a set of where operations inside a single knex `where(callback)`
|
|
6
|
+
// group, i.e. wraps them in parentheses. See `wrapWhereGroups`.
|
|
7
|
+
class WhereGroupOperation extends QueryBuilderOperation {
|
|
8
|
+
constructor(name, opt) {
|
|
9
|
+
super(name, opt);
|
|
10
|
+
this.operations = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
onBuildKnex(knexBuilder, builder) {
|
|
14
|
+
const operations = this.operations;
|
|
15
|
+
|
|
16
|
+
return knexBuilder.where(function () {
|
|
17
|
+
let knex = this;
|
|
18
|
+
|
|
19
|
+
const build = (op) => {
|
|
20
|
+
if (op.hasOnBuildKnex()) {
|
|
21
|
+
knex = builder.callOperationMethod(op, 'onBuildKnex', [knex, builder]) || knex;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (const op of operations) {
|
|
26
|
+
build(op);
|
|
27
|
+
op.forEachDescendantOperation(build);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
clone() {
|
|
33
|
+
const clone = super.clone();
|
|
34
|
+
clone.operations = this.operations.map((op) => op.clone());
|
|
35
|
+
return clone;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
WhereGroupOperation,
|
|
41
|
+
};
|
|
@@ -9,6 +9,9 @@ class EagerOperation extends QueryBuilderOperation {
|
|
|
9
9
|
|
|
10
10
|
this.expression = RelationExpression.create();
|
|
11
11
|
this.graphOptions = this.opt.defaultGraphOptions;
|
|
12
|
+
// The `joinOperation` options passed to the `withGraphJoined` calls,
|
|
13
|
+
// together with the expressions of the calls.
|
|
14
|
+
this.joinOperations = [];
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
buildFinalExpression(builder) {
|
|
@@ -41,6 +44,7 @@ class EagerOperation extends QueryBuilderOperation {
|
|
|
41
44
|
cloneFrom(eagerOp) {
|
|
42
45
|
this.expression = eagerOp.expression.clone();
|
|
43
46
|
this.graphOptions = Object.assign({}, eagerOp.graphOptions);
|
|
47
|
+
this.joinOperations = eagerOp.joinOperations.slice();
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
clone() {
|
|
@@ -24,6 +24,7 @@ class JoinEagerOperation extends EagerOperation {
|
|
|
24
24
|
.setExpression(this.buildFinalExpression(builder))
|
|
25
25
|
.setModifiers(this.buildFinalModifiers(builder))
|
|
26
26
|
.setOptions(this.graphOptions)
|
|
27
|
+
.setJoinOperations(this.joinOperations)
|
|
27
28
|
.fetchColumnInfo(builder);
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -32,15 +33,10 @@ class JoinEagerOperation extends EagerOperation {
|
|
|
32
33
|
.setExpression(this.buildFinalExpression(builder))
|
|
33
34
|
.setModifiers(this.buildFinalModifiers(builder))
|
|
34
35
|
.setOptions(this.graphOptions)
|
|
36
|
+
.setJoinOperations(this.joinOperations)
|
|
35
37
|
.build(builder);
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
// Stops the joiner from omitting an internally selected column of the root
|
|
39
|
-
// table from the result. Returns true if such a selection existed.
|
|
40
|
-
releaseInternalRootSelection(column) {
|
|
41
|
-
return !!this.joiner && this.joiner.releaseInternalRootSelection(column);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
40
|
onRawResult(builder, rows) {
|
|
45
41
|
return this.joiner.parseResult(builder, rows);
|
|
46
42
|
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const promiseUtils = require('../../../utils/promiseUtils');
|
|
4
4
|
|
|
5
5
|
const { EagerOperation } = require('./EagerOperation');
|
|
6
|
-
const { JoinEagerOperation } = require('./JoinEagerOperation');
|
|
7
6
|
const { isMsSql, isOracle, isSqlite } = require('../../../utils/knexUtils');
|
|
8
7
|
const { isObject, asArray, flatten, chunk } = require('../../../utils/objectUtils');
|
|
9
8
|
const { ValidationErrorType } = require('../../../model/ValidationError');
|
|
@@ -16,7 +15,6 @@ class WhereInEagerOperation extends EagerOperation {
|
|
|
16
15
|
super(name, opt);
|
|
17
16
|
|
|
18
17
|
this.relationsToFetch = [];
|
|
19
|
-
this.omitProps = [];
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
batchSize(knex) {
|
|
@@ -51,8 +49,15 @@ class WhereInEagerOperation extends EagerOperation {
|
|
|
51
49
|
builder.select(selectionsToAdd);
|
|
52
50
|
}
|
|
53
51
|
|
|
52
|
+
// Omit the implicitly selected columns from the result once the relations
|
|
53
|
+
// have been fetched.
|
|
54
|
+
if (!builder.internalOptions().keepImplicitJoinProps) {
|
|
55
|
+
for (const prop of selectedProps) {
|
|
56
|
+
builder.addInternalSelection(prop);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
this.relationsToFetch = relationsToFetch;
|
|
55
|
-
this.omitProps = selectedProps;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
async onAfter2(builder, result) {
|
|
@@ -75,22 +80,6 @@ class WhereInEagerOperation extends EagerOperation {
|
|
|
75
80
|
{ concurrency: modelClass.getConcurrency(builder.unsafeKnex()) },
|
|
76
81
|
);
|
|
77
82
|
|
|
78
|
-
const intOpt = builder.internalOptions();
|
|
79
|
-
|
|
80
|
-
if (!this.omitProps.length || intOpt.keepImplicitJoinProps) {
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Now that relations have been fetched for `models` we can omit the
|
|
85
|
-
// columns that were implicitly selected by this class.
|
|
86
|
-
for (let i = 0, l = result.length; i < l; ++i) {
|
|
87
|
-
const model = result[i];
|
|
88
|
-
|
|
89
|
-
for (let c = 0, lc = this.omitProps.length; c < lc; ++c) {
|
|
90
|
-
modelClass.omitImpl(model, this.omitProps[c]);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
83
|
return result;
|
|
95
84
|
}
|
|
96
85
|
|
|
@@ -205,7 +194,6 @@ class WhereInEagerOperation extends EagerOperation {
|
|
|
205
194
|
const clone = super.clone();
|
|
206
195
|
|
|
207
196
|
clone.relationsToFetch = this.relationsToFetch.slice();
|
|
208
|
-
clone.omitProps = this.omitProps.slice();
|
|
209
197
|
|
|
210
198
|
return clone;
|
|
211
199
|
}
|
|
@@ -238,11 +226,6 @@ function findRelationPropsToSelect(builder, relationsToFetch) {
|
|
|
238
226
|
const selectionsToAdd = [];
|
|
239
227
|
const selectedProps = [];
|
|
240
228
|
|
|
241
|
-
// When mixed with `withGraphJoined`, the join operation may have selected
|
|
242
|
-
// some of the needed columns internally, in which case it would omit them
|
|
243
|
-
// from the result models before we get to use them.
|
|
244
|
-
const joinOp = builder.findOperation(JoinEagerOperation);
|
|
245
|
-
|
|
246
229
|
// Collect columns that need to be selected for the eager fetch
|
|
247
230
|
// to work that are not currently selected.
|
|
248
231
|
for (const { relation } of relationsToFetch) {
|
|
@@ -253,15 +236,9 @@ function findRelationPropsToSelect(builder, relationsToFetch) {
|
|
|
253
236
|
const prop = ownerProp.props[c];
|
|
254
237
|
const col = ownerProp.cols[c];
|
|
255
238
|
|
|
256
|
-
if (!builder.hasSelectionAs(fullCol, col)) {
|
|
257
|
-
if (selectionsToAdd.indexOf(fullCol) === -1) {
|
|
258
|
-
selectedProps.push(prop);
|
|
259
|
-
selectionsToAdd.push(fullCol);
|
|
260
|
-
}
|
|
261
|
-
} else if (joinOp && joinOp.releaseInternalRootSelection(col)) {
|
|
262
|
-
// The join operation no longer omits the column, so we need to
|
|
263
|
-
// omit the property once the relations have been fetched.
|
|
239
|
+
if (!builder.hasSelectionAs(fullCol, col) && selectionsToAdd.indexOf(fullCol) === -1) {
|
|
264
240
|
selectedProps.push(prop);
|
|
241
|
+
selectionsToAdd.push(fullCol);
|
|
265
242
|
}
|
|
266
243
|
}
|
|
267
244
|
}
|
|
@@ -7,7 +7,7 @@ class WhereJsonHasPostgresOperation extends ObjectionToKnexConvertingOperation {
|
|
|
7
7
|
onBuildKnex(knexBuilder, builder) {
|
|
8
8
|
const args = this.getKnexArgs(builder);
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const rawArgs = jsonApi.whereJsonFieldRightStringArrayOnLeftQuery(
|
|
11
11
|
builder.knex(),
|
|
12
12
|
args[0],
|
|
13
13
|
this.opt.operator,
|
|
@@ -15,9 +15,9 @@ class WhereJsonHasPostgresOperation extends ObjectionToKnexConvertingOperation {
|
|
|
15
15
|
);
|
|
16
16
|
|
|
17
17
|
if (this.opt.bool === 'or') {
|
|
18
|
-
knexBuilder = knexBuilder.orWhereRaw(
|
|
18
|
+
knexBuilder = knexBuilder.orWhereRaw(...rawArgs);
|
|
19
19
|
} else {
|
|
20
|
-
knexBuilder = knexBuilder.whereRaw(
|
|
20
|
+
knexBuilder = knexBuilder.whereRaw(...rawArgs);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
return knexBuilder;
|
|
@@ -18,7 +18,7 @@ class WhereJsonNotObjectPostgresOperation extends ObjectionToKnexConvertingOpera
|
|
|
18
18
|
'not',
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
-
builder.orWhereRaw(jsonApi.whereJsonFieldQuery(knex, fieldExpression, 'IS', null));
|
|
21
|
+
builder.orWhereRaw(...jsonApi.whereJsonFieldQuery(knex, fieldExpression, 'IS', null));
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
if (this.opt.bool === 'or') {
|
|
@@ -2,6 +2,7 @@
|
|
|
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
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @typedef {String} FieldExpression
|
|
@@ -38,22 +39,24 @@ const { asArray, isObject, isString } = require('../../../utils/objectUtils');
|
|
|
38
39
|
* 4. Keys containing square brackets and quotes
|
|
39
40
|
* `objectColumn:['Double."Quote".[]']` and `objectColumn:["Sinlge.'Quote'.[]"]`
|
|
40
41
|
* Column `{ "Double.\"Quote\".[]" : "I was referred", "Single.'Quote'.[]" : "Mee too!" }`
|
|
42
|
+
* 5. Empty keys `objectColumn:[""]` or `objectColumn:['']` Column `{ "" : "I was referred" }`
|
|
41
43
|
* 99. Keys containing dots, square brackets, single quotes and double quotes in one json key is
|
|
42
44
|
* not currently supported
|
|
43
45
|
*/
|
|
44
46
|
|
|
47
|
+
// Returns the SQL and bindings of a field expression. The column is passed as an
|
|
48
|
+
// identifier binding (`??`), so that knex's `wrapIdentifier` (e.g. used by
|
|
49
|
+
// `knexSnakeCaseMappers()`) is applied to it.
|
|
45
50
|
function parseFieldExpression(expression, extractAsText) {
|
|
46
51
|
let parsed = parser.parseFieldExpression(expression);
|
|
47
|
-
let
|
|
48
|
-
let column = `"${middleQuotedColumnName}"`;
|
|
52
|
+
let bindings = [parsed.columnName];
|
|
49
53
|
// Reference the bare column when there is no json path, so that Postgres can
|
|
50
54
|
// use indexes on it. `#>> '{}'` is still needed to extract the value as text.
|
|
51
55
|
if (parsed.access.length === 0 && !extractAsText) {
|
|
52
|
-
return
|
|
56
|
+
return ['??', bindings];
|
|
53
57
|
}
|
|
54
|
-
let jsonRefs = parsed.access.map((it) => it.ref).join(',');
|
|
55
58
|
let extractor = extractAsText ? '#>>' : '#>';
|
|
56
|
-
return
|
|
59
|
+
return [`??${extractor}${parser.toPostgresJsonPathLiteral(parsed.access)}`, bindings];
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
function whereJsonbRefOnLeftJsonbValOrRefOnRight(
|
|
@@ -78,36 +81,36 @@ function whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams(
|
|
|
78
81
|
jsonObjectOrFieldExpression,
|
|
79
82
|
queryPrefix,
|
|
80
83
|
) {
|
|
81
|
-
let fieldReference = parseFieldExpression(fieldExpression);
|
|
84
|
+
let [fieldReference, bindings] = parseFieldExpression(fieldExpression);
|
|
85
|
+
let [rightHandSql, rightHandBindings] = rightHandExpression(jsonObjectOrFieldExpression);
|
|
86
|
+
let query = ['(', fieldReference, ')::jsonb', operator, rightHandSql];
|
|
82
87
|
|
|
88
|
+
if (queryPrefix) {
|
|
89
|
+
query.unshift(queryPrefix);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [query.join(' '), [...bindings, ...rightHandBindings]];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function rightHandExpression(jsonObjectOrFieldExpression) {
|
|
83
96
|
if (isString(jsonObjectOrFieldExpression)) {
|
|
84
|
-
let rightHandReference = parseFieldExpression(jsonObjectOrFieldExpression);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
')::jsonb',
|
|
93
|
-
];
|
|
94
|
-
if (queryPrefix) {
|
|
95
|
-
refRefQuery.unshift(queryPrefix);
|
|
96
|
-
}
|
|
97
|
-
return [refRefQuery.join(' ')];
|
|
97
|
+
let [rightHandReference, bindings] = parseFieldExpression(jsonObjectOrFieldExpression);
|
|
98
|
+
return [`( ${rightHandReference} )::jsonb`, bindings];
|
|
99
|
+
} else if (
|
|
100
|
+
isKnexRaw(jsonObjectOrFieldExpression) ||
|
|
101
|
+
isKnexQueryBuilder(jsonObjectOrFieldExpression)
|
|
102
|
+
) {
|
|
103
|
+
// ref(), val(), raw() and subqueries, already converted to knex by the operation.
|
|
104
|
+
return ['( ? )::jsonb', [jsonObjectOrFieldExpression]];
|
|
98
105
|
} else if (isObject(jsonObjectOrFieldExpression)) {
|
|
99
|
-
|
|
100
|
-
if (queryPrefix) {
|
|
101
|
-
refValQuery.unshift(queryPrefix);
|
|
102
|
-
}
|
|
103
|
-
return [refValQuery.join(' '), JSON.stringify(jsonObjectOrFieldExpression)];
|
|
106
|
+
return ['?::jsonb', [JSON.stringify(jsonObjectOrFieldExpression)]];
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
throw new Error('Invalid right hand expression.');
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
function whereJsonFieldRightStringArrayOnLeftQuery(knex, fieldExpression, operator, keys) {
|
|
110
|
-
let fieldReference = parseFieldExpression(fieldExpression);
|
|
113
|
+
let [fieldReference, bindings] = parseFieldExpression(fieldExpression);
|
|
111
114
|
keys = asArray(keys);
|
|
112
115
|
|
|
113
116
|
let questionMarksArray = keys.map((key) => {
|
|
@@ -117,19 +120,19 @@ function whereJsonFieldRightStringArrayOnLeftQuery(knex, fieldExpression, operat
|
|
|
117
120
|
return '?';
|
|
118
121
|
});
|
|
119
122
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
return [
|
|
124
|
+
`${fieldReference} ${operator.replace('?', '\\?')} array[${questionMarksArray.join(',')}]`,
|
|
125
|
+
[...bindings, ...keys],
|
|
126
|
+
];
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
function whereJsonFieldQuery(knex, fieldExpression, operator, value) {
|
|
127
|
-
let fieldReference = parseFieldExpression(fieldExpression, true);
|
|
130
|
+
let [fieldReference, bindings] = parseFieldExpression(fieldExpression, true);
|
|
128
131
|
let normalizedOperator = normalizeOperator(knex, operator);
|
|
129
132
|
|
|
130
133
|
// json type comparison takes json type in string format
|
|
131
134
|
let cast;
|
|
132
|
-
let
|
|
135
|
+
let valuePlaceholder = '?';
|
|
133
136
|
let type = typeof value;
|
|
134
137
|
|
|
135
138
|
if (type === 'number') {
|
|
@@ -140,12 +143,16 @@ function whereJsonFieldQuery(knex, fieldExpression, operator, value) {
|
|
|
140
143
|
cast = '::TEXT';
|
|
141
144
|
} else if (value === null) {
|
|
142
145
|
cast = '::TEXT';
|
|
143
|
-
|
|
146
|
+
valuePlaceholder = 'NULL';
|
|
144
147
|
} else {
|
|
145
148
|
throw new Error('Value must be string, number, boolean or null.');
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
|
|
151
|
+
// Bind the value so that `?` in it isn't read as a binding placeholder.
|
|
152
|
+
return [
|
|
153
|
+
`(${fieldReference})${cast} ${normalizedOperator} ${valuePlaceholder}`,
|
|
154
|
+
value === null ? bindings : [...bindings, value],
|
|
155
|
+
];
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
function normalizeOperator(knex, operator) {
|