Skip to content

Commit

Permalink
feat(typeorm): Add support for filtering on relations
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-martin committed Jun 30, 2020
1 parent 92a51c1 commit aa8788c
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 49 deletions.
74 changes: 40 additions & 34 deletions packages/query-typeorm/__tests__/query/filter-query.builder.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { anything, instance, mock, verify, when } from 'ts-mockito';
import { anything, instance, mock, verify, when, deepEqual } from 'ts-mockito';
import { QueryBuilder, WhereExpression } from 'typeorm';
import { Class, Filter, Query, SortDirection, SortNulls } from '@nestjs-query/core';
import { closeTestConnection, createTestConnection, getTestConnection } from '../__fixtures__/connection.fixture';
Expand Down Expand Up @@ -82,14 +82,14 @@ describe('FilterQueryBuilder', (): void => {
it('should not call whereBuilder#build', () => {
const mockWhereBuilder: WhereBuilder<TestEntity> = mock(WhereBuilder);
assertSelectSQL({}, instance(mockWhereBuilder), '', []);
verify(mockWhereBuilder.build(anything(), anything(), 'TestEntity')).never();
verify(mockWhereBuilder.build(anything(), anything(), [], 'TestEntity')).never();
});

it('should call whereBuilder#build if there is a filter', () => {
const mockWhereBuilder: WhereBuilder<TestEntity> = mock(WhereBuilder);
const query = { filter: { stringType: { eq: 'foo' } } };
when(mockWhereBuilder.build(anything(), query.filter, 'TestEntity')).thenCall(
(where: WhereExpression, field: Filter<TestEntity>, alias: string) => {
when(mockWhereBuilder.build(anything(), query.filter, deepEqual([]), 'TestEntity')).thenCall(
(where: WhereExpression, field: Filter<TestEntity>, relationNames: string[], alias: string) => {
return where.andWhere(`${alias}.stringType = 'foo'`);
},
);
Expand All @@ -101,7 +101,7 @@ describe('FilterQueryBuilder', (): void => {
it('should apply empty paging args', () => {
const mockWhereBuilder: WhereBuilder<TestEntity> = mock(WhereBuilder);
assertSelectSQL({}, instance(mockWhereBuilder), '', []);
verify(mockWhereBuilder.build(anything(), anything(), 'TestEntity')).never();
verify(mockWhereBuilder.build(anything(), anything(), deepEqual([]), 'TestEntity')).never();
});

it('should apply paging args going forward', () => {
Expand All @@ -117,7 +117,7 @@ describe('FilterQueryBuilder', (): void => {
' LIMIT 10 OFFSET 11',
[],
);
verify(mockWhereBuilder.build(anything(), anything(), 'TestEntity')).never();
verify(mockWhereBuilder.build(anything(), anything(), deepEqual([]), 'TestEntity')).never();
});

it('should apply paging args going backward', () => {
Expand All @@ -133,7 +133,7 @@ describe('FilterQueryBuilder', (): void => {
' LIMIT 10 OFFSET 10',
[],
);
verify(mockWhereBuilder.build(anything(), anything(), 'TestEntity')).never();
verify(mockWhereBuilder.build(anything(), anything(), [], 'TestEntity')).never();
});
});

Expand All @@ -148,7 +148,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "TestEntity"."number_type" ASC',
[],
);
verify(mockWhereBuilder.build(anything(), anything(), 'TestEntity')).never();
verify(mockWhereBuilder.build(anything(), anything(), [], 'TestEntity')).never();
});

it('should apply ASC NULLS_FIRST sorting', () => {
Expand All @@ -161,7 +161,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "TestEntity"."number_type" ASC NULLS FIRST',
[],
);
verify(mockWhereBuilder.build(anything(), anything(), 'TestEntity')).never();
verify(mockWhereBuilder.build(anything(), anything(), [], 'TestEntity')).never();
});

it('should apply ASC NULLS_LAST sorting', () => {
Expand All @@ -174,7 +174,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "TestEntity"."number_type" ASC NULLS LAST',
[],
);
verify(mockWhereBuilder.build(anything(), anything(), 'TestEntity')).never();
verify(mockWhereBuilder.build(anything(), anything(), [], 'TestEntity')).never();
});

it('should apply DESC sorting', () => {
Expand All @@ -187,7 +187,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "TestEntity"."number_type" DESC',
[],
);
verify(mockWhereBuilder.build(anything(), anything(), 'TestEntity')).never();
verify(mockWhereBuilder.build(anything(), anything(), [], 'TestEntity')).never();
});

it('should apply DESC NULLS_FIRST sorting', () => {
Expand All @@ -212,7 +212,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "TestEntity"."number_type" DESC NULLS LAST',
[],
);
verify(mockWhereBuilder.build(anything(), anything(), 'TestEntity')).never();
verify(mockWhereBuilder.build(anything(), anything(), [], 'TestEntity')).never();
});

it('should apply multiple sorts', () => {
Expand All @@ -230,7 +230,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "TestEntity"."number_type" ASC, "TestEntity"."bool_type" DESC, "TestEntity"."string_type" ASC NULLS FIRST, "TestEntity"."date_type" DESC NULLS LAST',
[],
);
verify(mockWhereBuilder.build(anything(), anything(), 'TestEntity')).never();
verify(mockWhereBuilder.build(anything(), anything(), [], 'TestEntity')).never();
});
});
});
Expand All @@ -240,9 +240,11 @@ describe('FilterQueryBuilder', (): void => {
it('should call whereBuilder#build if there is a filter', () => {
const mockWhereBuilder: WhereBuilder<TestEntity> = mock(WhereBuilder);
const query = { filter: { stringType: { eq: 'foo' } } };
when(mockWhereBuilder.build(anything(), query.filter, undefined)).thenCall((where: WhereExpression) => {
return where.andWhere(`stringType = 'foo'`);
});
when(mockWhereBuilder.build(anything(), query.filter, deepEqual([]), undefined)).thenCall(
(where: WhereExpression) => {
return where.andWhere(`stringType = 'foo'`);
},
);
assertUpdateSQL(query, instance(mockWhereBuilder), ` WHERE "string_type" = 'foo'`, []);
});
});
Expand All @@ -260,7 +262,7 @@ describe('FilterQueryBuilder', (): void => {
'',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});
});

