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.
Files changed (52) hide show
  1. package/README.md +13 -13
  2. package/lib/model/AjvValidator.js +133 -3
  3. package/lib/queryBuilder/QueryBuilder.js +186 -5
  4. package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
  5. package/lib/queryBuilder/ReferenceBuilder.js +5 -3
  6. package/lib/queryBuilder/RelationExpression.js +13 -1
  7. package/lib/queryBuilder/StaticHookArguments.js +12 -1
  8. package/lib/queryBuilder/graph/GraphUpsert.js +22 -0
  9. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
  10. package/lib/queryBuilder/graph/insert/GraphInsert.js +107 -8
  11. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -1
  12. package/lib/queryBuilder/join/JoinResultParser.js +22 -0
  13. package/lib/queryBuilder/join/RelationJoiner.js +71 -22
  14. package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
  15. package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
  16. package/lib/queryBuilder/operations/InsertOperation.js +121 -12
  17. package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
  18. package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
  19. package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
  20. package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
  21. package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
  22. package/lib/queryBuilder/operations/eager/EagerOperation.js +4 -0
  23. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +2 -6
  24. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +10 -33
  25. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
  26. package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
  27. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +41 -34
  28. package/lib/queryBuilder/operations/select/Selection.js +95 -4
  29. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
  30. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
  31. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
  32. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
  33. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
  34. package/lib/queryBuilder/whereGroups.js +125 -0
  35. package/lib/relations/Relation.js +5 -2
  36. package/lib/relations/hasMany/HasManyRelateOperation.js +5 -2
  37. package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
  38. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
  39. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
  40. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
  41. package/lib/relations/manyToMany/ManyToManyRelation.js +70 -48
  42. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
  43. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
  44. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
  45. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
  46. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +8 -2
  47. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
  48. package/lib/utils/deprecate.js +6 -0
  49. package/lib/utils/identifierMapping.js +15 -1
  50. package/lib/utils/parseFieldExpression.js +28 -0
  51. package/package.json +10 -6
  52. package/typings/objection/index.d.ts +280 -28
