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
|
@@ -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(
|
|
171
|
-
peg$c9 = '
|
|
172
|
-
peg$c10 = peg$literalExpectation('
|
|
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
|
-
|
|
455
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
511
|
+
peg$fail(peg$c10);
|
|
489
512
|
}
|
|
490
513
|
}
|
|
491
514
|
if (s5 !== peg$FAILED) {
|
|
492
|
-
|
|
493
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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:([^"])
|
|
90
|
+
chars:([^"])* { return chars.join(""); }
|
|
90
91
|
|
|
91
92
|
stringWithoutSingleQuotes =
|
|
92
|
-
chars:([^'])
|
|
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
|
-
|
|
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
|
-
.
|
|
35
|
-
|
|
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
|
-
.
|
|
23
|
-
|
|
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
|
-
|
|
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
|
+
};
|