Skip to content

Commit b59ad40

Browse files
authored
Merge pull request #1428 from ovos/m2m-join-table-modify
filters/modify on join table of m2m relations
2 parents 373a1b5 + cf8f4d9 commit b59ad40

File tree

7 files changed

+119
-12
lines changed

7 files changed

+119
-12
lines changed

‎doc/api/types/README.md‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@ This page contains the documentation of all other types and classes than [Model]
3030

3131
## `type` RelationThrough
3232

33-
| Property | Type | Description |
34-
| ------------ | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35-
| from | string<br>[ReferenceBuilder](/api/objection/#ref)<br>Array | The column that is joined to `from` property of the `RelationJoin`. For example `Person_movies.actorId` where `Person_movies` is the join table. Composite key can be specified using an array of columns e.g. `['persons_movies.a', 'persons_movies.b']`. You can join nested json fields using the [ref](/api/objection/#ref) helper. |
36-
| to | string<br>[ReferenceBuilder](/api/objection/#ref)<br>Array | The column that is joined to `to` property of the `RelationJoin`. For example `Person_movies.movieId` where `Person_movies` is the join table. Composite key can be specified using an array of columns e.g. `['persons_movies.a', 'persons_movies.b']`. You can join nested json fields using the [ref](/api/objection/#ref) helper. |
37-
| modelClass | string<br>ModelClass | If you have a model class for the join table, you should specify it here. This is optional so you don't need to create a model class if you don't want to. |
38-
| extra | string<br>string[]<br>Object | Join table columns listed here are automatically joined to the related objects when they are fetched and automatically written to the join table instead of the related table on insert and update. The values can be aliased by providing an object `{propertyName: 'columnName', otherPropertyName: 'otherColumnName'} instead of array` See [this recipe](/recipes/extra-properties.html) for more info. |
39-
| beforeInsert | function([Model](/api/model/),&nbsp;[QueryContext](/api/query-builder/other-methods.html#context)) | Optional insert hook that is called for each inserted join table model instance. This function can be async. |
33+
| Property | Type | Description |
34+
| ------------ | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35+
| from | string<br>[ReferenceBuilder](/api/objection/#ref)<br>Array | The column that is joined to `from` property of the `RelationJoin`. For example `Person_movies.actorId` where `Person_movies` is the join table. Composite key can be specified using an array of columns e.g. `['persons_movies.a', 'persons_movies.b']`. You can join nested json fields using the [ref](/api/objection/#ref) helper. |
36+
| to | string<br>[ReferenceBuilder](/api/objection/#ref)<br>Array | The column that is joined to `to` property of the `RelationJoin`. For example `Person_movies.movieId` where `Person_movies` is the join table. Composite key can be specified using an array of columns e.g. `['persons_movies.a', 'persons_movies.b']`. You can join nested json fields using the [ref](/api/objection/#ref) helper. |
37+
| modelClass | string<br>ModelClass | If you have a model class for the join table, you should specify it here. This is optional so you don't need to create a model class if you don't want to. |
38+
| extra | string<br>string[]<br>Object | Join table columns listed here are automatically joined to the related objects when they are fetched and automatically written to the join table instead of the related table on insert and update. The values can be aliased by providing an object `{propertyName: 'columnName', otherPropertyName: 'otherColumnName'} instead of array` See [this recipe](/recipes/extra-properties.html) for more info. |
39+
| modify | function([QueryBuilder](/api/query-builder/))<br>string<br>object | Optional modifier for the join table query. If specified as a function, it will be called each time before fetching the relation. If specified as a string, modifier with specified name will be applied each time when fetching the relation. If specified as an object, it will be used as an additional query parameter - e. g. passing {name: 'Jenny'} would additionally narrow fetched rows to the ones with the name 'Jenny'. |
40+
| filter | function([QueryBuilder](/api/query-builder/))<br>string<br>object | Alias for modify. |
41+
| beforeInsert | function([Model](/api/model/),&nbsp;[QueryContext](/api/query-builder/other-methods.html#context)) | Optional insert hook that is called for each inserted join table model instance. This function can be async. |
4042

4143
## `type` ModelOptions
4244

‎lib/queryBuilder/QueryBuilder.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ class QueryBuilder extends QueryBuilderBase {
325325
);
326326
} else if (op.constructor === FromOperation) {
327327
return op.table === tableName;
328-
} else if (op.name === 'as' || op.is(FindOperation)) {
328+
} else if (op.name === 'as' || op.is(FindOperation) || op.is(OnErrorOperation)) {
329329
return true;
330330
} else {
331331
return false;

‎lib/relations/manyToMany/ManyToManyRelation.js‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const { inheritModel } = require('../../model/inheritModel');
99
const { resolveModel } = require('../../utils/resolveModel');
1010
const { mapAfterAllReturn } = require('../../utils/promiseUtils');
1111
const { isFunction, isObject, isString } = require('../../utils/objectUtils');
12+
const { createModifier } = require('../../utils/createModifier');
1213

1314
const { ManyToManyFindOperation } = require('./find/ManyToManyFindOperation');
1415
const { ManyToManyInsertOperation } = require('./insert/ManyToManyInsertOperation');
@@ -49,10 +50,12 @@ class ManyToManyRelation extends Relation {
4950
ctx = resolveJoinModelClassIfDefined(ctx);
5051
ctx = createJoinProperties(ctx);
5152
ctx = parseExtras(ctx);
53+
ctx = parseModify(ctx);
5254
ctx = parseBeforeInsert(ctx);
5355
ctx = finalizeJoinModelClass(ctx);
5456

5557
this.joinTableExtras = ctx.joinTableExtras;
58+
this.joinTableModify = ctx.joinTableModify;
5659
this.joinTableModelClass = ctx.joinTableModelClass;
5760
this.joinTableOwnerProp = ctx.joinTableOwnerProp;
5861
this.joinTableRelatedProp = ctx.joinTableRelatedProp;
@@ -70,7 +73,16 @@ class ManyToManyRelation extends Relation {
7073
const joinTable = builder.tableNameFor(this.joinTable);
7174
const joinTableAlias = builder.tableRefFor(this.joinTable);
7275

73-
builder.join(aliasedTableName(joinTable, joinTableAlias), (join) => {
76+
let joinTableSelect = this.joinTableModelClass
77+
.query()
78+
.childQueryOf(builder)
79+
.modify(this.joinTableModify)
80+
.as(joinTableAlias);
81+
if (joinTableSelect.isSelectAll()) {
82+
joinTableSelect = aliasedTableName(joinTable, joinTableAlias);
83+
}
84+
85+
builder.join(joinTableSelect, (join) => {
7486
this.relatedProp.forEach((i) => {
7587
const relatedRef = this.relatedProp.ref(builder, i);
7688
const joinTableRelatedRef = this.joinTableRelatedProp.ref(builder, i);
@@ -101,7 +113,16 @@ class ManyToManyRelation extends Relation {
101113
relatedJoinSelect = aliasedTableName(relatedTable, relatedTableAlias);
102114
}
103115

104-
return builder[joinOperation](aliasedTableName(this.joinTable, joinTableAlias), (join) => {
116+
let joinTableSelect = this.joinTableModelClass
117+
.query()
118+
.childQueryOf(builder)
119+
.modify(this.joinTableModify)
120+
.as(joinTableAlias);
121+
if (joinTableSelect.isSelectAll()) {
122+
joinTableSelect = aliasedTableName(this.joinTable, joinTableAlias);
123+
}
124+
125+
return builder[joinOperation](joinTableSelect, (join) => {
105126
const ownerProp = this.ownerProp;
106127
const joinTableOwnerProp = this.joinTableOwnerProp;
107128

@@ -442,6 +463,19 @@ function parseExtras(ctx) {
442463
return Object.assign(ctx, { joinTableExtras });
443464
}
444465

466+
function parseModify(ctx) {
467+
const mapping = ctx.mapping.join.through;
468+
const modifier = mapping.modify || mapping.filter;
469+
const joinTableModify =
470+
modifier &&
471+
createModifier({
472+
modifier,
473+
modelClass: ctx.relatedModelClass,
474+
});
475+
476+
return Object.assign(ctx, { joinTableModify });
477+
}
478+
445479
function parseBeforeInsert(ctx) {
446480
let joinTableBeforeInsert;
447481

‎lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class ManyToManyUnrelateOperationBase extends UnrelateOperation {
1010
.childQueryOf(builder)
1111
.delete();
1212

13-
return this.applyModifyFilterForJoinTable(unrelateQuery);
13+
return this.applyModifyFilterForJoinTable(unrelateQuery).modify(this.relation.joinTableModify);
1414
}
1515

1616
/* istanbul ignore next */

‎lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
4444
.childQueryOf(builder)
4545
.patch(this.joinTablePatch);
4646

47-
await this.applyModifyFilterForJoinTable(joinTableUpdateQuery);
47+
await this.applyModifyFilterForJoinTable(joinTableUpdateQuery).modify(
48+
this.relation.joinTableModify
49+
);
4850
return result;
4951
} else {
5052
return result;

‎tests/unit/relations/ManyToManyRelation.js‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,32 @@ describe('ManyToManyRelation', () => {
435435
});
436436
});
437437

438+
it('should accept a modifier in join.through', () => {
439+
// modifier defined in join.through.modify
440+
let modifier = (builder) => builder.where('someColumn', 'foo');
441+
createJoinThroughModifiedRelation(modifier);
442+
443+
expect(relation.joinTableModify).to.be.a(Function);
444+
445+
// test also join.through.filter
446+
let relationFilter = new ManyToManyRelation('testRelation', OwnerModel);
447+
relationFilter.setMapping({
448+
relation: ManyToManyRelation,
449+
modelClass: RelatedModel,
450+
join: {
451+
from: 'OwnerModel.id',
452+
through: {
453+
from: 'JoinModel.ownerId',
454+
to: 'JoinModel.relatedId',
455+
filter: modifier,
456+
},
457+
to: 'RelatedModel.ownerId',
458+
},
459+
});
460+
461+
expect(relationFilter.joinTableModify).to.be.a(Function);
462+
});
463+
438464
describe('find', () => {
439465
it('should generate a find query', () => {
440466
let owner = OwnerModel.fromJson({ oid: 666 });
@@ -732,6 +758,30 @@ describe('ManyToManyRelation', () => {
732758
);
733759
});
734760
});
761+
762+
it('should support modifiers in join.through', () => {
763+
createJoinThroughModifiedRelation((builder) => builder.where('someColumn', 'foo'));
764+
765+
let owner = OwnerModel.fromJson({ oid: 666 });
766+
767+
let builder = QueryBuilder.forClass(RelatedModel).findOperationFactory(function () {
768+
return relation.find(this, RelationOwner.create(owner));
769+
});
770+
771+
return builder.then(() => {
772+
expect(executedQueries).to.have.length(1);
773+
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
774+
expect(executedQueries[0]).to.equal(
775+
[
776+
'select "RelatedModel".*, "JoinModel"."ownerId" as "objectiontmpjoin0"',
777+
'from "RelatedModel"',
778+
'inner join (select "JoinModel".* from "JoinModel" where "someColumn" = \'foo\') as "JoinModel"',
779+
'on "RelatedModel"."rid" = "JoinModel"."relatedId"',
780+
'where "JoinModel"."ownerId" in (666)',
781+
].join(' ')
782+
);
783+
});
784+
});
735785
});
736786

737787
describe('insert', () => {
@@ -1561,6 +1611,23 @@ describe('ManyToManyRelation', () => {
15611611
},
15621612
});
15631613
}
1614+
1615+
function createJoinThroughModifiedRelation(modify) {
1616+
relation = new ManyToManyRelation('nameOfOurRelation', OwnerModel);
1617+
relation.setMapping({
1618+
modelClass: RelatedModel,
1619+
relation: ManyToManyRelation,
1620+
join: {
1621+
from: 'OwnerModel.oid',
1622+
through: {
1623+
from: 'JoinModel.ownerId',
1624+
to: 'JoinModel.relatedId',
1625+
modify: modify,
1626+
},
1627+
to: 'RelatedModel.rid',
1628+
},
1629+
});
1630+
}
15641631
});
15651632

15661633
function isSubclassOf(Constructor, SuperConstructor) {

‎typings/objection/index.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,8 @@ declare namespace Objection {
12531253
to: RelationMappingColumnRef;
12541254
extra?: string | string[] | Record<string, string>;
12551255
modelClass?: ModelClassSpecifier;
1256+
modify?: Modifier<QueryBuilderType<M>>;
1257+
filter?: Modifier<QueryBuilderType<M>>;
12561258
beforeInsert?: RelationMappingHook<M>;
12571259
}
12581260

0 commit comments

Comments
 (0)