Skip to content

Commit

Permalink
feat(graphql,doug-martin#1048): added filter-only option to filterabl…
Browse files Browse the repository at this point in the history
…e fields
  • Loading branch information
mwoelk committed Apr 8, 2021
1 parent 70fd48e commit 3ce38e7
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 24 deletions.
65 changes: 43 additions & 22 deletions documentation/docs/graphql/dtos.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ If you use the @nestjs/graphql `Field` decorator it will not be exposed in the q
### Options

In addition to the normal field options you can also specify the following options
* `allowedComparisons` - An array of allowed comparisons. You can use this option to allow a subset of filter comparisons when querying through graphql.
* This option is useful if the field is expensive to query on for certain operators, or your data source supports a limited set of comparisons.
* `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.

- `allowedComparisons` - An array of allowed comparisons. You can use this option to allow a subset of filter comparisons when querying through graphql.
- This option is useful if the field is expensive to query on for certain operators, or your data source supports a limited set of comparisons.
- `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 All @@ -50,7 +53,6 @@ export class TodoItemDTO {
@Field(() => GraphQLISODateTime)
updated!: Date;
}

```

### Example - allowedComparisons
Expand Down Expand Up @@ -82,7 +84,6 @@ export class TodoItemDTO {
@Field(() => GraphQLISODateTime)
updated!: Date;
}

```

### Example - filterRequired
Expand Down Expand Up @@ -110,7 +111,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 @@ -151,7 +182,6 @@ export class TodoItemDTO {
@Field(() => GraphQLISODateTime)
updated!: Date;
}

```

---
Expand All @@ -163,7 +193,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 All @@ -187,7 +217,6 @@ export class TodoItemDTO {
@Field(() => GraphQLISODateTime)
updated!: Date;
}

```

### Limiting Results Size
Expand Down Expand Up @@ -224,7 +253,6 @@ export class TodoItemDTO {
@Field(() => GraphQLISODateTime)
updated!: Date;
}

```

---
Expand All @@ -237,8 +265,8 @@ For a more in-depth overview of paging check out the [paging docs](./paging.mdx)

You can override the default `pagingStrategy` to one of the following alternatives

* `OFFSET` - sets paging to allow `limit` and `offset` fields, and returns an `OffsetConnection`.
* `NONE` - turn off all paging and always return an `ArrayConnection`.
- `OFFSET` - sets paging to allow `limit` and `offset` fields, and returns an `OffsetConnection`.
- `NONE` - turn off all paging and always return an `ArrayConnection`.

When using the `OFFSET` strategy your the paging arguments for a many query will accept a `limit` and/or `offset`.
This will also change the return type from a `CursorConnection` to an `OffsetConnection`.
Expand All @@ -265,7 +293,6 @@ export class TodoItemDTO {
@Field(() => GraphQLISODateTime)
updated!: Date;
}

```

To disable paging entirely you can use the `NONE` `pagingStrategy`. When using `NONE` an `ArrayConnection` will be returned.
Expand All @@ -292,7 +319,6 @@ export class TodoItemDTO {
@Field(() => GraphQLISODateTime)
updated!: Date;
}

```

### Paging with Total Count
Expand All @@ -307,7 +333,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 @@ -335,7 +361,6 @@ export class TodoItemDTO {
@Field(() => GraphQLISODateTime)
updated!: Date;
}

```

When setting `enableTotalCount` to `true` you will be able to query for `totalCount` on `cursor` or offset connections
Expand All @@ -353,7 +378,7 @@ values={[
{
todoItems {
totalCount
pageInfo{
pageInfo {
hasNextPage
hasPreviousPage
startCursor
Expand All @@ -368,7 +393,6 @@ values={[
}
}
}

```

</TabItem>
Expand Down Expand Up @@ -460,7 +484,6 @@ export class TodoItemDTO {
@Field(() => GraphQLISODateTime)
updated!: Date;
}

```

### Allowed Boolean Expressions
Expand Down Expand Up @@ -492,7 +515,6 @@ export class TodoItemDTO {
@Field(() => GraphQLISODateTime)
updated!: Date;
}

```

To turn off all boolean expressions you can set `allowedBooleanExpressions` to an empty array. This is useful if you
Expand Down Expand Up @@ -522,5 +544,4 @@ export class TodoItemDTO {
@Field(() => GraphQLISODateTime)
updated!: Date;
}

```
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 3ce38e7

Please sign in to comment.