Skip to content

Commit

Permalink
feat(graphql): Enabling registering DTOs without auto-generating a re…
Browse files Browse the repository at this point in the history
…solver
  • Loading branch information
doug-martin committed Feb 26, 2021
1 parent b65544f commit 2f18142
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 42 deletions.
4 changes: 4 additions & 0 deletions documentation/docs/graphql/authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ export class TodoItemResolver extends CRUDResolver(TodoItemDTO) {

```

:::important
If you are extending the `CRUDResolver` directly be sure to [register your DTOs with the `NestjsQueryGraphQLModule`](./resolvers.mdx#crudresolver)
:::

## Complete Example

You can find a complete example in [`examples/auth`](https://github.com/doug-martin/nestjs-query/tree/master/examples/auth)
88 changes: 88 additions & 0 deletions documentation/docs/graphql/hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,91 @@ export class TodoItemDTO {
}

```

## Using Hooks In Custom Endpoints

You can also use hooks in custom endpoints by using the `HookInterceptor` along with
* `HookArgs` - Used to apply hooks to any query endpoint.
* `MutationHookArgs` - Used to apply hooks to any `mutation` that uses `MutationArgsType`

### Example

In this example we'll create an endpoint that marks all todo items that are currently not completed as completed.

To start we'll create our input types.

There are two types that are created
* The `UpdateManyTodoItemsInput` which extends the `UpdateManyInputType` this exposes an `update` and `filter` field just like the `updateMany` endpoints that are auto generated.
* The `UpdateManyTodoItemsArgs` which extends `MutationArgsType`, this provides a uniform interface for all mutations
ensuring that the argument provided to the mutation is named `input`.

```ts title="todo-item/types.ts"
import { MutationArgsType, UpdateManyInputType } from '@nestjs-query/query-graphql';
import { ArgsType, InputType } from '@nestjs/graphql';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoItemUpdateDTO } from './dto/todo-item-update.dto';

// create the base input type
@InputType()
export class UpdateManyTodoItemsInput extends UpdateManyInputType(TodoItemDTO, TodoItemUpdateDTO) {}

// Wrap the input in the MutationArgsType to provide a uniform format for all mutations
// The `MutationArgsType` is a thin wrapper that names the args as input
@ArgsType()
export class UpdateManyTodoItemsArgs extends MutationArgsType(UpdateManyTodoItemsInput) {}
```
Now we can use our new types in the resolver.

```ts title="todo-item/todo-item.resolver.ts" {14,15}
import { InjectQueryService, mergeFilter, QueryService, UpdateManyResponse } from '@nestjs-query/core';
import { HookTypes, HookInterceptor, MutationHookArgs, UpdateManyResponseType } from '@nestjs-query/query-graphql';
import { UseInterceptors } from '@nestjs/common';
import { Mutation, Resolver } from '@nestjs/graphql';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoItemEntity } from './todo-item.entity';
import { UpdateManyTodoItemsArgs } from './types';

@Resolver(() => TodoItemDTO)
export class TodoItemResolver {
constructor(@InjectQueryService(TodoItemEntity) readonly service: QueryService<TodoItemDTO>) {}

@Mutation(() => UpdateManyResponseType())
@UseInterceptors(HookInterceptor(HookTypes.BEFORE_UPDATE_MANY, TodoItemDTO))
markTodoItemsAsCompleted(@MutationHookArgs() { input }: UpdateManyTodoItemsArgs): Promise<UpdateManyResponse> {
return this.service.updateMany(
{ ...input.update, completed: false },
mergeFilter(input.filter, { completed: { is: false } }),
);
}
}
```
The first thing to notice is the

```ts
@UseInterceptors(HookInterceptor(HookTypes.BEFORE_UPDATE_MANY, TodoItemDTO))
```

This interceptor adds the correct hook to the `context` to be used by the param decorator.

:::note
In this example we bind the `BEFORE_UPDATE_MANY` hook, you can use any of the hooks available to bind to the correct
one when `creating`, `updating`, or `deleting` records.
:::

The next piece is the

```ts
@MutationHookArgs() { input }: UpdateManyTodoItemsArgs
```
By using the `MutationHookArgs` decorator we ensure that the hook is applied to the arguments adding any additional
fields to the update.

Finally we invoke the service `updateMany` with a filter that ensures we only update `TodoItems` that are completed,
and add an setting `completed` to true to the update

```ts
return this.service.updateMany(
{ ...input.update, completed: false },
mergeFilter(input.filter, { completed: { is: false } }),
);
```
160 changes: 137 additions & 23 deletions documentation/docs/graphql/resolvers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export class TodoItemModule {}

### CRUDResolver

If you want to add custom queries or mutations you can use the `CRUDResolver` to manually define your resolver.
If you want to override auto generated queries or mutations you can use the `CRUDResolver` to manually define your
resolver.

Resolvers work the same as they do in [`@nestjs/graphql`](https://docs.nestjs.com/graphql/resolvers-map) by annotating
your class with `@Resolver`.
Expand Down Expand Up @@ -122,6 +123,101 @@ export class TodoItemResolver extends CRUDResolver(TodoItemDTO) {

```

To ensure that all the correct providers are setup (e.g. hooks, assemblers, and authorizers) you also need to
register your DTOs with the `NestjsQueryGraphQLModule`.

Notice how the `dtos` property is specified instead of the resolvers, this allows you to specify your `DTOClass`,
`CreateDTOClass`, and `UpdateDTOClass` without creating an auto-generated resolver.

<Tabs
defaultValue="typeorm"
groupId="orm"
values={[
{ label: 'TypeOrm', value: 'typeorm', },
{ label: 'Sequelize', value: 'sequelize', },
{ label: 'Mongoose', value: 'mongoose', },
]
}>
<TabItem value="typeorm">

```ts title="todo-item.module.ts" {9,13}
import { NestjsQueryGraphQLModule } from '@nestjs-query/query-graphql';
import { NestjsQueryTypeOrmModule } from '@nestjs-query/query-typeorm';
import { Module } from '@nestjs/common';
import { TodoItemDTO } from './todo-item.dto';
import { TodoItemEntity } from './todo-item.entity';
import { TodoItemResolver } from './todo-item.resolver'

@Module({
providers: [TodoItemResolver]
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity])],
dtos: [{ DTOClass: TodoItemDTO }],
}),
],
})
export class TodoItemModule {}
```

</TabItem>
<TabItem value="sequelize">

```ts title="todo-item.module.ts" {9,13}
import { NestjsQueryGraphQLModule } from '@nestjs-query/query-graphql';
import { NestjsQuerySequelizeModule } from '@nestjs-query/query-sequelize';
import { Module } from '@nestjs/common';
import { TodoItemDTO } from './todo-item.dto';
import { TodoItemEntity } from './todo-item.entity';
import { TodoItemResolver } from './todo-item.resolver'

