objection 3.5.0 → 3.6.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 +3 -3
- package/lib/queryBuilder/InternalOptions.js +2 -0
- package/lib/queryBuilder/graph/GraphOptions.js +153 -6
- package/lib/queryBuilder/graph/GraphUpsert.js +35 -0
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +3 -0
- package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +434 -364
- package/lib/queryBuilder/parsers/relationExpressionParser.js +512 -547
- package/lib/relations/Relation.js +6 -0
- package/lib/relations/RelationFindOperation.js +6 -1
- package/lib/relations/belongsToOne/BelongsToOneRelation.js +9 -0
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +5 -1
- package/lib/relations/manyToMany/ManyToManyRelation.js +7 -2
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +21 -1
- package/lib/utils/objectUtils.js +77 -2
- package/package.json +5 -2
- package/typings/objection/index.d.ts +10 -10
- package/lib/utils/clone.js +0 -2329
package/README.md
CHANGED
|
@@ -36,9 +36,9 @@ What objection.js **doesn't** give you:
|
|
|
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
|
|
38
38
|
to you. knex has a great [migration tool](https://knexjs.org/guide/migrations.html) that we recommend for this job. Check
|
|
39
|
-
out the [example project](https://github.com/
|
|
39
|
+
out the [example project](https://github.com/ditojs/objection/tree/main/examples/koa-ts).
|
|
40
40
|
|
|
41
|
-
The best way to get started is to clone our [example project](https://github.com/
|
|
41
|
+
The best way to get started is to clone our [example project](https://github.com/ditojs/objection/tree/main/examples/koa) and start playing with it. There's also a [typescript version](https://github.com/ditojs/objection/tree/main/examples/koa-ts) available.
|
|
42
42
|
|
|
43
43
|
Check out [this issue](https://github.com/Vincit/objection.js/issues/1069) to see who is using objection and what they think about it.
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ Shortcuts:
|
|
|
46
46
|
|
|
47
47
|
- [Who uses objection.js](https://github.com/Vincit/objection.js/discussions/2464)
|
|
48
48
|
- [API reference](https://ditojs.github.io/objection/api/query-builder/)
|
|
49
|
-
- [Example projects](https://github.com/
|
|
49
|
+
- [Example projects](https://github.com/ditojs/objection/tree/main/examples)
|
|
50
50
|
- [Changelog](https://ditojs.github.io/objection/release-notes/changelog.html)
|
|
51
51
|
- [v1 -> v2 -> v3 migration guide](https://ditojs.github.io/objection/release-notes/migration.html)
|
|
52
52
|
- [Contribution guide](https://ditojs.github.io/objection/guide/contributing.html)
|
|
@@ -6,6 +6,7 @@ class InternalOptions {
|
|
|
6
6
|
this.keepImplicitJoinProps = false;
|
|
7
7
|
this.returnImmediatelyValue = undefined;
|
|
8
8
|
this.isInternalQuery = false;
|
|
9
|
+
this.skipRelationFilter = false;
|
|
9
10
|
this.debug = false;
|
|
10
11
|
this.schema = undefined;
|
|
11
12
|
}
|
|
@@ -17,6 +18,7 @@ class InternalOptions {
|
|
|
17
18
|
copy.keepImplicitJoinProps = this.keepImplicitJoinProps;
|
|
18
19
|
copy.returnImmediatelyValue = this.returnImmediatelyValue;
|
|
19
20
|
copy.isInternalQuery = this.isInternalQuery;
|
|
21
|
+
copy.skipRelationFilter = this.skipRelationFilter;
|
|
20
22
|
copy.debug = this.debug;
|
|
21
23
|
copy.schema = this.schema;
|
|
22
24
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { RelationExpression } = require('../RelationExpression');
|
|
4
|
+
const { isString } = require('../../utils/objectUtils');
|
|
5
|
+
|
|
3
6
|
const NO_RELATE = 'noRelate';
|
|
4
7
|
const NO_UNRELATE = 'noUnrelate';
|
|
5
8
|
const NO_INSERT = 'noInsert';
|
|
@@ -27,6 +30,20 @@ const OPTION_NAMES = [
|
|
|
27
30
|
ALLOW_REFS,
|
|
28
31
|
];
|
|
29
32
|
|
|
33
|
+
// The options that are matched against the relation path of each node, see
|
|
34
|
+
// `_hasOption()`.
|
|
35
|
+
const NODE_OPTION_NAMES = [
|
|
36
|
+
NO_RELATE,
|
|
37
|
+
NO_UNRELATE,
|
|
38
|
+
NO_INSERT,
|
|
39
|
+
NO_UPDATE,
|
|
40
|
+
NO_DELETE,
|
|
41
|
+
UPDATE,
|
|
42
|
+
RELATE,
|
|
43
|
+
UNRELATE,
|
|
44
|
+
INSERT_MISSING,
|
|
45
|
+
];
|
|
46
|
+
|
|
30
47
|
const FetchStrategy = {
|
|
31
48
|
OnlyIdentifiers: 'OnlyIdentifiers',
|
|
32
49
|
Everything: 'Everything',
|
|
@@ -34,12 +51,20 @@ const FetchStrategy = {
|
|
|
34
51
|
};
|
|
35
52
|
|
|
36
53
|
class GraphOptions {
|
|
37
|
-
|
|
54
|
+
// `rootRelationPath` is the relation path of the root node of a recursive
|
|
55
|
+
// upsert in the original graph, against which relation expressions match.
|
|
56
|
+
constructor(options, rootRelationPath = []) {
|
|
38
57
|
if (options instanceof GraphOptions) {
|
|
39
58
|
this.options = options.options;
|
|
59
|
+
this.rootRelationPath = options.rootRelationPath;
|
|
40
60
|
} else {
|
|
41
|
-
this.options = options;
|
|
61
|
+
this.options = parseRelationExpressions(options);
|
|
62
|
+
this.rootRelationPath = rootRelationPath;
|
|
42
63
|
}
|
|
64
|
+
|
|
65
|
+
// Caches the result of matching relation expressions against relation
|
|
66
|
+
// paths, by option name and relation path key.
|
|
67
|
+
this.expressionMatches = new Map();
|
|
43
68
|
}
|
|
44
69
|
|
|
45
70
|
isFetchStrategy(strategy) {
|
|
@@ -190,7 +215,7 @@ class GraphOptions {
|
|
|
190
215
|
|
|
191
216
|
if (Array.isArray(value)) {
|
|
192
217
|
newOpt[name] = value
|
|
193
|
-
.filter((it) => it.startsWith(newRootRelationPath))
|
|
218
|
+
.filter((it) => it === newRootRelationPath || it.startsWith(`${newRootRelationPath}.`))
|
|
194
219
|
.map((it) => it.slice(newRootRelationPath.length + 1))
|
|
195
220
|
.filter((it) => !!it);
|
|
196
221
|
} else {
|
|
@@ -198,13 +223,15 @@ class GraphOptions {
|
|
|
198
223
|
}
|
|
199
224
|
}
|
|
200
225
|
|
|
201
|
-
return new GraphOptions(newOpt);
|
|
226
|
+
return new GraphOptions(newOpt, [...this.rootRelationPath, ...newRoot.relationPath]);
|
|
202
227
|
}
|
|
203
228
|
|
|
204
229
|
_hasOption(node, optionName) {
|
|
205
230
|
const option = this.options[optionName];
|
|
206
231
|
|
|
207
|
-
if (
|
|
232
|
+
if (isRelationExpression(option)) {
|
|
233
|
+
return this._matchesRelationExpression(node, optionName, option);
|
|
234
|
+
} else if (Array.isArray(option)) {
|
|
208
235
|
return option.indexOf(node.relationPathKey) !== -1;
|
|
209
236
|
} else if (typeof option === 'boolean') {
|
|
210
237
|
return option;
|
|
@@ -212,10 +239,130 @@ class GraphOptions {
|
|
|
212
239
|
return false;
|
|
213
240
|
} else {
|
|
214
241
|
throw new Error(
|
|
215
|
-
`expected ${optionName} option value "${option}" to be an
|
|
242
|
+
`expected ${optionName} option value "${option}" to be a boolean, an array of relation paths or a relation expression`,
|
|
216
243
|
);
|
|
217
244
|
}
|
|
218
245
|
}
|
|
246
|
+
|
|
247
|
+
// A node matches a relation expression when its relation path leads to a
|
|
248
|
+
// leaf of the expression, like it would match the same relation paths in an
|
|
249
|
+
// array: `'movies.reviews'` matches `movies.reviews` but not `movies`.
|
|
250
|
+
// `'*'` matches all relations below it and recursive expressions like
|
|
251
|
+
// `'parent.^'` match every level of the recursion. The root has no relation
|
|
252
|
+
// path and never matches.
|
|
253
|
+
_matchesRelationExpression(node, optionName, expression) {
|
|
254
|
+
const relationPath = [...this.rootRelationPath, ...node.relationPath];
|
|
255
|
+
|
|
256
|
+
if (relationPath.length === 0) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const relationPathKey = relationPath.join('.');
|
|
261
|
+
|
|
262
|
+
let matches = this.expressionMatches.get(optionName);
|
|
263
|
+
|
|
264
|
+
if (!matches) {
|
|
265
|
+
matches = new Map();
|
|
266
|
+
this.expressionMatches.set(optionName, matches);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let isMatch = matches.get(relationPathKey);
|
|
270
|
+
|
|
271
|
+
if (isMatch === undefined) {
|
|
272
|
+
isMatch = matchesRelationPath(expression, relationPath);
|
|
273
|
+
matches.set(relationPathKey, isMatch);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return isMatch;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Parses the string values of the node options into relation expressions,
|
|
281
|
+
// once per operation.
|
|
282
|
+
function parseRelationExpressions(options) {
|
|
283
|
+
let parsedOptions = options;
|
|
284
|
+
|
|
285
|
+
for (const optionName of NODE_OPTION_NAMES) {
|
|
286
|
+
const option = options[optionName];
|
|
287
|
+
|
|
288
|
+
if (isString(option)) {
|
|
289
|
+
if (parsedOptions === options) {
|
|
290
|
+
parsedOptions = { ...options };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
parsedOptions[optionName] = parseRelationExpression(optionName, option);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return parsedOptions;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function parseRelationExpression(optionName, expression) {
|
|
301
|
+
const createError = (message) => {
|
|
302
|
+
return new Error(
|
|
303
|
+
`invalid relation expression "${expression}" in ${optionName} option: ${message}`,
|
|
304
|
+
);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
let parsedExpression;
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
parsedExpression = RelationExpression.create(expression);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
throw createError(err.message);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (parsedExpression.isEmpty && !parsedExpression.node.$allRecursive) {
|
|
316
|
+
throw createError('the expression is empty');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Relation paths are matched by relation name, so aliases would never match.
|
|
320
|
+
const alias = findAlias(parsedExpression.node);
|
|
321
|
+
|
|
322
|
+
if (alias) {
|
|
323
|
+
throw createError(`aliases like "${alias.$relation} as ${alias.$name}" are not supported`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return parsedExpression;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function findAlias(node) {
|
|
330
|
+
for (const childName of node.$childNames) {
|
|
331
|
+
const child = node[childName];
|
|
332
|
+
|
|
333
|
+
if (child.$name !== child.$relation) {
|
|
334
|
+
return child;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const alias = findAlias(child);
|
|
338
|
+
|
|
339
|
+
if (alias) {
|
|
340
|
+
return alias;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function matchesRelationPath(expression, relationPath) {
|
|
348
|
+
for (const relationName of relationPath) {
|
|
349
|
+
expression = expression.childExpression(relationName);
|
|
350
|
+
|
|
351
|
+
if (!expression) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Below `*`, the expression stays on the same node with increasing depth.
|
|
357
|
+
if (expression.node.$allRecursive) {
|
|
358
|
+
return expression.recursionDepth > 0;
|
|
359
|
+
} else {
|
|
360
|
+
return expression.isEmpty;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function isRelationExpression(value) {
|
|
365
|
+
return !!value && value.isObjectionRelationExpression === true;
|
|
219
366
|
}
|
|
220
367
|
|
|
221
368
|
function getCurrentNode(node, graphData) {
|
|
@@ -57,6 +57,7 @@ class GraphUpsert {
|
|
|
57
57
|
return createGraphData(builder, graphOptions, graph)
|
|
58
58
|
.then(checkForErrors(builder))
|
|
59
59
|
.then(pruneGraphs())
|
|
60
|
+
.then(checkForJoinTableRelatedTableErrors(builder))
|
|
60
61
|
.then(executeOperations(builder))
|
|
61
62
|
.then(returnResult(this.objects, this.isArray));
|
|
62
63
|
}
|
|
@@ -66,6 +67,11 @@ class GraphUpsert {
|
|
|
66
67
|
// default behaviour instead (#84). A warning is logged once per option.
|
|
67
68
|
// TODO: Throw an error instead in objection 4.0.
|
|
68
69
|
function warnOnUnknownOptions(upsertOptions) {
|
|
70
|
+
// Recursive upserts pass on the already checked options of their parent.
|
|
71
|
+
if (upsertOptions instanceof GraphOptions) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
69
75
|
for (const name of Object.keys(upsertOptions)) {
|
|
70
76
|
if (!OPTION_NAMES.includes(name)) {
|
|
71
77
|
deprecate(
|
|
@@ -249,6 +255,35 @@ function checkForErrors(builder) {
|
|
|
249
255
|
};
|
|
250
256
|
}
|
|
251
257
|
|
|
258
|
+
// Inserting or relating a node of a many-to-many relation inserts a join row,
|
|
259
|
+
// which would be a new related row if the join table is also the related
|
|
260
|
+
// table (#133). Check this before this graph is written. It runs after
|
|
261
|
+
// pruning, as the branches below related nodes are upserted and checked by
|
|
262
|
+
// nested `upsertGraph` calls against their own current graphs.
|
|
263
|
+
function checkForJoinTableRelatedTableErrors(builder) {
|
|
264
|
+
return (graphData) => {
|
|
265
|
+
const { graphOptions, currentGraph, graph } = graphData;
|
|
266
|
+
|
|
267
|
+
for (const node of graph.nodes) {
|
|
268
|
+
const relation = node.parentEdge && node.parentEdge.relation;
|
|
269
|
+
|
|
270
|
+
if (
|
|
271
|
+
relation &&
|
|
272
|
+
relation.isObjectionManyToManyRelation &&
|
|
273
|
+
!currentGraph.nodeForNode(node) &&
|
|
274
|
+
graphOptions.shouldInsertOrRelate(node, graphData)
|
|
275
|
+
) {
|
|
276
|
+
relation.assertCanModifyJoinTable(
|
|
277
|
+
builder,
|
|
278
|
+
graphOptions.shouldRelate(node, graphData) ? 'relate' : 'insert',
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return graphData;
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
252
287
|
function checkForNotFoundErrors(graphData, builder) {
|
|
253
288
|
const { graphOptions, currentGraph, graph } = graphData;
|
|
254
289
|
|
|
@@ -85,6 +85,9 @@ class GraphPatchAction extends GraphAction {
|
|
|
85
85
|
|
|
86
86
|
const updateBuilder = this._createBuilder(node)
|
|
87
87
|
.childQueryOf(builder, childQueryOptions())
|
|
88
|
+
// The current graph was fetched through the relations, so the patched
|
|
89
|
+
// models are known to be related to their parents.
|
|
90
|
+
.internalOptions({ skipRelationFilter: true })
|
|
88
91
|
.copyFrom(builder, GraphAction.ReturningAllSelector);
|
|
89
92
|
|
|
90
93
|
if (hasBeforeUpdate) {
|