Expand All @@ -275,7 +277,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "number_type" ASC',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});

it('should apply ASC NULLS_FIRST sorting', () => {
Expand All @@ -288,7 +290,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "number_type" ASC NULLS FIRST',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});

it('should apply ASC NULLS_LAST sorting', () => {
Expand All @@ -301,7 +303,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "number_type" ASC NULLS LAST',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});

it('should apply DESC sorting', () => {
Expand All @@ -314,7 +316,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "number_type" DESC',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});

it('should apply DESC NULLS_FIRST sorting', () => {
Expand All @@ -327,7 +329,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "number_type" DESC NULLS FIRST',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});

it('should apply DESC NULLS_LAST sorting', () => {
Expand All @@ -340,7 +342,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "number_type" DESC NULLS LAST',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});

it('should apply multiple sorts', () => {
Expand All @@ -358,7 +360,7 @@ describe('FilterQueryBuilder', (): void => {
' ORDER BY "number_type" ASC, "bool_type" DESC, "string_type" ASC NULLS FIRST, "date_type" DESC NULLS LAST',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});
});
});
Expand All @@ -368,9 +370,11 @@ describe('FilterQueryBuilder', (): void => {
it('should call whereBuilder#build if there is a filter', () => {
const mockWhereBuilder: WhereBuilder<TestEntity> = mock(WhereBuilder);
const query = { filter: { stringType: { eq: 'foo' } } };
when(mockWhereBuilder.build(anything(), query.filter, undefined)).thenCall((where: WhereExpression) => {
return where.andWhere(`stringType = 'foo'`);
});
when(mockWhereBuilder.build(anything(), query.filter, deepEqual([]), undefined)).thenCall(
(where: WhereExpression) => {
return where.andWhere(`stringType = 'foo'`);
},
);
assertDeleteSQL(query, instance(mockWhereBuilder), ` WHERE "string_type" = 'foo'`, []);
});
});
Expand All @@ -388,7 +392,7 @@ describe('FilterQueryBuilder', (): void => {
'',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});
});

Expand All @@ -408,7 +412,7 @@ describe('FilterQueryBuilder', (): void => {
'',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});
});
});
Expand All @@ -418,9 +422,11 @@ describe('FilterQueryBuilder', (): void => {
it('should call whereBuilder#build if there is a filter', () => {
const mockWhereBuilder: WhereBuilder<TestSoftDeleteEntity> = mock(WhereBuilder);
const query = { filter: { stringType: { eq: 'foo' } } };
when(mockWhereBuilder.build(anything(), query.filter, undefined)).thenCall((where: WhereExpression) => {
return where.andWhere(`stringType = 'foo'`);
});
when(mockWhereBuilder.build(anything(), query.filter, deepEqual([]), undefined)).thenCall(
(where: WhereExpression) => {
return where.andWhere(`stringType = 'foo'`);
},
);
assertSoftDeleteSQL(query, instance(mockWhereBuilder), ` WHERE "string_type" = 'foo'`, []);
});
});
Expand All @@ -438,7 +444,7 @@ describe('FilterQueryBuilder', (): void => {
'',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});
});

