From 55cb0105a11224db1e61023762f030d5c2dae6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20W=C3=B6lk?= Date: Thu, 8 Apr 2021 22:16:36 +0200 Subject: [PATCH] feat(graphql,#1048): added filter-only option to filterable fields --- documentation/docs/graphql/dtos.mdx | 38 +++++++++++-- examples/basic/e2e/todo-item.resolver.spec.ts | 54 +++++++++++++++++++ .../basic/src/todo-item/dto/todo-item.dto.ts | 4 +- ...ts => filterable-fields.decorator.spec.ts} | 9 ++++ .../decorators/filterable-field.decorator.ts | 6 +++ 5 files changed, 106 insertions(+), 5 deletions(-) rename packages/query-graphql/__tests__/decorators/{fitlerable-fields.decorator.spec.ts => filterable-fields.decorator.spec.ts} (92%) diff --git a/documentation/docs/graphql/dtos.mdx b/documentation/docs/graphql/dtos.mdx index 5edb4f6f3..5a42d47fc 100644 --- a/documentation/docs/graphql/dtos.mdx +++ b/documentation/docs/graphql/dtos.mdx @@ -24,6 +24,8 @@ In addition to the normal field options you can also specify the following optio * `filterRequired` - When set to `true` the field will be required whenever a `filter` is used. The `filter` requirement applies to all `read`, `update`, and `delete` endpoints that use a `filter`. * The `filterRequired` option is useful when your entity has an index that requires a subset of fields to be used to provide certain level of query performance. * **NOTE**: When a field is a required in a filter the default `filter` option is ignored. +* `filterOnly`- When set to `true`, the field will only appear as `filter` but isn't included as field inside the `ObjectType`. + * This option is useful if you want to filter on foreign keys without resolving the relation but you don't want to have the foreign key show up as field in your query type for the DTO. This might be especially useful for [federated relations](./federation.mdx#reference-decorator) ### Example @@ -110,7 +112,37 @@ export class TodoItemDTO { @Field(() => GraphQLISODateTime) updated!: Date; } +``` + +### Example - filterOnly + +In the following example the `filterOnly` option is applied to the `assigneeId` field, which makes a query filerable by the id of an assignd user but won't return the `assigneeId` as field. + +```ts title="todo-item.dto.ts" +import { FilterableField } from '@nestjs-query/query-graphql'; +import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql'; +@ObjectType('TodoItem') +@Relation('assignee', () => UserDTO) +export class TodoItemDTO { + @FilterableField(() => ID) + id!: string; + + @FilterableField() + title!: string; + + @FilterableField() + completed!: boolean; + + @FilterableField({ filterOnly: true }) + assigneeId!: string; + + @Field(() => GraphQLISODateTime) + created!: Date; + + @Field(() => GraphQLISODateTime) + updated!: Date; +} ``` ## `@QueryOptions` @@ -163,7 +195,7 @@ By default all results will be limited to 10 records. To override the default you can override the default page size by setting the `defaultResultSize` option. In this example we specify the `defaultResultSize` to 5 which means if a page size is not specified 5 results will be - returned. +returned. ```ts title="todo-item.dto.ts" {5} import { FilterableField, QueryOptions } from '@nestjs-query/query-graphql'; @@ -307,7 +339,7 @@ Enabling `totalCount` can be expensive. If your table is large the `totalCount` The `totalCount` field is not eagerly fetched. It will only be executed if the field is queried from the client. ::: -When using the `CURSOR` (the default) or `OFFSET` paging strategy you have the option to expose a `totalCount` field to +When using the `CURSOR` (the default) or `OFFSET` paging strategy you have the option to expose a `totalCount` field to allow clients to fetch a total count of records in a connection. To enable the `totalCount` field for connections set the `enableTotalCount` option to `true` using the @@ -353,7 +385,7 @@ values={[ { todoItems { totalCount - pageInfo{ + pageInfo { hasNextPage hasPreviousPage startCursor diff --git a/examples/basic/e2e/todo-item.resolver.spec.ts b/examples/basic/e2e/todo-item.resolver.spec.ts index 8dc939aef..a5e23046e 100644 --- a/examples/basic/e2e/todo-item.resolver.spec.ts +++ b/examples/basic/e2e/todo-item.resolver.spec.ts @@ -75,6 +75,25 @@ describe('TodoItemResolver (basic - e2e)', () => { }, })); + it(`should not include filter-only fields`, () => + request(app.getHttpServer()) + .post('/graphql') + .send({ + operationName: null, + variables: {}, + query: `{ + todoItem(id: 1) { + ${todoItemFields} + created + } + }`, + }) + .expect(400) + .then(({ body }) => { + expect(body.errors).toHaveLength(1); + expect(body.errors[0].message).toBe('Cannot query field "created" on type "TodoItem".'); + })); + it(`should return subTasks as a connection`, () => request(app.getHttpServer()) .post('/graphql') @@ -199,6 +218,41 @@ describe('TodoItemResolver (basic - e2e)', () => { ]); })); + it(`should allow querying by filter-only fields`, () => + request(app.getHttpServer()) + .post('/graphql') + .send({ + operationName: null, + variables: { + now: new Date().toISOString(), + }, + query: `query ($now: DateTime!) { + todoItems(filter: { created: { lt: $now } }) { + ${pageInfoField} + ${edgeNodes(todoItemFields)} + } + }`, + }) + .expect(200) + .then(({ body }) => { + const { edges, pageInfo }: CursorConnectionType = body.data.todoItems; + expect(pageInfo).toEqual({ + endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=', + hasNextPage: false, + hasPreviousPage: false, + startCursor: 'YXJyYXljb25uZWN0aW9uOjA=', + }); + expect(edges).toHaveLength(5); + + expect(edges.map((e) => e.node)).toEqual([ + { id: '1', title: 'Create Nest App', completed: true, description: null }, + { id: '2', title: 'Create Entity', completed: false, description: null }, + { id: '3', title: 'Create Entity Service', completed: false, description: null }, + { id: '4', title: 'Add Todo Item Resolver', completed: false, description: null }, + { id: '5', title: 'How to create item With Sub Tasks', completed: false, description: null }, + ]); + })); + it(`should allow sorting`, () => request(app.getHttpServer()) .post('/graphql') diff --git a/examples/basic/src/todo-item/dto/todo-item.dto.ts b/examples/basic/src/todo-item/dto/todo-item.dto.ts index 6815b60cd..97bd4a0d7 100644 --- a/examples/basic/src/todo-item/dto/todo-item.dto.ts +++ b/examples/basic/src/todo-item/dto/todo-item.dto.ts @@ -19,9 +19,9 @@ export class TodoItemDTO { @FilterableField() completed!: boolean; - @FilterableField(() => GraphQLISODateTime) + @FilterableField(() => GraphQLISODateTime, { filterOnly: true }) created!: Date; - @FilterableField(() => GraphQLISODateTime) + @FilterableField(() => GraphQLISODateTime, { filterOnly: true }) updated!: Date; } diff --git a/packages/query-graphql/__tests__/decorators/fitlerable-fields.decorator.spec.ts b/packages/query-graphql/__tests__/decorators/filterable-fields.decorator.spec.ts similarity index 92% rename from packages/query-graphql/__tests__/decorators/fitlerable-fields.decorator.spec.ts rename to packages/query-graphql/__tests__/decorators/filterable-fields.decorator.spec.ts index a1ce7951a..9e1d39fe3 100644 --- a/packages/query-graphql/__tests__/decorators/fitlerable-fields.decorator.spec.ts +++ b/packages/query-graphql/__tests__/decorators/filterable-fields.decorator.spec.ts @@ -23,6 +23,9 @@ describe('FilterableField decorator', (): void => { @FilterableField(undefined, { nullable: true }) numberField?: number; + + @FilterableField({ filterOnly: true }) + filterOnlyField!: string; } const fields = getFilterableFields(TestDto); expect(fields).toMatchObject([ @@ -40,6 +43,12 @@ describe('FilterableField decorator', (): void => { returnTypeFunc: floatReturnFunc, }, { propertyName: 'numberField', target: Number, advancedOptions: { nullable: true }, returnTypeFunc: undefined }, + { + propertyName: 'filterOnlyField', + target: String, + advancedOptions: { filterOnly: true }, + returnTypeFunc: undefined, + }, ]); expect(fieldSpy).toHaveBeenCalledTimes(4); expect(fieldSpy).toHaveBeenNthCalledWith(1); diff --git a/packages/query-graphql/src/decorators/filterable-field.decorator.ts b/packages/query-graphql/src/decorators/filterable-field.decorator.ts index 1fff96fd3..25908dc18 100644 --- a/packages/query-graphql/src/decorators/filterable-field.decorator.ts +++ b/packages/query-graphql/src/decorators/filterable-field.decorator.ts @@ -6,6 +6,7 @@ const reflector = new ArrayReflector(FILTERABLE_FIELD_KEY); export type FilterableFieldOptions = { allowedComparisons?: FilterComparisonOperators[]; filterRequired?: boolean; + filterOnly?: boolean; } & FieldOptions; export interface FilterableFieldDescriptor { @@ -78,6 +79,11 @@ export function FilterableField( returnTypeFunc, advancedOptions, }); + + if (advancedOptions?.filterOnly) { + return undefined; + } + if (returnTypeFunc) { return Field(returnTypeFunc, advancedOptions)(target, propertyName, descriptor); }