@Module({
providers: [TodoItemResolver]
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [NestjsQuerySequelizeModule.forFeature([TodoItemEntity])],
dtos: [{ DTOClass: TodoItemDTO }],
}),
],
})
export class TodoItemModule {}
```

</TabItem>
<TabItem value="mongoose">

```ts title="todo-item.module.ts" {9,17}
import { NestjsQueryGraphQLModule } from '@nestjs-query/query-graphql';
import { NestjsQueryMongooseModule } from '@nestjs-query/query-mongoose';
import { Module } from '@nestjs/common';
import { TodoItemDTO } from './todo-item.dto';
import { TodoItemEntity } from './todo-item.entity';
import { TodoItemResolver } from './todo-item.resolver'

@Module({
providers: [TodoItemResolver]
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [
NestjsQueryMongooseModule.forFeature([
{ document: TodoItemEntity, name: TodoItemEntity.name, schema: TodoItemEntitySchema },
]),
],
dtos: [{ DTOClass: TodoItemDTO }],
}),
],
})
export class TodoItemModule {}
```

</TabItem>
</Tabs>

:::warning
All of the subsequent examples omit the module definition for custom resolvers but you should still register your
DTOs to ensure all the providers are set up properly.
:::

### Generated Endpoints

When using the auto-generated resolver or extending `CRUDResolver` the methods that will be exposed for the `TodoItemDTO` are:
Expand Down Expand Up @@ -1281,7 +1377,7 @@ The `DeleteResolver` will only expose the `deleteOne` and `deleteMany` endpoints

For example the following resolver will expose the `updateOneTodoItem` and `updateManyTodoItems` mutations.

``` title="todo-item.resolver.ts"
```ts title="todo-item.resolver.ts"
import { QueryService, InjectQueryService } from '@nestjs-query/core';
import { DeleteResolver } from '@nestjs-query/query-graphql';
import { Resolver } from '@nestjs/graphql';
Expand All @@ -1299,9 +1395,14 @@ export class TodoItemResolver extends DeleteResolver(TodoItemDTO) {
```
---

