From cb997cfdea1984d351ec3f544c72dca48a1cf1c3 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Thu, 11 Jan 2024 15:10:09 +0100 Subject: [PATCH] feat(query-typeorm): Support virtual columns in sorting Fixes #67 --- .../typeorm/e2e/todo-item.resolver.spec.ts | 94 ++++++++----------- .../src/query/filter-query.builder.ts | 15 ++- 2 files changed, 53 insertions(+), 56 deletions(-) diff --git a/examples/typeorm/e2e/todo-item.resolver.spec.ts b/examples/typeorm/e2e/todo-item.resolver.spec.ts index 551a9fdbb..69eaaf7bc 100644 --- a/examples/typeorm/e2e/todo-item.resolver.spec.ts +++ b/examples/typeorm/e2e/todo-item.resolver.spec.ts @@ -456,60 +456,46 @@ describe('TodoItemResolver (typeorm - e2e)', () => { expect(edges).toHaveLength(1) })) - // it(`should allow sorting on a virtual column`, () => - // request(app.getHttpServer()) - // .post('/graphql') - // .send({ - // operationName: null, - // variables: {}, - // query: `{ - // todoItems(sorting: [{field: subTasksCount, direction: DESC}]) { - // ${pageInfoField} - // ${edgeNodes(todoItemFields)} - // totalCount - // } - // }` - // }) - // .expect(200) - // .then(({ body }) => { - // const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.todoItems - // expect(pageInfo).toEqual({ - // endCursor: 'eyJ0eXBlIjoia2V5c2V0IiwiZmllbGRzIjpbeyJmaWVsZCI6ImlkIiwidmFsdWUiOjF9XX0=', - // hasNextPage: false, - // hasPreviousPage: false, - // startCursor: 'eyJ0eXBlIjoia2V5c2V0IiwiZmllbGRzIjpbeyJmaWVsZCI6ImlkIiwidmFsdWUiOjV9XX0=' - // }) - // expect(totalCount).toBe(5) - // expect(edges).toHaveLength(5) - // expect(edges.map((e) => e.node)).toEqual([ - // { - // id: '5', - // title: 'How to create item With Sub Tasks', - // completed: false, - // description: null, - // age: expect.any(Number), - // subTasksCount: 3 - // }, - // { - // id: '4', - // title: 'Add Todo Item Resolver', - // completed: false, - // description: null, - // age: expect.any(Number), - // subTasksCount: 3 - // }, - // { - // id: '3', - // title: 'Create Entity Service', - // completed: false, - // description: null, - // age: expect.any(Number), - // subTasksCount: 3 - // }, - // { id: '2', title: 'Create Entity', completed: false, description: null, age: expect.any(Number), subTasksCount: 3 }, - // { id: '1', title: 'Create Nest App', completed: true, description: null, age: expect.any(Number), subTasksCount: 3 } - // ]) - // })) + it(`should allow sorting on a virtual column`, () => + request(app.getHttpServer()) + .post('/graphql') + .send({ + operationName: null, + variables: {}, + // language=graphql + query: `{ + todoItems(sorting: [{field: subTasksCount, direction: ASC}], paging: {first: 2}) { + ${pageInfoField} + ${edgeNodes(todoItemFields)} + totalCount + } + }` + }) + .expect(200) + .then(({ body }) => { + const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.todoItems + expect(pageInfo).toEqual({ + endCursor: + 'eyJ0eXBlIjoia2V5c2V0IiwiZmllbGRzIjpbeyJmaWVsZCI6InN1YlRhc2tzQ291bnQiLCJ2YWx1ZSI6M30seyJmaWVsZCI6ImlkIiwidmFsdWUiOjF9XX0=', + hasNextPage: true, + hasPreviousPage: false, + startCursor: + 'eyJ0eXBlIjoia2V5c2V0IiwiZmllbGRzIjpbeyJmaWVsZCI6InN1YlRhc2tzQ291bnQiLCJ2YWx1ZSI6Mn0seyJmaWVsZCI6ImlkIiwidmFsdWUiOjV9XX0=' + }) + expect(totalCount).toBe(5) + expect(edges).toHaveLength(2) + expect(edges.map((e) => e.node)).toEqual([ + { + id: '5', + title: 'How to create item With Sub Tasks', + completed: false, + description: null, + age: expect.any(Number), + subTasksCount: 2 + }, + { id: '1', title: 'Create Nest App', completed: true, description: null, age: expect.any(Number), subTasksCount: 3 } + ]) + })) describe('paging', () => { it(`should allow paging with the 'first' field`, () => diff --git a/packages/query-typeorm/src/query/filter-query.builder.ts b/packages/query-typeorm/src/query/filter-query.builder.ts index 2aa842b83..9ac5ab234 100644 --- a/packages/query-typeorm/src/query/filter-query.builder.ts +++ b/packages/query-typeorm/src/query/filter-query.builder.ts @@ -79,13 +79,19 @@ export interface NestedRelationsAliased { * Class that will convert a Query into a `typeorm` Query Builder. */ export class FilterQueryBuilder { + private readonly virtualColumns: string[] = [] + constructor( readonly repo: Repository, readonly whereBuilder: WhereBuilder = new WhereBuilder( new SQLComparisonBuilder(SQLComparisonBuilder.DEFAULT_COMPARISON_MAP, repo) ), readonly aggregateBuilder: AggregateBuilder = new AggregateBuilder(repo) - ) {} + ) { + this.virtualColumns = repo.metadata.columns + .filter(({ isVirtualProperty }) => isVirtualProperty) + .map(({ propertyName }) => propertyName) + } /** * Create a `typeorm` SelectQueryBuilder with `WHERE`, `ORDER BY` and `LIMIT/OFFSET` clauses. @@ -212,7 +218,12 @@ export class FilterQueryBuilder { } return sorts.reduce((prevQb, { field, direction, nulls }) => { - const col = alias ? `${alias}.${field as string}` : `${field as string}` + let col = alias ? `${alias}.${field as string}` : `${field as string}` + + if (this.virtualColumns.includes(field as string)) { + col = prevQb.escape(alias ? `${alias}_${field as string}` : `${field as string}`) + } + return prevQb.addOrderBy(col, direction, nulls) }, qb) }