Skip to content

Commit

Permalink
feat(typeorm): Switch to use unioned queries for relations
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-martin committed Aug 10, 2020
1 parent 1a60b47 commit 327c676
Show file tree
Hide file tree
Showing 6 changed files with 421 additions and 49 deletions.
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

163 changes: 148 additions & 15 deletions packages/query-typeorm/__tests__/services/typeorm-query.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,27 +252,67 @@ describe('TypeOrmQueryService', (): void => {
describe('with one entity', () => {
it('call select and return the result', async () => {
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.queryRelations(TestRelation, 'testRelations', TEST_ENTITIES[0], {
filter: { relationName: { isNot: null } },
});
const queryResult = await queryService.queryRelations(TestRelation, 'testRelations', TEST_ENTITIES[0], {});
return expect(queryResult.map((r) => r.testEntityId)).toEqual([
TEST_ENTITIES[0].testEntityPk,
TEST_ENTITIES[0].testEntityPk,
TEST_ENTITIES[0].testEntityPk,
]);
});

it('should apply a filter', async () => {
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.queryRelations(TestRelation, 'testRelations', TEST_ENTITIES[0], {
filter: { testRelationPk: { notLike: '%-1' } },
});
return expect(queryResult.map((r) => r.testRelationPk)).toEqual([
TEST_RELATIONS[1].testRelationPk,
TEST_RELATIONS[2].testRelationPk,
]);
});

it('should apply a paging', async () => {
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.queryRelations(TestRelation, 'testRelations', TEST_ENTITIES[0], {
paging: { limit: 2, offset: 1 },
});
return expect(queryResult.map((r) => r.testRelationPk)).toEqual([
TEST_RELATIONS[1].testRelationPk,
TEST_RELATIONS[2].testRelationPk,
]);
});
});

describe('with multiple entities', () => {
it('call select and return the result', async () => {
const entities = TEST_ENTITIES.slice(0, 3);
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.queryRelations(TestRelation, 'testRelations', entities, {});

expect(queryResult.size).toBe(3);
entities.forEach((e) => expect(queryResult.get(e)).toHaveLength(3));
});

it('should apply a filter', async () => {
const entities = TEST_ENTITIES.slice(0, 3);
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.queryRelations(TestRelation, 'testRelations', entities, {
filter: { relationName: { isNot: null } },
filter: { testRelationPk: { notLike: '%-1' } },
});

expect(queryResult.size).toBe(3);
entities.forEach((e) => expect(queryResult.get(e)).toHaveLength(3));
entities.forEach((e) => expect(queryResult.get(e)).toHaveLength(2));
});

it('should apply paging', async () => {
const entities = TEST_ENTITIES.slice(0, 3);
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.queryRelations(TestRelation, 'testRelations', entities, {
paging: { limit: 2, offset: 1 },
});

expect(queryResult.size).toBe(3);
entities.forEach((e) => expect(queryResult.get(e)).toHaveLength(2));
});

it('should return an empty array if no results are found.', async () => {
Expand All @@ -282,9 +322,9 @@ describe('TypeOrmQueryService', (): void => {
filter: { relationName: { isNot: null } },
});

expect(queryResult.size).toBe(2);
expect(queryResult.size).toBe(1);
expect(queryResult.get(entities[0])).toHaveLength(3);
expect(queryResult.get(entities[1])).toHaveLength(0);
expect(queryResult.get(entities[1])).toBeUndefined();
});
});
});
Expand All @@ -297,7 +337,7 @@ describe('TypeOrmQueryService', (): void => {
TestRelation,
'testRelations',
TEST_ENTITIES[0],
{ relationName: { isNot: null } },
{},
{ count: ['testRelationPk'] },
);
return expect(aggResult).toEqual({
Expand All @@ -306,6 +346,22 @@ describe('TypeOrmQueryService', (): void => {
},
});
});

it('should apply a filter', async () => {
const queryService = moduleRef.get(TestEntityService);
const aggResult = await queryService.aggregateRelations(
TestRelation,
'testRelations',
TEST_ENTITIES[0],
{ testRelationPk: { notLike: '%-1' } },
{ count: ['testRelationPk'] },
);
return expect(aggResult).toEqual({
count: {
testRelationPk: 2,
},
});
});
});