## Custom Methods
## Custom Endpoints

You can also create custom methods.

You can also create custom methods that build on the methods added by CRUDResolver.
:::note
Unless you are overriding an endpoint you DO NOT need to extend the crud resolver directly, instead you can create a
new resolver for your type and add the new endpoint. `@nestjs/graphql` will handle merging the two resolver into one.
:::

Lets create a new query endpoint that only returns completed `TodoItems`.

Expand All @@ -1327,18 +1428,16 @@ In the code above we export two types. `TodoItemConnection` and `TodoItemQuery`.
In your resolver you can now create a new `completedTodoItems` method with the following:

```ts title="todo-item.resolver.ts"
import { Filter, QueryService, InjectQueryService } from '@nestjs-query/core';
import { ConnectionType, CRUDResolver } from '@nestjs-query/query-graphql';
import { Filter, InjectAssemblerQueryService, QueryService } from '@nestjs-query/core';
import { ConnectionType } from '@nestjs-query/query-graphql';
import { Args, Query, Resolver } from '@nestjs/graphql';
import { TodoItemDTO } from './todo-item.dto';
import { TodoItemEntity } from './todo-item.entity';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoItemAssembler } from './todo-item.assembler';
import { TodoItemConnection, TodoItemQuery } from './types';

@Resolver(() => TodoItemDTO)
export class TodoItemResolver extends CRUDResolver(TodoItemDTO) {
constructor(@InjectQueryService(TodoItemEntity) readonly service: QueryService<TodoItemEntity>) {
super(service);
}
export class TodoItemResolver {
constructor(@InjectQueryService(TodoItemEntity) readonly service: QueryService<TodoItemEntity>) {}

// Set the return type to the TodoItemConnection
@Query(() => TodoItemConnection)
Expand All @@ -1349,11 +1448,11 @@ export class TodoItemResolver extends CRUDResolver(TodoItemDTO) {
...{ completed: { is: true } },
};

// call the original queryMany method with the new query
return this.queryMany({ ...query, ...{ filter } });
}
return TodoItemConnection.createFromPromise((q) => this.service.query(q), { ...query, ...{ filter } });

}


```
Lets break this down so you know what is going on.
Expand All @@ -1378,7 +1477,7 @@ completedTodoItems(
): TodoItemConnection!
```
Notice how there is not a query arg but instead the you see the fields of `TodoItemQuery` that is because we used
`@Args` without a name.
`@Args` without a name and added the `@ArgsType` decorator to the `TodoItemQuery`.
The next piece is
Expand All @@ -1393,26 +1492,41 @@ const filter: Filter<TodoItemDTO> = {
Here we do a shallow copy of the `filter` and add `completed: { is: true }`. This will override any completed arguments
that an end user may have provided to ensure we always query for completed todos.
Finally we call the `queryMany` method from the `CRUDResolver` with the new filter set on the query.
Finally we call create our connection response by using the `createFromPromise` method on the connection.
```ts
// call the original queryMany method with the new query
return this.queryMany({ ...query, ...{ filter } });
return TodoItemConnection.createFromPromise((q) => this.service.query(q), { ...query, ...{ filter } });
```
The last step is to add the resolver to the module.
The last step is to add the resolver to the module, by registering our resolver as a provider and importing the
`NestjsQueryGraphQLModule` we will get both the auto generated resolver along with the custom endpoints from the
custom resolver.
```ts title="todo-items.module.ts"
import { Module } from '@nestjs/common';
import { NestjsQueryGraphQLModule } from '@nestjs-query/query-graphql';
import { NestjsQueryTypeOrmModule } from '@nestjs-query/query-typeorm';
import { Module } from '@nestjs/common';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoItemAssembler } from './todo-item.assembler';
import { TodoItemEntity } from './todo-item.entity';
import { TodoItemsResolver } from './todo-items.resolver';
import { TodoItemResolver } from './todo-item.resolver';