@@ -166,10 +166,10 @@ function peg$parse(input, options) {
166
166
  peg$c4 = peg$literalExpectation('[', false),
167
167
  peg$c5 = '"',
168
168
  peg$c6 = peg$literalExpectation('"', false),
169
- peg$c7 = "'",
170
- peg$c8 = peg$literalExpectation("'", false),
171
- peg$c9 = ']',
172
- peg$c10 = peg$literalExpectation(']', false),
169
+ peg$c7 = ']',
170
+ peg$c8 = peg$literalExpectation(']', false),
171
+ peg$c9 = "'",
172
+ peg$c10 = peg$literalExpectation("'", false),
173
173
  peg$c11 = function (key) {
174
174
  return { type: 'object', ref: Array.isArray(key) ? key[1] : key };
175
175
  },
@@ -415,7 +415,7 @@ function peg$parse(input, options) {
415
415
  }
416
416
 
417
417
  function peg$parsebracketStringRef() {
418
- var s0, s1, s2, s3, s4, s5;
418
+ var s0, s1, s2, s3, s4, s5, s6, s7;
419
419
 
420
420
  s0 = peg$currPos;
421
421
  if (input.charCodeAt(peg$currPos) === 91) {
@@ -451,8 +451,31 @@ function peg$parse(input, options) {
451
451
  }
452
452
  }
453
453
  if (s5 !== peg$FAILED) {
454
- s3 = [s3, s4, s5];
455
- s2 = s3;
454
+ s6 = peg$currPos;
455
+ peg$silentFails++;
456
+ if (input.charCodeAt(peg$currPos) === 93) {
457
+ s7 = peg$c7;
458
+ peg$currPos++;
459
+ } else {
460
+ s7 = peg$FAILED;
461
+ if (peg$silentFails === 0) {
462
+ peg$fail(peg$c8);
463
+ }
464
+ }
465
+ peg$silentFails--;
466
+ if (s7 !== peg$FAILED) {
467
+ peg$currPos = s6;
468
+ s6 = void 0;
469
+ } else {
470
+ s6 = peg$FAILED;
471
+ }
472
+ if (s6 !== peg$FAILED) {
473
+ s3 = [s3, s4, s5, s6];
474
+ s2 = s3;
475
+ } else {
476
+ peg$currPos = s2;
477
+ s2 = peg$FAILED;
478
+ }
456
479
  } else {
457
480
  peg$currPos = s2;
458
481
  s2 = peg$FAILED;
@@ -468,29 +491,52 @@ function peg$parse(input, options) {
468
491
  if (s2 === peg$FAILED) {
469
492
  s2 = peg$currPos;
470
493
  if (input.charCodeAt(peg$currPos) === 39) {
471
- s3 = peg$c7;
494
+ s3 = peg$c9;
472
495
  peg$currPos++;
473
496
  } else {
474
497
  s3 = peg$FAILED;
475
498
  if (peg$silentFails === 0) {
476
- peg$fail(peg$c8);
499
+ peg$fail(peg$c10);
477
500
  }
478
501
  }
479
502
  if (s3 !== peg$FAILED) {
480
503
  s4 = peg$parsestringWithoutSingleQuotes();
481
504
  if (s4 !== peg$FAILED) {
482
505
  if (input.charCodeAt(peg$currPos) === 39) {
483
- s5 = peg$c7;
506
+ s5 = peg$c9;
484
507
  peg$currPos++;
485
508
  } else {
486
509
  s5 = peg$FAILED;
487
510
  if (peg$silentFails === 0) {
488
- peg$fail(peg$c8);
511
+ peg$fail(peg$c10);
489
512
  }
490
513
  }
491
514
  if (s5 !== peg$FAILED) {
492
- s3 = [s3, s4, s5];
493
- s2 = s3;
515
+ s6 = peg$currPos;
516
+ peg$silentFails++;
517
+ if (input.charCodeAt(peg$currPos) === 93) {
518
+ s7 = peg$c7;
519
+ peg$currPos++;
520
+ } else {
521
+ s7 = peg$FAILED;
522
+ if (peg$silentFails === 0) {
523
+ peg$fail(peg$c8);
524
+ }
525
+ }
526
+ peg$silentFails--;
527
+ if (s7 !== peg$FAILED) {
528
+ peg$currPos = s6;
529
+ s6 = void 0;
530
+ } else {
531
+ s6 = peg$FAILED;
532
+ }
533
+ if (s6 !== peg$FAILED) {
534
+ s3 = [s3, s4, s5, s6];
535
+ s2 = s3;
536
+ } else {
537
+ peg$currPos = s2;
538
+ s2 = peg$FAILED;
539
+ }
494
540
  } else {
495
541
  peg$currPos = s2;
496
542
  s2 = peg$FAILED;
@@ -509,12 +555,12 @@ function peg$parse(input, options) {
509
555
  }
510
556
  if (s2 !== peg$FAILED) {
511
557
  if (input.charCodeAt(peg$currPos) === 93) {
512
- s3 = peg$c9;
558
+ s3 = peg$c7;
513
559
  peg$currPos++;
514
560
  } else {
515
561
  s3 = peg$FAILED;
516
562
  if (peg$silentFails === 0) {
517
- peg$fail(peg$c10);
563
+ peg$fail(peg$c8);
518
564
  }
519
565
  }
520
566
  if (s3 !== peg$FAILED) {
@@ -554,12 +600,12 @@ function peg$parse(input, options) {
554
600
  s2 = peg$parseinteger();
555
601
  if (s2 !== peg$FAILED) {
556
602
  if (input.charCodeAt(peg$currPos) === 93) {
557
- s3 = peg$c9;
603
+ s3 = peg$c7;
558
604
  peg$currPos++;
559
605
  } else {
560
606
  s3 = peg$FAILED;
561
607
  if (peg$silentFails === 0) {
562
- peg$fail(peg$c10);
608
+ peg$fail(peg$c8);
563
609
  }
564
610
  }
565
611
  if (s3 !== peg$FAILED) {
@@ -719,21 +765,17 @@ function peg$parse(input, options) {
719
765
  peg$fail(peg$c22);
720
766
  }
721
767
  }
722
- if (s2 !== peg$FAILED) {
723
- while (s2 !== peg$FAILED) {
724
- s1.push(s2);
725
- if (peg$c21.test(input.charAt(peg$currPos))) {
726
- s2 = input.charAt(peg$currPos);
727
- peg$currPos++;
728
- } else {
729
- s2 = peg$FAILED;
730
- if (peg$silentFails === 0) {
731
- peg$fail(peg$c22);
732
- }
768
+ while (s2 !== peg$FAILED) {
769
+ s1.push(s2);
770
+ if (peg$c21.test(input.charAt(peg$currPos))) {
771
+ s2 = input.charAt(peg$currPos);
772
+ peg$currPos++;
773
+ } else {
774
+ s2 = peg$FAILED;
775
+ if (peg$silentFails === 0) {
776
+ peg$fail(peg$c22);
733
777
  }
734
778
  }
735
- } else {
736
- s1 = peg$FAILED;
737
779
  }
738
780
  if (s1 !== peg$FAILED) {
739
781
  peg$savedPos = s0;
@@ -758,21 +800,17 @@ function peg$parse(input, options) {
758
800
  peg$fail(peg$c24);
759
801
  }
760
802
  }
761
- if (s2 !== peg$FAILED) {
762
- while (s2 !== peg$FAILED) {
763
- s1.push(s2);
764
- if (peg$c23.test(input.charAt(peg$currPos))) {
765
- s2 = input.charAt(peg$currPos);
766
- peg$currPos++;
767
- } else {
768
- s2 = peg$FAILED;
769
- if (peg$silentFails === 0) {
770
- peg$fail(peg$c24);
771
- }
803
+ while (s2 !== peg$FAILED) {
804
+ s1.push(s2);
805
+ if (peg$c23.test(input.charAt(peg$currPos))) {
806
+ s2 = input.charAt(peg$currPos);
807
+ peg$currPos++;
808
+ } else {
809
+ s2 = peg$FAILED;
810
+ if (peg$silentFails === 0) {
811
+ peg$fail(peg$c24);
772
812
  }
773
813
  }
774
- } else {
775
- s1 = peg$FAILED;
776
814
  }
777
815
  if (s1 !== peg$FAILED) {
778
816
  peg$savedPos = s0;
@@ -31,6 +31,7 @@
31
31
  * 4. Keys containing square brackets and quotes
32
32
  * `objectColumn:['Double."Quote".[]']` and `objectColumn:["Sinlge.'Quote'.[]"]`
33
33
  * Column `{ "Double.\"Quote\".[]" : "I was referred", "Sinlge.'Quote'.[]" : "Mee too!" }`
34
+ * 5. Empty keys `objectColumn:[""]` or `objectColumn:['']` Column `{ "" : "I was referred" }`
34
35
  * 99. Keys containing dots, square brackets, single quotes and double quotes in one json key is
35
36
  * not currently supported
36
37
  *
@@ -60,8 +61,8 @@ start =
60
61
  bracketStringRef =
61
62
  '['
62
63
  key:(
63
- '"' stringWithoutDoubleQuotes '"' /
64
- "'" stringWithoutSingleQuotes "'" /
64
+ '"' stringWithoutDoubleQuotes '"' &']' /
65
+ "'" stringWithoutSingleQuotes "'" &']' /
65
66
  stringWithoutSquareBrackets
66
67
  )
67
68
  ']'
@@ -86,10 +87,10 @@ stringWithoutColon =
86
87
  chars:([^:])+ { return chars.join(""); }
87
88
 
88
89
  stringWithoutDoubleQuotes =
89
- chars:([^"])+ { return chars.join(""); }
90
+ chars:([^"])* { return chars.join(""); }
90
91
 
91
92
  stringWithoutSingleQuotes =
92
- chars:([^'])+ { return chars.join(""); }
93
+ chars:([^'])* { return chars.join(""); }
93
94
 
94
95
  stringWithoutSquareBracketsOrDots =
95
96
  chars:([^.\x5D\x5B])+ { return chars.join(""); }
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ const { WhereGroupOperation } = require('./operations/WhereGroupOperation');
4
+ const { isObject, isFunction } = require('../utils/objectUtils');
5
+ const { isKnexRaw } = require('../utils/knexUtils');
6
+
7
+ // Where operations of a query can come from different sources: the user, a
8
+ // relation's owner condition, the relation's `modify`/`filter` and so on. Each
9
+ // source forms its own "where group". All operations the user adds belong to
10
+ // the default group, and `addWhereGroup` assigns the operations added inside
11
+ // its callback to a new, separate group.
12
+ //
13
+ // Knex simply joins all where clauses with their `and` or `or` boolean. Since
14
+ // `and` binds tighter than `or`, a user's `orWhere` would escape the relation's
15
+ // owner condition: `where "ownerId" in (1) and a or b`. This would find, update
16
+ // and delete rows of other owners too. See issues #2191 and #1909.
17
+ //
18
+ // To prevent this, `wrapWhereGroups` is called after all `onBuild` hooks have
19
+ // run. If a query contains more than one where group, every group that may
20
+ // contain an `or` at its top level is wrapped in parentheses, so that the
21
+ // groups are always ANDed together: `where "ownerId" in (1) and (a or b)`.
22
+ // Groups without an `or` are left alone so that their SQL doesn't change.
23
+ function addWhereGroup(builder, callback) {
24
+ const parentWhereGroup = builder._whereGroup;
25
+ builder._whereGroup = {};
26
+
27
+ try {
28
+ return callback(builder);
29
+ } finally {
30
+ builder._whereGroup = parentWhereGroup;
31
+ }
32
+ }
33
+
34
+ function wrapWhereGroups(builder) {
35
+ const WhereSelector = builder.constructor.WhereSelector;
36
+ const whereOps = new Set();
37
+ const groups = new Map();
38
+
39
+ builder.forEachOperation(WhereSelector, (op) => {
40
+ // Only consider the top-most where operations. The descendants of a
41
+ // where operation are built as part of it.
42
+ if (op.isAncestorInSet(whereOps)) {
43
+ return;
44
+ }
45
+
46
+ whereOps.add(op);
47
+
48
+ const group = getWhereGroup(op);
49
+ let ops = groups.get(group);
50
+
51
+ if (!ops) {
52
+ ops = [];
53
+ groups.set(group, ops);
54
+ }
55
+
56
+ ops.push(op);
57
+ });
58
+
59
+ if (groups.size < 2) {
60
+ return builder;
61
+ }
62
+
63
+ for (const [group, ops] of groups) {
64
+ if (ops.some(mayContainTopLevelOr)) {
65
+ wrapOperations(builder, group, ops);
66
+ }
67
+ }
68
+
69
+ return builder;
70
+ }
71
+
72
+ function getWhereGroup(op) {
73
+ for (let it = op; it; it = it.parentOperation) {
74
+ if (it.whereGroup) {
75
+ return it.whereGroup;
76
+ }
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ function mayContainTopLevelOr(op) {
83
+ if (op instanceof WhereGroupOperation) {
84
+ return false;
85
+ }
86
+
87
+ // orWhere, orWhereIn, orWhereRaw, orWhereExists, orWhereJsonSupersetOf etc.
88
+ if (/^or[A-Z]/.test(op.name)) {
89
+ return true;
90
+ }
91
+
92
+ // Raw SQL isn't parenthesized by knex and can contain `or`.
93
+ if (/Raw$/.test(op.name)) {
94
+ return true;
95
+ }
96
+
97
+ return Array.isArray(op.args) && op.args.length === 1 && isRaw(op.args[0]);
98
+ }
99
+
100
+ function isRaw(arg) {
101
+ return isKnexRaw(arg) || (isObject(arg) && isFunction(arg.toKnexRaw));
102
+ }
103
+
104
+ function wrapOperations(builder, group, ops) {
105
+ const groupOp = new WhereGroupOperation('where');
106
+ groupOp.whereGroup = group;
107
+
108
+ builder.replaceOperation(ops[0], groupOp);
109
+
110
+ for (let i = 1; i < ops.length; ++i) {
111
+ builder.removeOperation(ops[i]);
112
+ }
113
+
114
+ for (const op of ops) {
115
+ op.parentOperation = null;
116
+ op.adderHookName = null;
117
+ }
118
+
119
+ groupOp.operations = ops;
120
+ }
121
+
122
+ module.exports = {
123
+ addWhereGroup,
124
+ wrapWhereGroups,
125
+ };
@@ -10,6 +10,7 @@ const { resolveModel } = require('../utils/resolveModel');
10
10
  const { get, isFunction } = require('../utils/objectUtils');
11
11
  const { mapAfterAllReturn } = require('../utils/promiseUtils');
12
12
  const { createModifier } = require('../utils/createModifier');
13
+ const { addWhereGroup } = require('../queryBuilder/whereGroups');
13
14
 
14
15
  class Relation {
15
16
  constructor(relationName, OwnerClass) {
@@ -115,14 +116,16 @@ class Relation {
115
116
 
116
117
  findQuery(builder, owner) {
117
118
  const relatedRefs = this.relatedProp.refs(builder);
118
- owner.buildFindQuery(builder, this, relatedRefs);
119
+ addWhereGroup(builder, () => owner.buildFindQuery(builder, this, relatedRefs));
119
120
 
120
121
  return this.applyModify(builder);
121
122
  }
122
123
 
123
124
  applyModify(builder) {
124
125
  try {
125
- return builder.modify(this.modify);
126
+ // Keep the relation's `modify` in a separate where group so that an
127
+ // `orWhere` in it can't escape the owner condition. See `whereGroups.js`.
128
+ return addWhereGroup(builder, () => builder.modify(this.modify));
126
129
  } catch (err) {
127
130
  if (err instanceof this.relatedModelClass.ModifierNotFoundError) {
128
131
  throw this.createError(err.message);
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { normalizeIds } = require('../../utils/normalizeIds');
4
4
  const { RelateOperation } = require('../../queryBuilder/operations/RelateOperation');
5
+ const { addWhereGroup } = require('../../queryBuilder/whereGroups');
5
6
 
6
7
  class HasManyRelateOperation extends RelateOperation {
7
8
  onAdd(_, args) {
@@ -31,8 +32,10 @@ class HasManyRelateOperation extends RelateOperation {
31
32
  .copyFrom(builder, /returning/)
32
33
  .copyFrom(builder, builder.constructor.JoinSelector)
33
34
  .copyFrom(builder, builder.constructor.WhereSelector)
34
- .findByIds(this.ids)
35
- .modify(this.relation.modify);
35
+ .modify((query) => {
36
+ addWhereGroup(query, () => query.findByIds(this.ids));
37
+ addWhereGroup(query, () => query.modify(this.relation.modify));
38
+ });
36
39
  }
37
40
  }
38
41
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { UnrelateOperation } = require('../../queryBuilder/operations/UnrelateOperation');
4
+ const { addWhereGroup } = require('../../queryBuilder/whereGroups');
4
5
 
5
6
  class HasManyUnrelateOperation extends UnrelateOperation {
6
7
  queryExecutor(builder) {
@@ -19,8 +20,10 @@ class HasManyUnrelateOperation extends UnrelateOperation {
19
20
  .patch(patch)
20
21
  .copyFrom(builder, builder.constructor.JoinSelector)
21
22
  .copyFrom(builder, builder.constructor.WhereSelector)
22
- .whereInComposite(relatedRefs, ownerValues)
23
- .modify(this.relation.modify);
23
+ .modify((query) => {
24
+ addWhereGroup(query, () => query.whereInComposite(relatedRefs, ownerValues));
25
+ addWhereGroup(query, () => query.modify(this.relation.modify));
26
+ });
24
27
  }
25
28
  }
26
29
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { ManyToManyFindOperation } = require('./find/ManyToManyFindOperation');
4
+ const { addWhereGroup } = require('../../queryBuilder/whereGroups');
4
5
 
5
6
  // This mixin contains the shared code for all modify operations (update, delete, relate, unrelate)
6
7
  // for ManyToManyRelation operations.
@@ -31,7 +32,7 @@ const ManyToManyModifyMixin = (Operation) => {
31
32
  this.removeFiltersFromMainQuery(builder);
32
33
 
33
34
  // Add a single where clause that uses the created subquery.
34
- this.applyModifyFilterForRelatedTable(builder);
35
+ addWhereGroup(builder, () => this.applyModifyFilterForRelatedTable(builder));
35
36
  }
36
37
 
37
38
  return super.onBuild(builder);
@@ -70,6 +71,10 @@ const ManyToManyModifyMixin = (Operation) => {
70
71
  return builder.whereInComposite(idRefs, subquery);
71
72
  }
72
73
 
74
+ // Modifies the join rows of all related rows that match the filters. If the filters
75
+ // reference join table columns, this also modifies join rows that don't match them
76
+ // but share a related row with one that does (#1853). The dialect specific mixins
77
+ // override this to only modify the join rows that match the filters themselves.
73
78
  applyModifyFilterForJoinTable(builder) {
74
79
  const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
75
80
  const joinTableRelatedRefs = this.relation.joinTableRelatedProp.refs(builder);
@@ -85,10 +90,19 @@ const ManyToManyModifyMixin = (Operation) => {
85
90
  }
86
91
 
87
92
  toFindOperation() {
88
- return new ManyToManyFindOperation('find', {
93
+ const opt = {
89
94
  relation: this.relation,
90
95
  owner: this.owner,
91
- });
96
+ };
97
+
98
+ if (this.modifyMainQuery && this.modifyFilterSubquery) {
99
+ // `onBuild` has already moved the main query's filters into the
100
+ // subquery and removed them. Apply them to the find query too.
101
+ // See issue #2266.
102
+ return new ManyToManyModifyFindOperation('find', { ...opt, modifyOperation: this });
103
+ } else {
104
+ return new ManyToManyFindOperation('find', opt);
105
+ }
92
106
  }
93
107
 
94
108
  clone() {
@@ -99,6 +113,15 @@ const ManyToManyModifyMixin = (Operation) => {
99
113
  };
100
114
  };
101
115
 
116
+ class ManyToManyModifyFindOperation extends ManyToManyFindOperation {
117
+ onBuild(builder) {
118
+ super.onBuild(builder);
119
+ addWhereGroup(builder, () =>
120
+ this.opt.modifyOperation.applyModifyFilterForRelatedTable(builder),
121
+ );
122
+ }
123
+ }
124
+
102
125
  module.exports = {
103
126
  ManyToManyModifyMixin,
104
127
  };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const { ManyToManyModifyMixin } = require('./ManyToManyModifyMixin');
4
+ const { isObject } = require('../../utils/objectUtils');
5
+
6
+ // We need to override this mixin for mysql because mysql doesn't allow a
7
+ // statement that modifies the join table to read the related table in a
8
+ // subquery, if a trigger on the join table modifies the related table:
9
+ //
10
+ // ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG
11
+ //
12
+ // So the join rows that match the filters are fetched in a separate query
13
+ // before the main query, which then only modifies these rows (#1853). Mysql
14
+ // has no row ids and the join table's primary key isn't known, so the rows are
15
+ // identified by the values of all their columns.
16
+ //
17
+ // https://stackoverflow.com/a/2314264/3729316
18
+ const ManyToManyMySqlModifyMixin = (Operation) => {
19
+ return class extends ManyToManyModifyMixin(Operation) {
20
+ applyModifyFilterForJoinTable(builder) {
21
+ const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
22
+ const tableRef = builder.tableRefFor(this.relation.getJoinModelClass(builder));
23
+
24
+ const ownerValues = this.owner.getProps(this.relation);
25
+ const subquery = this.modifyFilterSubquery.clone().select(`${tableRef}.*`);
26
+
27
+ return builder
28
+ .runBefore(async (_, builder) => {
29
+ // Fetch the raw rows: no models, so that no hooks run for them.
30
+ const rows = await subquery.toKnexQuery();
31
+
32
+ if (rows.length) {
33
+ builder.where((builder) => {
34
+ for (const row of rows) {
35
+ builder.orWhere((builder) => {
36
+ for (const [column, value] of Object.entries(row)) {
37
+ builder.whereRaw('?? <=> ?', [
38
+ `${tableRef}.${column}`,
39
+ toComparableValue(value),
40
+ ]);
41
+ }
42
+ });
43
+ }
44
+ });
45
+ } else {
46
+ // Still execute the query so that it returns the expected
47
+ // result for the dialect, e.g. a count of 0 for deletes.
48
+ builder.where(false);
49
+ }
50
+ })
51
+ .whereInComposite(joinTableOwnerRefs, ownerValues);
52
+ }
53
+ };
54
+ };
55
+
56
+ // Some drivers return JSON columns parsed. Mysql parses strings that are
57
+ // compared to JSON columns, so the stringified value matches them again.
58
+ function toComparableValue(value) {
59
+ return isObject(value) && !(value instanceof Date) && !Buffer.isBuffer(value)
60
+ ? JSON.stringify(value)
61
+ : value;
62
+ }
63
+
64
+ module.exports = {
65
+ ManyToManyMySqlModifyMixin,
66
+ };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const { ManyToManyModifyMixin } = require('./ManyToManyModifyMixin');
4
+
5
+ // Like `ManyToManySqliteModifyMixin`, this identifies the join rows to modify
6
+ // by their physical row id, so that only the join rows that match the filters
7
+ // are modified (#1853). `tableoid` makes `ctid` unique for partitioned tables.
8
+ const ManyToManyPostgresModifyMixin = (Operation) => {
9
+ return class extends ManyToManyModifyMixin(Operation) {
10
+ applyModifyFilterForJoinTable(builder) {
11
+ const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
12
+ const tableRef = builder.tableRefFor(this.relation.getJoinModelClass(builder));
13
+ const rowIdRefs = [`${tableRef}.tableoid`, `${tableRef}.ctid`];
14
+
15
+ const ownerValues = this.owner.getProps(this.relation);
16
+ const subquery = this.modifyFilterSubquery.clone().select(rowIdRefs);
17
+
18
+ return builder
19
+ .whereInComposite(rowIdRefs, subquery)
20
+ .whereInComposite(joinTableOwnerRefs, ownerValues);
21
+ }
22
+ };
23
+ };
24
+
25
+ module.exports = {
26
+ ManyToManyPostgresModifyMixin,
27
+ };