describe('with multiple entities', () => {
Expand All @@ -316,7 +372,7 @@ describe('TypeOrmQueryService', (): void => {
TestRelation,
'testRelations',
entities,
{ relationName: { isNot: null } },
{},
{
count: ['testRelationPk', 'relationName', 'testEntityId'],
min: ['testRelationPk', 'relationName', 'testEntityId'],
Expand Down Expand Up @@ -391,6 +447,88 @@ describe('TypeOrmQueryService', (): void => {
);
});

it('should apply a filter', async () => {
const entities = TEST_ENTITIES.slice(0, 3);
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.aggregateRelations(
TestRelation,
'testRelations',
entities,
{ testRelationPk: { notLike: '%-1' } },
{
count: ['testRelationPk', 'relationName', 'testEntityId'],
min: ['testRelationPk', 'relationName', 'testEntityId'],
max: ['testRelationPk', 'relationName', 'testEntityId'],
},
);

expect(queryResult.size).toBe(3);
expect(queryResult).toEqual(
new Map([
[
entities[0],
{
count: {
relationName: 2,
testEntityId: 2,
testRelationPk: 2,
},
max: {
relationName: 'foo1-test-relation',
testEntityId: 'test-entity-1',
testRelationPk: 'test-relations-test-entity-1-3',
},
min: {
relationName: 'foo1-test-relation',
testEntityId: 'test-entity-1',
testRelationPk: 'test-relations-test-entity-1-2',
},
},
],
[
entities[1],
{
count: {
relationName: 2,
testEntityId: 2,
testRelationPk: 2,
},
max: {
relationName: 'foo2-test-relation',
testEntityId: 'test-entity-2',
testRelationPk: 'test-relations-test-entity-2-3',
},
min: {
relationName: 'foo2-test-relation',
testEntityId: 'test-entity-2',
testRelationPk: 'test-relations-test-entity-2-2',
},
},
],
[
entities[2],
{
count: {
relationName: 2,
testEntityId: 2,
testRelationPk: 2,
},
max: {
relationName: 'foo3-test-relation',
testEntityId: 'test-entity-3',
testRelationPk: 'test-relations-test-entity-3-3',
},
min: {
relationName: 'foo3-test-relation',
testEntityId: 'test-entity-3',
testRelationPk: 'test-relations-test-entity-3-2',
},
},
],
]),
);
});

it('should return an empty array if no results are found.', async () => {
const entities: TestEntity[] = [TEST_ENTITIES[0], { testEntityPk: 'does-not-exist' } as TestEntity];
const queryService = moduleRef.get(TestEntityService);
Expand Down Expand Up @@ -530,12 +668,7 @@ describe('TypeOrmQueryService', (): void => {
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.findRelation(TestRelation, 'oneTestRelation', entities);

expect(queryResult).toEqual(
new Map([
[entities[0], TEST_RELATIONS[0]],
[entities[1], undefined],
]),
);
expect(queryResult).toEqual(new Map([[entities[0], TEST_RELATIONS[0]]]));
});
});
});
Expand Down
6 changes: 5 additions & 1 deletion packages/query-typeorm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"access": "public"
},
"dependencies": {
"@nestjs-query/core": "0.17.10"
"@nestjs-query/core": "0.17.10",
"lodash.filter": "^4.6.0",
"lodash.omit": "^4.5.0"
},
"peerDependencies": {
"@nestjs/common": "^7.0.0",
Expand All @@ -30,6 +32,8 @@
"@nestjs/common": "7.4.2",
"@nestjs/testing": "7.4.2",
"@nestjs/typeorm": "7.1.0",
"@types/lodash.filter": "^4.6.6",
"@types/lodash.omit": "^4.5.6",
"class-transformer": "0.3.1",
"sqlite3": "5.0.0",
"ts-mockito": "2.6.1",
Expand Down
21 changes: 19 additions & 2 deletions packages/query-typeorm/src/query/aggregate.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ export class AggregateBuilder<Entity> {
return this.convertToAggregateResponse(aggResponse);
}

static getAggregateAliases<Entity>(query: AggregateQuery<Entity>): string[] {
const aggs: [AggregateFuncs, (keyof Entity)[] | undefined][] = [
[AggregateFuncs.COUNT, query.count],
[AggregateFuncs.SUM, query.sum],
[AggregateFuncs.AVG, query.avg],
[AggregateFuncs.MAX, query.max],
[AggregateFuncs.MIN, query.min],
];
return aggs.reduce((cols, [func, fields]) => {
const aliases = (fields ?? []).map((f) => this.getAggregateAlias(func, f));
return [...cols, ...aliases];
}, [] as string[]);
}

static getAggregateAlias<Entity>(func: AggregateFuncs, field: keyof Entity): string {
return `${func}_${field as string}`;
}

static convertToAggregateResponse<Entity>(response: Record<string, unknown>): AggregateResponse<Entity> {
return Object.keys(response).reduce((agg, resultField: string) => {
const matchResult = AGG_REGEXP.exec(resultField);
Expand Down Expand Up @@ -71,8 +89,7 @@ export class AggregateBuilder<Entity> {
}
return fields.map((field) => {
const col = alias ? `${alias}.${field as string}` : (field as string);
const aggAlias = `${func}_${field as string}`;
return [`${func}(${col})`, aggAlias];
return [`${func}(${col})`, AggregateBuilder.getAggregateAlias(func, field)];
});
}
}
Loading

0 comments on commit 327c676

Please sign in to comment.