Expand All @@ -456,7 +462,7 @@ describe('FilterQueryBuilder', (): void => {
'',
[],
);
verify(mockWhereBuilder.build(anything(), anything())).never();
verify(mockWhereBuilder.build(anything(), anything(), anything())).never();
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('WhereBuilder', (): void => {
const createWhereBuilder = () => new WhereBuilder<TestEntity>();

const assertSQL = (filter: Filter<TestEntity>, expectedSql: string, expectedArgs: any[]): void => {
const selectQueryBuilder = createWhereBuilder().build(getQueryBuilder(), filter, 'TestEntity');
const selectQueryBuilder = createWhereBuilder().build(getQueryBuilder(), filter, [], 'TestEntity');
const [sql, params] = selectQueryBuilder.getQueryAndParameters();
expect(sql).toEqual(`${baseQuery}${expectedSql}`);
expect(params).toEqual(expectedArgs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,58 @@ describe('TypeOrmQueryService', (): void => {
const queryResult = await queryService.query({ filter: { stringType: { eq: 'foo1' } } });
return expect(queryResult).toEqual([TEST_ENTITIES[0]]);
});

describe('filter on relations', () => {
describe('oneToOne', () => {
it('should allow filtering on a one to one relation', async () => {
const entity = TEST_ENTITIES[0];
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.query({
filter: {
oneTestRelation: {
testRelationPk: {
in: [`test-relations-${entity.testEntityPk}-1`, `test-relations-${entity.testEntityPk}-3`],
},
},
},
});
expect(queryResult).toEqual([entity]);
});
});

describe('manyToOne', () => {
it('should allow filtering on a many to one relation', async () => {
const queryService = moduleRef.get(TestRelationService);
const queryResults = await queryService.query({
filter: {
testEntity: {
testEntityPk: {
in: [TEST_ENTITIES[0].testEntityPk, TEST_ENTITIES[1].testEntityPk],
},
},
},
});
expect(queryResults).toEqual(TEST_RELATIONS.slice(0, 6));
});
});

describe('oneToMany', () => {
it('should allow filtering on a many to one relation', async () => {
const entity = TEST_ENTITIES[0];
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.query({
filter: {
testRelations: {
relationName: {
in: [TEST_RELATIONS[0].relationName, TEST_RELATIONS[1].relationName],
},
},
},
});
expect(queryResult).toEqual([entity]);
});
});
});
});

describe('#count', () => {
Expand All @@ -72,6 +124,52 @@ describe('TypeOrmQueryService', (): void => {
const queryResult = await queryService.count({ stringType: { like: 'foo%' } });
return expect(queryResult).toBe(10);
});

describe('with relations', () => {
describe('oneToOne', () => {
it('should properly count the number pf records with the associated relations', async () => {
const entity = TEST_ENTITIES[0];
const queryService = moduleRef.get(TestEntityService);
const count = await queryService.count({
oneTestRelation: {
testRelationPk: {
in: [`test-relations-${entity.testEntityPk}-1`, `test-relations-${entity.testEntityPk}-3`],
},
},
});
expect(count).toEqual(1);
});
});

describe('manyToOne', () => {
it('set the relation to null', async () => {
const queryService = moduleRef.get(TestRelationService);
const count = await queryService.count({
testEntity: {
testEntityPk: {
in: [TEST_ENTITIES[0].testEntityPk, TEST_ENTITIES[2].testEntityPk],
},
},
});
expect(count).toEqual(6);
});
});

describe('oneToMany', () => {
it('set the relation to null', async () => {
const relation = TEST_RELATIONS[0];
const queryService = moduleRef.get(TestEntityService);
const count = await queryService.count({
testRelations: {
testEntityId: {
in: [relation.testEntityId as string],
},
},
});
expect(count).toEqual(1);
});
});
});
});

describe('#queryRelations', () => {
Expand Down
Loading

0 comments on commit aa8788c

Please sign in to comment.