@Module({
providers: [TodoItemResolver],
imports: [
NestjsQueryTypeOrmModule.forFeature([TodoItemEntity]),
NestjsQueryGraphQLModule.forFeature({
imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity])],
assemblers: [TodoItemAssembler],
resolvers: [
{
DTOClass: TodoItemDTO,
EntityClass: TodoItemEntity,
},
],
}),
],
})
export class TodoItemsModule {}
export class TodoItemModule {}

```
22 changes: 22 additions & 0 deletions examples/hooks/src/todo-item/todo-item.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { InjectQueryService, mergeFilter, QueryService, UpdateManyResponse } from '@nestjs-query/core';
import { HookTypes, HookInterceptor, MutationHookArgs, UpdateManyResponseType } from '@nestjs-query/query-graphql';
import { UseInterceptors } from '@nestjs/common';
import { Mutation, Resolver } from '@nestjs/graphql';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoItemEntity } from './todo-item.entity';
import { UpdateManyTodoItemsArgs } from './types';

@Resolver(() => TodoItemDTO)
export class TodoItemResolver {
constructor(@InjectQueryService(TodoItemEntity) readonly service: QueryService<TodoItemDTO>) {}

// Set the return type to the TodoItemConnection
@Mutation(() => UpdateManyResponseType())
@UseInterceptors(HookInterceptor(HookTypes.BEFORE_UPDATE_MANY, TodoItemDTO))
markTodoItemsAsCompleted(@MutationHookArgs() { input }: UpdateManyTodoItemsArgs): Promise<UpdateManyResponse> {
return this.service.updateMany(
{ ...input.update, completed: false },
mergeFilter(input.filter, { completed: { is: false } }),
);
}
}
10 changes: 6 additions & 4 deletions examples/hooks/src/todo-item/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ConnectionType, QueryArgsType } from '@nestjs-query/query-graphql';
import { ArgsType } from '@nestjs/graphql';
import { MutationArgsType, UpdateManyInputType } from '@nestjs-query/query-graphql';
import { ArgsType, InputType } from '@nestjs/graphql';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoItemUpdateDTO } from './dto/todo-item-update.dto';

export const TodoItemConnection = ConnectionType(TodoItemDTO, { enableTotalCount: true });
@InputType()
class UpdateManyTodoItemsInput extends UpdateManyInputType(TodoItemDTO, TodoItemUpdateDTO) {}

@ArgsType()
export class TodoItemQuery extends QueryArgsType(TodoItemDTO, { defaultResultSize: 2 }) {}
export class UpdateManyTodoItemsArgs extends MutationArgsType(UpdateManyTodoItemsInput) {}
5 changes: 4 additions & 1 deletion packages/query-graphql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export {
Authorize,
AuthorizerFilter,
RelationAuthorizerFilter,
HookArgs,
MutationHookArgs,
KeySet,
} from './decorators';
export * from './resolvers';
Expand All @@ -40,6 +42,7 @@ export { pubSubToken, GraphQLPubSub } from './subscription';
export { Authorizer, AuthorizerOptions } from './auth';
export {
Hook,
HookTypes,
BeforeCreateOneHook,
BeforeCreateManyHook,
BeforeUpdateOneHook,
Expand All @@ -49,4 +52,4 @@ export {
BeforeQueryManyHook,
BeforeFindOneHook,
} from './hooks';
export { AuthorizerInterceptor, AuthorizerContext } from './interceptors';
export { AuthorizerInterceptor, AuthorizerContext, HookInterceptor, HookContext } from './interceptors';
Loading

0 comments on commit 2f18142

Please sign in to comment.