Skip to content

Commit

Permalink
feat(graphql,#1048): added filter-only option to filterable fields
Browse files Browse the repository at this point in the history
  • Loading branch information
mwoelk authored and doug-martin committed Apr 13, 2021
1 parent 3d4efd4 commit 55cb010
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 5 deletions.
38 changes: 35 additions & 3 deletions documentation/docs/graphql/dtos.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -353,7 +385,7 @@ values={[
{
todoItems {
totalCount
pageInfo{
pageInfo {
hasNextPage
hasPreviousPage
startCursor
Expand Down
54 changes: 54 additions & 0 deletions examples/basic/e2e/todo-item.resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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<TodoItemDTO> = 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')
Expand Down
4 changes: 2 additions & 2 deletions examples/basic/src/todo-item/dto/todo-item.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const reflector = new ArrayReflector(FILTERABLE_FIELD_KEY);
export type FilterableFieldOptions = {
allowedComparisons?: FilterComparisonOperators<unknown>[];
filterRequired?: boolean;
filterOnly?: boolean;
} & FieldOptions;

export interface FilterableFieldDescriptor {
Expand Down Expand Up @@ -78,6 +79,11 @@ export function FilterableField(
returnTypeFunc,
advancedOptions,
});

if (advancedOptions?.filterOnly) {
return undefined;
}

if (returnTypeFunc) {
return Field(returnTypeFunc, advancedOptions)(target, propertyName, descriptor);
}
Expand Down

0 comments on commit 55cb010

Please sign in to comment.