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.
@@ -83,6 +83,12 @@ class Relation {
83
83
  return false;
84
84
  }
85
85
 
86
+ // Whether owner models may legitimately lack the owner join property at
87
+ // `index`, so that it isn't reported as missing (#132).
88
+ isOptionalOwnerProp(_index) {
89
+ return false;
90
+ }
91
+
86
92
  clone() {
87
93
  const relation = new this.constructor(this.name, this.ownerModelClass);
88
94
 
@@ -85,12 +85,17 @@ class RelationFindOperation extends FindOperation {
85
85
 
86
86
  // Owner models without the join columns, e.g. loaded with a partial
87
87
  // `select()`, silently get no related models (#1832). Only `undefined`
88
- // is reported, as `null` is a legit value for a missing relation.
88
+ // is reported, as `null` is a legit value for a missing relation, and
89
+ // owner props that may legitimately be unset are skipped (#132).
89
90
  // TODO: Consider throwing an error instead in objection 4.0.
90
91
  warnOnMissingOwnerProps() {
91
92
  const { ownerProp } = this.relation;
92
93
 
93
94
  for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
95
+ if (this.relation.isOptionalOwnerProp(c)) {
96
+ continue;
97
+ }
98
+
94
99
  if (this.owner.modelArray.some((owner) => ownerProp.getProp(owner, c) === undefined)) {
95
100
  deprecate(
96
101
  `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
@@ -11,6 +11,15 @@ class BelongsToOneRelation extends Relation {
11
11
  return true;
12
12
  }
13
13
 
14
+ // The foreign key is unset when no related model was ever assigned, e.g. on
15
+ // models returned by `insert()` or created with `fromJson()`, where no
16
+ // related model is the right result. This can't be told apart from a
17
+ // foreign key left out of a partial `select()`. Foreign keys that are part
18
+ // of the id are always expected to be set (#132).
19
+ isOptionalOwnerProp(index) {
20
+ return !this.ownerModelClass.getIdPropertyArray().includes(this.ownerProp.props[index]);
21
+ }
22
+
14
23
  insert(_, owner) {
15
24
  return new BelongsToOneInsertOperation('insert', {
16
25
  relation: this,
@@ -16,10 +16,13 @@ const ManyToManyModifyMixin = (Operation) => {
16
16
  constructor(...args) {
17
17
  super(...args);
18
18
  this.modifyFilterSubquery = null;
19
+ this.skipRelationFilter = false;
19
20
  }
20
21
 
22
+ // Updates of rows that are known to be related, like the patches of
23
+ // `upsertGraph()`, keep their own filters (#135).
21
24
  get modifyMainQuery() {
22
- return true;
25
+ return !this.skipRelationFilter;
23
26
  }
24
27
 
25
28
  // At this point `builder` should only have the user's own wheres and joins. There can
@@ -108,6 +111,7 @@ const ManyToManyModifyMixin = (Operation) => {
108
111
  clone() {
109
112
  const clone = super.clone();
110
113
  clone.modifyFilterSubquery = this.modifyFilterSubquery;
114
+ clone.skipRelationFilter = this.skipRelationFilter;
111
115
  return clone;
112
116
  }
113
117
  };
@@ -93,7 +93,8 @@ class ManyToManyRelation extends Relation {
93
93
 
94
94
  // The modify operations can't tell the join rows apart from the related rows
95
95
  // if the join table is also the related table, so they would modify the
96
- // wrong rows (#1803).
96
+ // wrong rows (#1803). Inserting and relating would insert the join rows as
97
+ // new related rows (#133).
97
98
  assertCanModifyJoinTable(builder, operationName) {
98
99
  if (this.isJoinTableRelatedTable(builder)) {
99
100
  throw this.createError(
@@ -188,7 +189,9 @@ class ManyToManyRelation extends Relation {
188
189
  });
189
190
  }
190
191
 
191
- insert(_, owner) {
192
+ insert(builder, owner) {
193
+ this.assertCanModifyJoinTable(builder, 'insert');
194
+
192
195
  return new ManyToManyInsertOperation('insert', {
193
196
  relation: this,
194
197
  owner,
@@ -218,6 +221,8 @@ class ManyToManyRelation extends Relation {
218
221
  }
219
222
 
220
223
  relate(builder, owner) {
224
+ this.assertCanModifyJoinTable(builder, 'relate');
225
+
221
226
  return new ManyToManyRelateOperation('relate', {
222
227
  relation: this,
223
228
  owner,
@@ -18,6 +18,11 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
18
18
  onAdd(builder, args) {
19
19
  const obj = args[0];
20
20
 
21
+ // The modified rows are known to be related to the owner, so they are
22
+ // filtered by their own ids instead of a subquery that joins the join
23
+ // table. On MySQL, concurrent updates with that subquery deadlock (#135).
24
+ this.skipRelationFilter = builder.internalOptions().skipRelationFilter;
25
+
21
26
  // Copy all extra properties to the `joinTablePatch` object.
22
27
  for (const extra of this.relation.joinTableExtras) {
23
28
  if (extra.aliasProp in obj) {
@@ -50,7 +55,9 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
50
55
  .patch(this.joinTablePatch);
51
56
 
52
57
  addWhereGroup(joinTableUpdateQuery, () =>
53
- this.applyModifyFilterForJoinTable(joinTableUpdateQuery),
58
+ this.skipRelationFilter
59
+ ? this.applyIdFilterForJoinTable(joinTableUpdateQuery)
60
+ : this.applyModifyFilterForJoinTable(joinTableUpdateQuery),
54
61
  );
55
62
  addWhereGroup(joinTableUpdateQuery, () =>
56
63
  joinTableUpdateQuery.modify(this.relation.joinTableModify),
@@ -63,6 +70,19 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
63
70
  }
64
71
  }
65
72
 
73
+ // Filters the join rows by the ids of the owner and the updated model.
74
+ applyIdFilterForJoinTable(builder) {
75
+ return builder
76
+ .whereInComposite(
77
+ this.relation.joinTableOwnerProp.refs(builder),
78
+ this.owner.getProps(this.relation),
79
+ )
80
+ .whereComposite(
81
+ this.relation.joinTableRelatedProp.refs(builder),
82
+ this.relation.relatedProp.getProps(this.model),
83
+ );
84
+ }
85
+
66
86
  /* istanbul ignore next */
67
87
  applyModifyFilterForRelatedTable(builder) {
68
88
  throw new Error('not implemented');
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const { clone, cloneDeep } = require('./clone');
4
3
  const SMALL_ARRAY_SIZE = 10;
5
4
 
6
5
  function isEmpty(item) {
@@ -389,6 +388,83 @@ function mergeMaps(map1, map2) {
389
388
  return map;
390
389
  }
391
390
 
391
+ // Clones `value` deeply, like lodash's `cloneDeep()`: objects keep their
392
+ // prototype and get copies of their own enumerable properties, symbols
393
+ // included. Arrays, dates, regular expressions, maps, sets, buffers, typed
394
+ // arrays and array buffers are copied, circular references are kept.
395
+ // Anything else, like functions and errors, is kept as it is.
396
+ function cloneDeep(value, clones = new Map()) {
397
+ if (!isObject(value)) {
398
+ return value;
399
+ } else if (clones.has(value)) {
400
+ return clones.get(value);
401
+ }
402
+
403
+ const Ctor = value.constructor;
404
+ const tag = Object.prototype.toString.call(value);
405
+ let copy;
406
+
407
+ if (tag === '[object Object]') {
408
+ copy = Object.create(Object.getPrototypeOf(value));
409
+ clones.set(value, copy);
410
+
411
+ for (const key of Object.keys(value)) {
412
+ const item = cloneDeep(value[key], clones);
413
+
414
+ if (key === '__proto__') {
415
+ // Assigning it would set the prototype instead of the property.
416
+ Object.defineProperty(copy, key, {
417
+ value: item,
418
+ writable: true,
419
+ enumerable: true,
420
+ configurable: true,
421
+ });
422
+ } else {
423
+ copy[key] = item;
424
+ }
425
+ }
426
+
427
+ for (const symbol of Object.getOwnPropertySymbols(value)) {
428
+ if (Object.prototype.propertyIsEnumerable.call(value, symbol)) {
429
+ copy[symbol] = cloneDeep(value[symbol], clones);
430
+ }
431
+ }
432
+ } else if (tag === '[object Array]') {
433
+ copy = new Ctor(value.length);
434
+ clones.set(value, copy);
435
+
436
+ for (let i = 0, l = value.length; i < l; ++i) {
437
+ copy[i] = cloneDeep(value[i], clones);
438
+ }
439
+ } else if (Buffer.isBuffer(value)) {
440
+ copy = Buffer.from(value);
441
+ } else if (tag === '[object DataView]') {
442
+ copy = new Ctor(value.buffer.slice(0), value.byteOffset, value.byteLength);
443
+ } else if (ArrayBuffer.isView(value)) {
444
+ copy = value.slice();
445
+ } else if (tag === '[object ArrayBuffer]') {
446
+ copy = value.slice(0);
447
+ } else if (tag === '[object Date]') {
448
+ copy = new Ctor(+value);
449
+ } else if (tag === '[object RegExp]') {
450
+ copy = new Ctor(value.source, value.flags);
451
+ copy.lastIndex = value.lastIndex;
452
+ } else if (tag === '[object Map]') {
453
+ copy = new Ctor();
454
+ clones.set(value, copy);
455
+ value.forEach((item, key) => copy.set(key, cloneDeep(item, clones)));
456
+ } else if (tag === '[object Set]') {
457
+ copy = new Ctor();
458
+ clones.set(value, copy);
459
+ value.forEach((item) => copy.add(cloneDeep(item, clones)));
460
+ } else {
461
+ return value;
462
+ }
463
+
464
+ clones.set(value, copy);
465
+ return copy;
466
+ }
467
+
392
468
  module.exports = {
393
469
  isEmpty,
394
470
  isString,
@@ -411,7 +487,6 @@ module.exports = {
411
487
  values,
412
488
  union,
413
489
  chunk,
414
- clone,
415
490
  omit,
416
491
  once,
417
492
  last,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "objection",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "An SQL-friendly ORM for Node.js",
5
5
  "main": "lib/objection.js",
6
6
  "license": "MIT",
@@ -8,8 +8,11 @@
8
8
  "test": "npm run eslint && mocha --slow 10 --timeout 15000 --reporter spec --recursive tests --exclude \"tests/unit/relations/files/**\" && npm run test:typings",
9
9
  "test:fast": "mocha --slow 10 --timeout 15000 --reporter spec --recursive tests --bail --exclude \"tests/unit/relations/files/**\"",
10
10
  "test:typings": "tsc && prettier --check \"typings/**/*.ts\" \"tests/ts/**/*.ts\"",
11
+ "db:up": "docker compose up -d --wait",
12
+ "db:down": "docker compose down -v",
13
+ "build:parsers": "npx --yes peggy@5.1.0 --format commonjs -o lib/queryBuilder/parsers/jsonFieldExpressionParser.js lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs && npx --yes peggy@5.1.0 --format commonjs -o lib/queryBuilder/parsers/relationExpressionParser.js lib/queryBuilder/parsers/relationExpressionParser.pegjs && prettier --write \"lib/queryBuilder/parsers/*Parser.js\"",
11
14
  "prettier": "prettier --write \"{examples,lib,tests,testUtils,typings,doc}/**/*.{js,ts}\"",
12
- "eslint": "eslint --format codeframe \"examples/**/*.js\" \"lib/**/*.js\" \"tests/**/*.js\"",
15
+ "eslint": "eslint --format codeframe \"examples/**/*.js\" \"lib/**/*.js\" \"tests/**/*.js\" \"testUtils/**/*.js\" \"*.js\"",
13
16
  "docs:dev": "vitepress dev doc",
14
17
  "docs:build": "vitepress build doc",
15
18
  "docs:preview": "vitepress preview doc"
@@ -1058,7 +1058,7 @@ declare namespace Objection {
1058
1058
  }
1059
1059
 
1060
1060
  export interface InsertGraphOptions {
1061
- relate?: boolean | string[];
1061
+ relate?: boolean | string | string[];
1062
1062
  allowRefs?: boolean;
1063
1063
  }
1064
1064
 
@@ -1077,15 +1077,15 @@ declare namespace Objection {
1077
1077
  }
1078
1078
 
1079
1079
  export interface UpsertGraphOptions {
1080
- relate?: boolean | string[];
1081
- unrelate?: boolean | string[];
1082
- insertMissing?: boolean | string[];
1083
- update?: boolean | string[];
1084
- noInsert?: boolean | string[];
1085
- noUpdate?: boolean | string[];
1086
- noDelete?: boolean | string[];
1087
- noRelate?: boolean | string[];
1088
- noUnrelate?: boolean | string[];
1080
+ relate?: boolean | string | string[];
1081
+ unrelate?: boolean | string | string[];
1082
+ insertMissing?: boolean | string | string[];
1083
+ update?: boolean | string | string[];
1084
+ noInsert?: boolean | string | string[];
1085
+ noUpdate?: boolean | string | string[];
1086
+ noDelete?: boolean | string | string[];
1087
+ noRelate?: boolean | string | string[];
1088
+ noUnrelate?: boolean | string | string[];
1089
1089
  allowRefs?: boolean;
1090
1090
  }
1091
1091