objection 3.2.0 → 3.3.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 +133 -3
- package/lib/queryBuilder/QueryBuilder.js +186 -5
- 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/GraphUpsert.js +22 -0
- 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/join/JoinResultParser.js +22 -0
- package/lib/queryBuilder/join/RelationJoiner.js +71 -22
- package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
- package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
- package/lib/queryBuilder/operations/InsertOperation.js +121 -12
- 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/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 +70 -48
- 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 +8 -2
- package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
- package/lib/utils/deprecate.js +6 -0
- package/lib/utils/identifierMapping.js +15 -1
- package/lib/utils/parseFieldExpression.js +28 -0
- package/package.json +10 -6
- package/typings/objection/index.d.ts +280 -28
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://github.com/ditojs/objection/actions)
|
|
4
4
|
[](https://gitter.im/Vincit/objection.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
|
5
5
|
|
|
6
|
-
# [Objection.js](https://
|
|
6
|
+
# [Objection.js](https://ditojs.github.io/objection/)
|
|
7
7
|
|
|
8
8
|
Objection.js is an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) for [Node.js](https://nodejs.org/) that aims to stay out of your way and make it as easy as possible to use the full power of SQL and the underlying database engine while still making the common stuff easy and enjoyable.
|
|
9
9
|
|
|
@@ -13,13 +13,13 @@ Objection.js is built on an SQL query builder called [knex](http://knexjs.org).
|
|
|
13
13
|
|
|
14
14
|
What objection.js gives you:
|
|
15
15
|
|
|
16
|
-
- **An easy declarative way of [defining models](https://
|
|
17
|
-
- **Simple and fun way to [fetch, insert, update and delete](https://
|
|
18
|
-
- **Powerful mechanisms for [eager loading](https://
|
|
19
|
-
- **Easy to use [transactions](https://
|
|
16
|
+
- **An easy declarative way of [defining models](https://ditojs.github.io/objection/guide/models.html) and relationships between them**
|
|
17
|
+
- **Simple and fun way to [fetch, insert, update and delete](https://ditojs.github.io/objection/guide/query-examples.html) objects using the full power of SQL**
|
|
18
|
+
- **Powerful mechanisms for [eager loading](https://ditojs.github.io/objection/guide/query-examples.html#eager-loading), [inserting](https://ditojs.github.io/objection/guide/query-examples.html#graph-inserts) and [upserting](https://ditojs.github.io/objection/guide/query-examples.html#graph-upserts) object graphs**
|
|
19
|
+
- **Easy to use [transactions](https://ditojs.github.io/objection/guide/transactions.html)**
|
|
20
20
|
- **Official [TypeScript](https://github.com/ditojs/objection/blob/main/typings/objection/index.d.ts) support**
|
|
21
|
-
- **Optional [JSON schema](https://
|
|
22
|
-
- **A way to [store complex documents](https://
|
|
21
|
+
- **Optional [JSON schema](https://ditojs.github.io/objection/guide/validation.html) validation**
|
|
22
|
+
- **A way to [store complex documents](https://ditojs.github.io/objection/guide/documents.html) as single rows**
|
|
23
23
|
|
|
24
24
|
What objection.js **doesn't** give you:
|
|
25
25
|
|
|
@@ -31,7 +31,7 @@ What objection.js **doesn't** give you:
|
|
|
31
31
|
- **A custom query DSL. SQL is used as a query language.**
|
|
32
32
|
This doesn't mean you have to write SQL strings though. A query builder based on [knex](http://knexjs.org) is
|
|
33
33
|
used to build the SQL. However, if the query builder fails you for some reason, raw SQL strings can be easily
|
|
34
|
-
written using the [raw](https://
|
|
34
|
+
written using the [raw](https://ditojs.github.io/objection/api/objection/#raw) helper function.
|
|
35
35
|
- **Automatic database schema creation and migration from model definitions.**
|
|
36
36
|
For simple things it is useful that the database schema is automatically generated from the model definitions,
|
|
37
37
|
but usually just gets in your way when doing anything non-trivial. Objection.js leaves the schema related things
|
|
@@ -45,9 +45,9 @@ Check out [this issue](https://github.com/Vincit/objection.js/issues/1069) to se
|
|
|
45
45
|
Shortcuts:
|
|
46
46
|
|
|
47
47
|
- [Who uses objection.js](https://github.com/Vincit/objection.js/discussions/2464)
|
|
48
|
-
- [API reference](https://
|
|
48
|
+
- [API reference](https://ditojs.github.io/objection/api/query-builder/)
|
|
49
49
|
- [Example projects](https://github.com/Vincit/objection.js/tree/main/examples)
|
|
50
|
-
- [Changelog](https://
|
|
51
|
-
- [v1 -> v2 -> v3 migration guide](https://
|
|
52
|
-
- [Contribution guide](https://
|
|
53
|
-
- [Plugins](https://
|
|
50
|
+
- [Changelog](https://ditojs.github.io/objection/release-notes/changelog.html)
|
|
51
|
+
- [v1 -> v2 -> v3 migration guide](https://ditojs.github.io/objection/release-notes/migration.html)
|
|
52
|
+
- [Contribution guide](https://ditojs.github.io/objection/guide/contributing.html)
|
|
53
|
+
- [Plugins](https://ditojs.github.io/objection/guide/plugins.html)
|
|
@@ -5,6 +5,7 @@ const addFormats = require('ajv-formats');
|
|
|
5
5
|
const { Validator } = require('./Validator');
|
|
6
6
|
const { ValidationErrorType } = require('../model/ValidationError');
|
|
7
7
|
const { isObject, once, cloneDeep: lodashCloneDeep, omit } = require('../utils/objectUtils');
|
|
8
|
+
const { parseFieldExpression } = require('../utils/parseFieldExpression');
|
|
8
9
|
|
|
9
10
|
class AjvValidator extends Validator {
|
|
10
11
|
static init(self, conf) {
|
|
@@ -81,13 +82,24 @@ class AjvValidator extends Validator {
|
|
|
81
82
|
json = cloneDeep(json);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
// Patch objects can contain field expression keys like `'meta:b'` that update
|
|
86
|
+
// a single property inside a json column. Validate them against the nested
|
|
87
|
+
// schema by converting them into nested objects like `{ meta: { b: value } }`.
|
|
88
|
+
const fieldExpressions = options.patch ? nestFieldExpressions(json) : null;
|
|
89
|
+
const validationJson = fieldExpressions ? fieldExpressions.json : json;
|
|
90
|
+
|
|
91
|
+
validator.call(model, validationJson);
|
|
85
92
|
const error = parseValidationError(validator.errors, modelClass, options, this.ajvOptions);
|
|
86
93
|
|
|
87
94
|
if (error) {
|
|
88
95
|
throw error;
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
if (fieldExpressions) {
|
|
99
|
+
// Copy back the changes made by Ajv options like `coerceTypes` and `removeAdditional`.
|
|
100
|
+
unnestFieldExpressions(json, fieldExpressions);
|
|
101
|
+
}
|
|
102
|
+
|
|
91
103
|
return json;
|
|
92
104
|
}
|
|
93
105
|
|
|
@@ -223,6 +235,116 @@ function parseValidationError(errors, modelClass, options, ajvOptions) {
|
|
|
223
235
|
});
|
|
224
236
|
}
|
|
225
237
|
|
|
238
|
+
function nestFieldExpressions(json) {
|
|
239
|
+
const keys = Object.keys(json);
|
|
240
|
+
|
|
241
|
+
if (!keys.some(isFieldExpression)) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const nested = Object.create(null);
|
|
246
|
+
const paths = new Map();
|
|
247
|
+
// The objects created here for the nested paths. Other objects are values
|
|
248
|
+
// of the input json and must not be modified.
|
|
249
|
+
const createdObjects = new Set();
|
|
250
|
+
|
|
251
|
+
for (const key of keys) {
|
|
252
|
+
if (!isFieldExpression(key)) {
|
|
253
|
+
nested[key] = json[key];
|
|
254
|
+
paths.set(key, [key]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const key of keys) {
|
|
259
|
+
if (isFieldExpression(key)) {
|
|
260
|
+
const path = fieldExpressionPath(key);
|
|
261
|
+
|
|
262
|
+
// Field expressions with array access, and those that conflict with other
|
|
263
|
+
// keys of the patch, can't be validated without the current value of the
|
|
264
|
+
// json column. Leave them out of the validation.
|
|
265
|
+
if (path && setPath(nested, path, json[key], createdObjects)) {
|
|
266
|
+
paths.set(key, path);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { json: nested, paths };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function unnestFieldExpressions(json, { json: nested, paths }) {
|
|
275
|
+
for (const [key, path] of paths) {
|
|
276
|
+
let value = nested;
|
|
277
|
+
let found = true;
|
|
278
|
+
|
|
279
|
+
for (const prop of path) {
|
|
280
|
+
found = isObject(value) && hasOwn(value, prop);
|
|
281
|
+
|
|
282
|
+
if (!found) {
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
value = value[prop];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!found) {
|
|
290
|
+
// Removed by `removeAdditional`.
|
|
291
|
+
delete json[key];
|
|
292
|
+
} else if (json[key] !== value) {
|
|
293
|
+
json[key] = value;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function isFieldExpression(key) {
|
|
299
|
+
return key.indexOf(':') !== -1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function fieldExpressionPath(key) {
|
|
303
|
+
let parsed;
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
parsed = parseFieldExpression(key);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
// Not a valid field expression, validate it as a top-level property.
|
|
309
|
+
return [key];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (parsed.access.some((access) => access.type !== 'object')) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return [parsed.column, ...parsed.access.map((access) => String(access.ref))];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function setPath(obj, path, value, createdObjects) {
|
|
320
|
+
const last = path.length - 1;
|
|
321
|
+
|
|
322
|
+
for (let i = 0; i < last; ++i) {
|
|
323
|
+
const prop = path[i];
|
|
324
|
+
|
|
325
|
+
if (!hasOwn(obj, prop)) {
|
|
326
|
+
const child = Object.create(null);
|
|
327
|
+
createdObjects.add(child);
|
|
328
|
+
obj[prop] = child;
|
|
329
|
+
} else if (!createdObjects.has(obj[prop])) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
obj = obj[prop];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (hasOwn(obj, path[last])) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
obj[path[last]] = value;
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function hasOwn(obj, prop) {
|
|
345
|
+
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
346
|
+
}
|
|
347
|
+
|
|
226
348
|
function cloneDeep(obj) {
|
|
227
349
|
if (isObject(obj) && obj.$isObjectionModel) {
|
|
228
350
|
return obj.$clone();
|
|
@@ -276,7 +398,12 @@ function objectHasDefaults(obj) {
|
|
|
276
398
|
}
|
|
277
399
|
|
|
278
400
|
function jsonSchemaWithoutRequired(jsonSchema) {
|
|
279
|
-
|
|
401
|
+
// `not` is intentionally left untouched: Stripping `required` inside it
|
|
402
|
+
// would turn it into a schema that rejects everything (e.g.
|
|
403
|
+
// `not: { required: ['a'] }` -> `not: {}`). Kept as is, it still holds for
|
|
404
|
+
// patches: Any property present in the patch is also present in the
|
|
405
|
+
// resulting object.
|
|
406
|
+
const subSchemaProps = ['anyOf', 'oneOf', 'allOf', 'then', 'else', 'properties'];
|
|
280
407
|
// `definitions` was renamed to `$defs` in draft 2019-09. Support both.
|
|
281
408
|
const definitionProps = ['definitions', '$defs'];
|
|
282
409
|
return Object.assign(
|
|
@@ -318,7 +445,10 @@ function subSchemaWithoutRequired(jsonSchema, prop) {
|
|
|
318
445
|
} else {
|
|
319
446
|
return {};
|
|
320
447
|
}
|
|
321
|
-
} else if (
|
|
448
|
+
} else if (prop === 'properties') {
|
|
449
|
+
// `properties` is a map of property names to schemas, regardless of the
|
|
450
|
+
// schema's `type` (which may also be e.g. `['object', 'null']`, or be
|
|
451
|
+
// missing entirely).
|
|
322
452
|
return {
|
|
323
453
|
[prop]: Object.fromEntries(
|
|
324
454
|
Object.entries(jsonSchema[prop]).map(([key, schema]) => [
|
|
@@ -46,6 +46,7 @@ const { FirstOperation } = require('./operations/FirstOperation');
|
|
|
46
46
|
const { NoneOperation } = require('./operations/NoneOperation');
|
|
47
47
|
const { FromOperation } = require('./operations/FromOperation');
|
|
48
48
|
const { KnexOperation } = require('./operations/KnexOperation');
|
|
49
|
+
const { wrapWhereGroups } = require('./whereGroups');
|
|
49
50
|
|
|
50
51
|
class QueryBuilder extends QueryBuilderBase {
|
|
51
52
|
static init(self, modelClass) {
|
|
@@ -58,8 +59,12 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
58
59
|
|
|
59
60
|
self._allowedGraphExpression = null;
|
|
60
61
|
self._graphModifiersAtPath = [];
|
|
62
|
+
// The eager algorithm that was most recently used explicitly, see `withGraph()`.
|
|
63
|
+
self._graphAlgorithm = null;
|
|
64
|
+
self._isJoinChildQuery = false;
|
|
61
65
|
self._findOperationOptions = modelClass.defaultFindOptions;
|
|
62
66
|
self._relatedQueryFor = null;
|
|
67
|
+
self._internalSelections = new Set();
|
|
63
68
|
|
|
64
69
|
self._findOperationFactory = findOperationFactory;
|
|
65
70
|
self._insertOperationFactory = insertOperationFactory;
|
|
@@ -78,6 +83,27 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
78
83
|
return RelationExpression.create(expr).toPojo();
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
childQueryOf(query, options = {}) {
|
|
87
|
+
super.childQueryOf(query, options);
|
|
88
|
+
|
|
89
|
+
if (query) {
|
|
90
|
+
this._isJoinChildQuery = !!options.isJoinChildQuery;
|
|
91
|
+
|
|
92
|
+
if (query.isObjectionQueryBuilder) {
|
|
93
|
+
this._graphAlgorithm = query._graphAlgorithm;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Returns true if this query was created for a relation that is loaded using
|
|
101
|
+
// `withGraphJoined()`. Such queries are only used to build the join and are
|
|
102
|
+
// never executed themselves.
|
|
103
|
+
isJoinChildQuery() {
|
|
104
|
+
return this._isJoinChildQuery;
|
|
105
|
+
}
|
|
106
|
+
|
|
81
107
|
tableNameFor(modelClassOrTableName, newTableName) {
|
|
82
108
|
return super.tableNameFor(getTableName(modelClassOrTableName), newTableName);
|
|
83
109
|
}
|
|
@@ -191,19 +217,50 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
191
217
|
}
|
|
192
218
|
|
|
193
219
|
withGraphFetched(exp, options = {}) {
|
|
194
|
-
return this.
|
|
220
|
+
return this._withGraphUsing(exp, options, getWhereInEagerAlgorithm(this));
|
|
195
221
|
}
|
|
196
222
|
|
|
197
223
|
withGraphJoined(exp, options = {}) {
|
|
198
|
-
return this.
|
|
224
|
+
return this._withGraphUsing(exp, options, getJoinEagerAlgorithm(this));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Like `withGraphFetched()` or `withGraphJoined()` with `options.algorithm`
|
|
228
|
+
// set to 'fetch' or 'join'. Without it, the top-level relations that are
|
|
229
|
+
// already loaded by one of the algorithms are merged into it, and new ones
|
|
230
|
+
// use the most recently used algorithm.
|
|
231
|
+
withGraph(exp, { algorithm, ...options } = {}) {
|
|
232
|
+
if (algorithm !== undefined) {
|
|
233
|
+
return this._withGraphUsing(exp, options, getEagerAlgorithmByName(this, algorithm));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const parsedExp = parseRelationExpression(this.modelClass(), exp);
|
|
237
|
+
|
|
238
|
+
for (const [algorithm, groupExp] of groupByEagerAlgorithm(this, parsedExp)) {
|
|
239
|
+
this._withGraph(groupExp, options, algorithm);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_withGraphUsing(exp, options, algorithm) {
|
|
246
|
+
this._graphAlgorithm = algorithm;
|
|
247
|
+
return this._withGraph(exp, options, algorithm);
|
|
199
248
|
}
|
|
200
249
|
|
|
201
250
|
_withGraph(exp, options, algorithm) {
|
|
202
251
|
const eagerOp = ensureEagerOperation(this, algorithm);
|
|
203
252
|
const parsedExp = parseRelationExpression(this.modelClass(), exp);
|
|
204
253
|
|
|
254
|
+
// The `joinOperation` option only applies to the relations of this call,
|
|
255
|
+
// see `RelationJoiner`. All other options are shared by all calls.
|
|
256
|
+
const { joinOperation, ...graphOptions } = options;
|
|
257
|
+
|
|
205
258
|
eagerOp.expression = eagerOp.expression.merge(parsedExp);
|
|
206
|
-
eagerOp.graphOptions = { ...eagerOp.graphOptions, ...
|
|
259
|
+
eagerOp.graphOptions = { ...eagerOp.graphOptions, ...graphOptions };
|
|
260
|
+
|
|
261
|
+
if (joinOperation) {
|
|
262
|
+
eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
|
|
263
|
+
}
|
|
207
264
|
|
|
208
265
|
checkMixedEager(this);
|
|
209
266
|
checkEager(this);
|
|
@@ -355,6 +412,8 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
355
412
|
|
|
356
413
|
builder._allowedGraphExpression = this._allowedGraphExpression;
|
|
357
414
|
builder._graphModifiersAtPath = this._graphModifiersAtPath.slice();
|
|
415
|
+
builder._graphAlgorithm = this._graphAlgorithm;
|
|
416
|
+
builder._isJoinChildQuery = this._isJoinChildQuery;
|
|
358
417
|
builder._findOperationOptions = this._findOperationOptions;
|
|
359
418
|
builder._relatedQueryFor = this._relatedQueryFor;
|
|
360
419
|
|
|
@@ -376,8 +435,17 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
376
435
|
return builder;
|
|
377
436
|
}
|
|
378
437
|
|
|
438
|
+
// Registers a property of the result models that was selected internally,
|
|
439
|
+
// e.g. by an eager operation, without the user asking for it. Such properties
|
|
440
|
+
// are omitted from the result models before `runAfter` hooks are called.
|
|
441
|
+
addInternalSelection(prop) {
|
|
442
|
+
this._internalSelections.add(prop);
|
|
443
|
+
return this;
|
|
444
|
+
}
|
|
445
|
+
|
|
379
446
|
clearWithGraph() {
|
|
380
447
|
this.clear(EagerOperation);
|
|
448
|
+
this._graphAlgorithm = null;
|
|
381
449
|
return this;
|
|
382
450
|
}
|
|
383
451
|
|
|
@@ -424,11 +492,27 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
424
492
|
async resultSize() {
|
|
425
493
|
const knex = this.knex();
|
|
426
494
|
const builder = this.clone().clear(/orderBy|offset|limit/);
|
|
495
|
+
const joinEagerOp = builder.findOperation(JoinEagerOperation);
|
|
496
|
+
|
|
497
|
+
if (joinEagerOp) {
|
|
498
|
+
// `withGraphJoined` needs the table metadata to build its selects.
|
|
499
|
+
await joinEagerOp.onBefore3(builder);
|
|
500
|
+
}
|
|
427
501
|
|
|
428
502
|
const countQuery = knex.count('* as count').from((knexBuilder) => {
|
|
429
503
|
// Also clear the order on the built knex query, since `orderBy` calls
|
|
430
504
|
// can be added at build time, e.g. by a relation's `modify`.
|
|
431
|
-
builder.toKnexQuery(knexBuilder).clearOrder()
|
|
505
|
+
knexBuilder = builder.toKnexQuery(knexBuilder).clearOrder();
|
|
506
|
+
|
|
507
|
+
if (joinEagerOp) {
|
|
508
|
+
// The joins of `withGraphJoined` return one row per joined child.
|
|
509
|
+
// Count the distinct root models instead.
|
|
510
|
+
const tableRef = builder.tableRef();
|
|
511
|
+
const idColumns = builder.modelClass().getIdColumnArray();
|
|
512
|
+
knexBuilder.clearSelect().distinct(idColumns.map((col) => `${tableRef}.${col}`));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
knexBuilder.as('temp');
|
|
432
516
|
});
|
|
433
517
|
|
|
434
518
|
if (this.internalOptions().debug) {
|
|
@@ -891,6 +975,26 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
891
975
|
return this;
|
|
892
976
|
}
|
|
893
977
|
|
|
978
|
+
updateById(id, modelOrObject) {
|
|
979
|
+
return this.findById(id)
|
|
980
|
+
.update(modelOrObject)
|
|
981
|
+
.runBefore((_, builder) => {
|
|
982
|
+
if (!builder.internalOptions().skipUndefined) {
|
|
983
|
+
assertIdNotUndefined(id, `undefined was passed to updateById`);
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
patchById(id, modelOrObject) {
|
|
989
|
+
return this.findById(id)
|
|
990
|
+
.patch(modelOrObject)
|
|
991
|
+
.runBefore((_, builder) => {
|
|
992
|
+
if (!builder.internalOptions().skipUndefined) {
|
|
993
|
+
assertIdNotUndefined(id, `undefined was passed to patchById`);
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
894
998
|
deleteById(id) {
|
|
895
999
|
return this.findById(id)
|
|
896
1000
|
.delete()
|
|
@@ -1055,6 +1159,66 @@ function getGraphExpression(builder) {
|
|
|
1055
1159
|
);
|
|
1056
1160
|
}
|
|
1057
1161
|
|
|
1162
|
+
function getEagerAlgorithmByName(builder, name) {
|
|
1163
|
+
if (name === 'fetch') {
|
|
1164
|
+
return getWhereInEagerAlgorithm(builder);
|
|
1165
|
+
} else if (name === 'join') {
|
|
1166
|
+
return getJoinEagerAlgorithm(builder);
|
|
1167
|
+
} else {
|
|
1168
|
+
throw new Error(`unknown graph algorithm "${name}", expected "fetch" or "join"`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function getEagerAlgorithmOfOperation(builder, eagerOp) {
|
|
1173
|
+
if (eagerOp instanceof JoinEagerOperation) {
|
|
1174
|
+
return getJoinEagerAlgorithm(builder);
|
|
1175
|
+
} else if (eagerOp instanceof NaiveEagerOperation) {
|
|
1176
|
+
return getNaiveEagerAlgorithm(builder);
|
|
1177
|
+
} else {
|
|
1178
|
+
return getWhereInEagerAlgorithm(builder);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Splits `expression` by its top-level relations into one expression per eager
|
|
1183
|
+
// algorithm. Relations that are already loaded by one of the eager operations
|
|
1184
|
+
// keep its algorithm, new ones use the most recently used one.
|
|
1185
|
+
function groupByEagerAlgorithm(builder, expression) {
|
|
1186
|
+
const defaultAlgorithm = builder._graphAlgorithm || getWhereInEagerAlgorithm(builder);
|
|
1187
|
+
const { node } = expression;
|
|
1188
|
+
const rootProps = {};
|
|
1189
|
+
const groups = new Map();
|
|
1190
|
+
|
|
1191
|
+
for (const key of Object.keys(node)) {
|
|
1192
|
+
if (key.startsWith('$')) {
|
|
1193
|
+
rootProps[key] = node[key];
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
for (const childName of node.$childNames) {
|
|
1198
|
+
const eagerOp = findEagerOperations(builder).find(
|
|
1199
|
+
(op) => op.expression.node.$allRecursive || op.expression.node[childName],
|
|
1200
|
+
);
|
|
1201
|
+
|
|
1202
|
+
const algorithm = eagerOp ? getEagerAlgorithmOfOperation(builder, eagerOp) : defaultAlgorithm;
|
|
1203
|
+
let group = groups.get(algorithm);
|
|
1204
|
+
|
|
1205
|
+
if (!group) {
|
|
1206
|
+
group = { ...rootProps, $childNames: [] };
|
|
1207
|
+
groups.set(algorithm, group);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
group.$childNames.push(childName);
|
|
1211
|
+
group[childName] = node[childName];
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (groups.size === 0) {
|
|
1215
|
+
// No top-level relations, e.g. `*` or an empty expression.
|
|
1216
|
+
return [[defaultAlgorithm, expression]];
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
return [...groups].map(([algorithm, group]) => [algorithm, RelationExpression.create(group)]);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1058
1222
|
function getWhereInEagerAlgorithm(builder) {
|
|
1059
1223
|
return builder.modelClass().WhereInEagerAlgorithm;
|
|
1060
1224
|
}
|
|
@@ -1200,6 +1364,7 @@ function afterExecute(builder, result) {
|
|
|
1200
1364
|
|
|
1201
1365
|
promise = chainOperationHooks(promise, builder, 'onAfter1');
|
|
1202
1366
|
promise = chainOperationHooks(promise, builder, 'onAfter2');
|
|
1367
|
+
promise = promise.then((result) => omitInternalSelections(builder, result));
|
|
1203
1368
|
|
|
1204
1369
|
promise = chainHooks(promise, builder, builder.context().runAfter);
|
|
1205
1370
|
promise = chainHooks(promise, builder, builder.internalContext().runAfter);
|
|
@@ -1209,6 +1374,22 @@ function afterExecute(builder, result) {
|
|
|
1209
1374
|
return promise;
|
|
1210
1375
|
}
|
|
1211
1376
|
|
|
1377
|
+
function omitInternalSelections(builder, result) {
|
|
1378
|
+
const props = builder._internalSelections;
|
|
1379
|
+
|
|
1380
|
+
if (props.size && Array.isArray(result) && isObject(result[0])) {
|
|
1381
|
+
const modelClass = builder.resultModelClass();
|
|
1382
|
+
|
|
1383
|
+
for (const model of result) {
|
|
1384
|
+
for (const prop of props) {
|
|
1385
|
+
modelClass.omitImpl(model, prop);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
return result;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1212
1393
|
class ReturnImmediatelyException {
|
|
1213
1394
|
constructor(value) {
|
|
1214
1395
|
this.value = value;
|
|
@@ -1331,7 +1512,7 @@ function callOnBuildHooks(builder) {
|
|
|
1331
1512
|
callOnBuildFuncs(builder, builder.internalContext().onBuild);
|
|
1332
1513
|
|
|
1333
1514
|
builder.executeOnBuild();
|
|
1334
|
-
return builder;
|
|
1515
|
+
return wrapWhereGroups(builder);
|
|
1335
1516
|
}
|
|
1336
1517
|
|
|
1337
1518
|
function callOnBuildFuncs(builder, func) {
|
|
@@ -26,6 +26,7 @@ class QueryBuilderOperationSupport {
|
|
|
26
26
|
self._parentQuery = null;
|
|
27
27
|
self._isPartialQuery = false;
|
|
28
28
|
self._activeOperations = [];
|
|
29
|
+
self._whereGroup = null;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
static forClass(modelClass) {
|
|
@@ -413,6 +414,10 @@ class QueryBuilderOperationSupport {
|
|
|
413
414
|
const shouldAdd = this.callOperationMethod(operation, 'onAdd', [this, args]);
|
|
414
415
|
|
|
415
416
|
if (shouldAdd) {
|
|
417
|
+
if (this._whereGroup) {
|
|
418
|
+
operation.whereGroup = this._whereGroup;
|
|
419
|
+
}
|
|
420
|
+
|
|
416
421
|
if (this._activeOperations.length) {
|
|
417
422
|
const { operation: parentOperation, hookName } = last(this._activeOperations);
|
|
418
423
|
parentOperation.addChildOperation(hookName, operation);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
parseFieldExpression,
|
|
5
|
+
toPostgresJsonPathLiteral,
|
|
6
|
+
} = require('../utils/parseFieldExpression');
|
|
4
7
|
const { isObject } = require('../utils/objectUtils');
|
|
5
8
|
|
|
6
9
|
class ReferenceBuilder {
|
|
@@ -185,8 +188,7 @@ class ReferenceBuilder {
|
|
|
185
188
|
|
|
186
189
|
if (this._parsedExpr.access.length > 0) {
|
|
187
190
|
const extractor = this._cast ? '#>>' : '#>';
|
|
188
|
-
|
|
189
|
-
return `??${extractor}'{${jsonFieldRef}}'`;
|
|
191
|
+
return `??${extractor}${toPostgresJsonPathLiteral(this._parsedExpr.access)}`;
|
|
190
192
|
} else {
|
|
191
193
|
return '??';
|
|
192
194
|
}
|
|
@@ -355,7 +355,19 @@ function normalizeAllRecursive(node, allRecusive) {
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
function normalizeChildNames(node) {
|
|
358
|
-
|
|
358
|
+
const childNames = getChildNames(node);
|
|
359
|
+
|
|
360
|
+
if (node && node.$childNames) {
|
|
361
|
+
// The actual keys of the node are authoritative, since the node may be a
|
|
362
|
+
// modified copy of `graphExpressionObject()` with a stale `$childNames`.
|
|
363
|
+
// Keep the order of `$childNames` for the children that still exist.
|
|
364
|
+
return union(
|
|
365
|
+
node.$childNames.filter((childName) => childNames.includes(childName)),
|
|
366
|
+
childNames,
|
|
367
|
+
);
|
|
368
|
+
} else {
|
|
369
|
+
return childNames;
|
|
370
|
+
}
|
|
359
371
|
}
|
|
360
372
|
|
|
361
373
|
function findExpressionsAtPath(target, path, results) {
|
|
@@ -24,7 +24,13 @@ class StaticHookArguments {
|
|
|
24
24
|
|
|
25
25
|
get asFindQuery() {
|
|
26
26
|
return () => {
|
|
27
|
-
|
|
27
|
+
// The `runAfter()` callbacks of the query being executed (including the
|
|
28
|
+
// ones installed by `throwIfNotFound()` and `traverse()`) are meant for
|
|
29
|
+
// its result, not for the result of this find query.
|
|
30
|
+
return this[BUILDER_SYMBOL].toFindQuery()
|
|
31
|
+
.clear('runAfter')
|
|
32
|
+
.clearWithGraphFetched()
|
|
33
|
+
.runAfter(toArray);
|
|
28
34
|
};
|
|
29
35
|
}
|
|
30
36
|
|
|
@@ -97,6 +103,11 @@ class StaticHookArguments {
|
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
// Like `asArray()`, but an empty `first()` result becomes an empty array.
|
|
107
|
+
function toArray(result) {
|
|
108
|
+
return result === undefined ? [] : asArray(result);
|
|
109
|
+
}
|
|
110
|
+
|
|
100
111
|
function getRelation(op) {
|
|
101
112
|
return op.relation;
|
|
102
113
|
}
|
|
@@ -14,6 +14,7 @@ const { RelationExpression } = require('../RelationExpression');
|
|
|
14
14
|
const { GraphNodeDbExistence } = require('./GraphNodeDbExistence');
|
|
15
15
|
const { GraphData } = require('./GraphData');
|
|
16
16
|
const { uniqBy, asArray, isObject } = require('../../utils/objectUtils');
|
|
17
|
+
const { deprecate } = require('../../utils/deprecate');
|
|
17
18
|
|
|
18
19
|
class GraphUpsert {
|
|
19
20
|
constructor({ rootModelClass, objects, upsertOptions }) {
|
|
@@ -28,6 +29,23 @@ class GraphUpsert {
|
|
|
28
29
|
return { skipValidation: true };
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// A graph is written using many insert queries (one per batch and table),
|
|
33
|
+
// so there's no single insert query an `onConflict` clause could be applied
|
|
34
|
+
// to. Even applying it to the root table only would be unsound: rows skipped
|
|
35
|
+
// by `ignore()` aren't returned, which leaves the root models without ids,
|
|
36
|
+
// so that related rows would be inserted with missing or wrong foreign keys.
|
|
37
|
+
// The clause is ignored, and a warning is logged once per method.
|
|
38
|
+
// TODO: Throw an error instead in objection 4.0.
|
|
39
|
+
static warnOnConflict(builder, method) {
|
|
40
|
+
if (builder.has(isOnConflictOperation)) {
|
|
41
|
+
deprecate(
|
|
42
|
+
`onConflict(), ignore() and merge() are not supported by ${method}(). ` +
|
|
43
|
+
'Insert the conflicting rows with a separate insert() query instead. ' +
|
|
44
|
+
'This will throw in objection 4.0.',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
run(builder) {
|
|
32
50
|
const modelClass = builder.modelClass();
|
|
33
51
|
const graphOptions = new GraphOptions(this.upsertOpt);
|
|
@@ -326,6 +344,10 @@ function executeActions(builder, actions) {
|
|
|
326
344
|
);
|
|
327
345
|
}
|
|
328
346
|
|
|
347
|
+
function isOnConflictOperation(op) {
|
|
348
|
+
return op.name === 'onConflict' || op.name === 'ignore' || op.name === 'merge';
|
|
349
|
+
}
|
|
350
|
+
|
|
329
351
|
function returnResult(objects, isArray) {
|
|
330
352
|
return () => (isArray ? objects : objects[0]);
|
|
331
353
|
}
|