objection 3.4.1 → 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 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/Vincit/objection.js/tree/main/examples/koa-ts).
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/Vincit/objection.js/tree/main/examples/koa) and start playing with it. There's also a [typescript version](https://github.com/Vincit/objection.js/tree/main/examples/koa-ts) available.
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/Vincit/objection.js/tree/main/examples)
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
- constructor(options) {
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 (Array.isArray(option)) {
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 instance of boolean or array of strings`,
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
 
@@ -208,7 +208,11 @@ class GraphInsertAction extends GraphAction {
208
208
  return Promise.all(
209
209
  nodes.map((node) => {
210
210
  if (node.parentEdge) {
211
- return node.parentEdge.relation.beforeInsert(node.obj, parentBuilder.context());
211
+ return node.parentEdge.relation.beforeInsert(
212
+ node.obj,
213
+ parentBuilder.context(),
214
+ node.parentNode.obj,
215
+ );
212
216
  } else {
213
217
  return null;
214
218
  }
@@ -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) {
@@ -63,7 +63,12 @@ class InsertGraphOperation extends DelegateOperation {
63
63
  if (this.relation) {
64
64
  // We still need to call the relation before insert hook if the the
65
65
  // delegate operation is a RelationInsertOperation.
66
- return this.relation.executeBeforeInsert(this.models, builder.context(), result);
66
+ return this.relation.executeBeforeInsert(
67
+ this.models,
68
+ builder.context(),
69
+ result,
70
+ this.delegate.ownerModel,
71
+ );
67
72
  } else {
68
73
  return result;
69
74
  }