From 9d3a7932d2fe86f6dfcb1a6cd4439c0fe5f9a325 Mon Sep 17 00:00:00 2001 From: Johannes Schobel Date: Fri, 21 Feb 2020 08:47:35 +0100 Subject: [PATCH 1/7] chore: add deps --- examples/nest-graphql-typeorm/package.json | 2 ++ package-lock.json | 14 +++++++------- packages/query-graphql/package.json | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/nest-graphql-typeorm/package.json b/examples/nest-graphql-typeorm/package.json index 354c87a27..913c4e4a3 100644 --- a/examples/nest-graphql-typeorm/package.json +++ b/examples/nest-graphql-typeorm/package.json @@ -26,6 +26,8 @@ "@nestjs/platform-express": "^6.7.2", "@nestjs/typeorm": "^6.2.0", "apollo-server-express": "^2.9.7", + "class-transformer": "^0.2.3", + "class-validator": "^0.11.0", "graphql": "^14.5.8", "graphql-tools": "^4.0.6", "pg": "^7.12.1", diff --git a/package-lock.json b/package-lock.json index 148acb390..cb014ee77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4551,13 +4551,13 @@ } }, "class-validator": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.10.2.tgz", - "integrity": "sha512-57bGDjoFXizqGZBHe/uHn5/K0MSjBkToaHpDhAXR6DIwjaoET37a0Uug4F5RZR7WF31/7SqzKFIvd+ZspszGUA==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.11.0.tgz", + "integrity": "sha512-niAmmSPFku9xsnpYYrddy8NZRrCX3yyoZ/rgPKOilE5BG0Ma1eVCIxpR4X0LasL/6BzbYzsutG+mSbAXlh4zNw==", "requires": { "@types/validator": "10.11.3", "google-libphonenumber": "^3.1.6", - "validator": "11.1.0" + "validator": "12.0.0" } }, "clean-stack": { @@ -15043,9 +15043,9 @@ } }, "validator": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-11.1.0.tgz", - "integrity": "sha512-qiQ5ktdO7CD6C/5/mYV4jku/7qnqzjrxb3C/Q5wR3vGGinHTgJZN/TdFT3ZX4vXhX2R1PXx42fB1cn5W+uJ4lg==" + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-12.0.0.tgz", + "integrity": "sha512-r5zA1cQBEOgYlesRmSEwc9LkbfNLTtji+vWyaHzRZUxCTHdsX3bd+sdHfs5tGZ2W6ILGGsxWxCNwT/h3IY/3ng==" }, "vary": { "version": "1.1.2", diff --git a/packages/query-graphql/package.json b/packages/query-graphql/package.json index c39eb38bb..4a1a9de35 100644 --- a/packages/query-graphql/package.json +++ b/packages/query-graphql/package.json @@ -40,7 +40,7 @@ "@nestjs/common": "^6.9.0", "@nestjs/graphql": "^6.5.3", "class-transformer": "^0.2.3", - "class-validator": "^0.10.2", + "class-validator": "^0.11.0", "graphql": "^14.5.8", "graphql-relay": "^0.6.0", "reflect-metadata": "^0.1.13", @@ -55,7 +55,7 @@ "@types/node-fetch": "^2.5.4", "@types/pluralize": "0.0.29", "class-transformer": "^0.2.3", - "class-validator": "^0.10.2", + "class-validator": "^0.11.0", "graphql": "^14.5.8", "graphql-relay": "^0.6.0", "reflect-metadata": "^0.1.13", From 82832b449b919fafc997984150b15247d178742f Mon Sep 17 00:00:00 2001 From: Johannes Schobel Date: Fri, 21 Feb 2020 08:53:19 +0100 Subject: [PATCH 2/7] fix: make validation pipe more restrictive --- examples/nest-graphql-typeorm/src/main.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/examples/nest-graphql-typeorm/src/main.ts b/examples/nest-graphql-typeorm/src/main.ts index 39c1a133c..3f313910b 100644 --- a/examples/nest-graphql-typeorm/src/main.ts +++ b/examples/nest-graphql-typeorm/src/main.ts @@ -4,7 +4,17 @@ import { AppModule } from './app.module'; async function bootstrap(): Promise { const app = await NestFactory.create(AppModule); - app.useGlobalPipes(new ValidationPipe({ transform: true })); + + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + skipMissingProperties: false, + forbidUnknownValues: true, + }), + ); + await app.listen(3000); } From ea0476054382431137e478650f77c26b4f244339 Mon Sep 17 00:00:00 2001 From: Johannes Schobel Date: Fri, 21 Feb 2020 09:04:08 +0100 Subject: [PATCH 3/7] feat: add a custom argstype with nested validation --- .../src/todo-item/args/custom-args.types.ts | 15 +++++++++++++++ .../src/todo-item/dto/todo-item-input.dto.ts | 4 ++++ .../src/todo-item/todo-item.resolver.ts | 8 ++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 examples/nest-graphql-typeorm/src/todo-item/args/custom-args.types.ts diff --git a/examples/nest-graphql-typeorm/src/todo-item/args/custom-args.types.ts b/examples/nest-graphql-typeorm/src/todo-item/args/custom-args.types.ts new file mode 100644 index 000000000..d4ebed621 --- /dev/null +++ b/examples/nest-graphql-typeorm/src/todo-item/args/custom-args.types.ts @@ -0,0 +1,15 @@ +import { CreateOneArgsType } from '@nestjs-query/query-graphql'; +import { Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { ArgsType, Field } from 'type-graphql'; +import { TodoItemInputDTO } from '../dto/todo-item-input.dto'; + +@ArgsType() +export class CreateOneTodoItemArgs extends CreateOneArgsType(TodoItemInputDTO) { + @Type(() => TodoItemInputDTO) + @ValidateNested() + @Field({ + description: 'The ToDo Item to be created', + }) + input!: TodoItemInputDTO; +} diff --git a/examples/nest-graphql-typeorm/src/todo-item/dto/todo-item-input.dto.ts b/examples/nest-graphql-typeorm/src/todo-item/dto/todo-item-input.dto.ts index 69162ea2d..a40445e49 100644 --- a/examples/nest-graphql-typeorm/src/todo-item/dto/todo-item-input.dto.ts +++ b/examples/nest-graphql-typeorm/src/todo-item/dto/todo-item-input.dto.ts @@ -1,10 +1,14 @@ +import { IsString, MaxLength, IsBoolean } from 'class-validator'; import { Field, InputType } from 'type-graphql'; @InputType('TodoItemInput') export class TodoItemInputDTO { + @IsString() + @MaxLength(20) @Field() title!: string; + @IsBoolean() @Field() completed!: boolean; } diff --git a/examples/nest-graphql-typeorm/src/todo-item/todo-item.resolver.ts b/examples/nest-graphql-typeorm/src/todo-item/todo-item.resolver.ts index bc1037306..243c2d55b 100644 --- a/examples/nest-graphql-typeorm/src/todo-item/todo-item.resolver.ts +++ b/examples/nest-graphql-typeorm/src/todo-item/todo-item.resolver.ts @@ -7,14 +7,18 @@ import { TodoItemInputDTO } from './dto/todo-item-input.dto'; import { TodoItemDTO } from './dto/todo-item.dto'; import { TodoItemService } from './todo-item.service'; import { TodoItemConnection, TodoItemQuery } from './types'; +import { CreateOneTodoItemArgs } from './args/custom-args.types'; const guards = [AuthGuard]; @Resolver(() => TodoItemDTO) export class TodoItemResolver extends CRUDResolver(TodoItemDTO, { - CreateDTOClass: TodoItemInputDTO, UpdateDTOClass: TodoItemInputDTO, - create: { guards }, + create: { + guards, + CreateOneArgs: CreateOneTodoItemArgs, + CreateDTOClass: TodoItemInputDTO, + }, update: { guards }, delete: { guards }, relations: { From d8b6e689b83a848aaca3757b5be9e47a17ceee0b Mon Sep 17 00:00:00 2001 From: Johannes Schobel Date: Fri, 21 Feb 2020 13:02:26 +0100 Subject: [PATCH 4/7] fix: BREAKING change input to id for DELETE ONE --- .../query-graphql/__tests__/resolvers/delete.resolver.spec.ts | 4 ++-- packages/query-graphql/src/resolvers/delete.resolver.ts | 2 +- packages/query-graphql/src/types/delete-one-args.type.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/query-graphql/__tests__/resolvers/delete.resolver.spec.ts b/packages/query-graphql/__tests__/resolvers/delete.resolver.spec.ts index b91ff8a8c..1b0e165db 100644 --- a/packages/query-graphql/__tests__/resolvers/delete.resolver.spec.ts +++ b/packages/query-graphql/__tests__/resolvers/delete.resolver.spec.ts @@ -117,14 +117,14 @@ describe('DeleteResolver', () => { it('should call the service deleteOne with the provided input', async () => { const mockService = mock>(); const input: DeleteOneArgsType = { - input: 'id-1', + id: 'id-1', }; const output: TestResolverDTO = { id: 'id-1', stringField: 'foo', }; const resolver = new TestResolver(instance(mockService)); - when(mockService.deleteOne(input.input)).thenResolve(output); + when(mockService.deleteOne(input.id)).thenResolve(output); const result = await resolver.deleteOne(input); return expect(result).toEqual(output); }); diff --git a/packages/query-graphql/src/resolvers/delete.resolver.ts b/packages/query-graphql/src/resolvers/delete.resolver.ts index f2eb10b27..1cfe324fd 100644 --- a/packages/query-graphql/src/resolvers/delete.resolver.ts +++ b/packages/query-graphql/src/resolvers/delete.resolver.ts @@ -55,7 +55,7 @@ export const Deletable = (DTOClass: Class, opts: DeleteResolverOpts
DeleteOneResponse, { name: `deleteOne${baseName}` }, commonResolverOpts, opts.one ?? {}) async deleteOne(@Args() input: DO): Promise> { const deleteOne = await transformAndValidate(DO, input); - return this.service.deleteOne(deleteOne.input); + return this.service.deleteOne(deleteOne.id); } @ResolverMutation(() => DMR, { name: `deleteMany${pluralBaseName}` }, commonResolverOpts, opts.many ?? {}) diff --git a/packages/query-graphql/src/types/delete-one-args.type.ts b/packages/query-graphql/src/types/delete-one-args.type.ts index f5d73a7f8..e98a2f208 100644 --- a/packages/query-graphql/src/types/delete-one-args.type.ts +++ b/packages/query-graphql/src/types/delete-one-args.type.ts @@ -2,7 +2,7 @@ import { Class } from '@nestjs-query/core'; import { Field, ID, ArgsType } from 'type-graphql'; export interface DeleteOneArgsType { - input: string | number; + id: string | number; } /** @internal */ @@ -14,7 +14,7 @@ export function DeleteOneArgsType(): Class { @ArgsType() class DeleteOneArgs implements DeleteOneArgsType { @Field(() => ID, { description: 'The id of the record to delete.' }) - input!: string | number; + id!: string | number; } deleteOneArgsType = DeleteOneArgs; return deleteOneArgsType; From edc9d2d2541ac8c7174a0b204d3c1a4fc8e57f5f Mon Sep 17 00:00:00 2001 From: Johannes Schobel Date: Fri, 21 Feb 2020 13:06:43 +0100 Subject: [PATCH 5/7] refactor: fix typo --- packages/query-graphql/src/types/create-many-args.type.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/query-graphql/src/types/create-many-args.type.ts b/packages/query-graphql/src/types/create-many-args.type.ts index 74f3447a4..f85b20aed 100644 --- a/packages/query-graphql/src/types/create-many-args.type.ts +++ b/packages/query-graphql/src/types/create-many-args.type.ts @@ -5,10 +5,10 @@ export interface CreateManyArgsType { input: C[]; } -export function CreateManyArgsType(ITemClass: Class): Class> { +export function CreateManyArgsType(ItemClass: Class): Class> { @ArgsType() class CreateManyArgs implements CreateManyArgsType { - @Field(() => [ITemClass], { description: 'Array of records to create' }) + @Field(() => [ItemClass], { description: 'Array of records to create' }) input!: C[]; } return CreateManyArgs; From 9620cf15f1678f074341383ca08dbd55dc1e8bb3 Mon Sep 17 00:00:00 2001 From: Johannes Schobel Date: Fri, 21 Feb 2020 13:29:02 +0100 Subject: [PATCH 6/7] feat: add validators --- packages/query-graphql/src/types/create-many-args.type.ts | 6 +++++- packages/query-graphql/src/types/create-one-args.type.ts | 6 +++++- packages/query-graphql/src/types/delete-many-args.type.ts | 2 +- packages/query-graphql/src/types/delete-one-args.type.ts | 2 ++ packages/query-graphql/src/types/update-many-args.type.ts | 7 +++++-- packages/query-graphql/src/types/update-one-args.type.ts | 5 +++++ 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/query-graphql/src/types/create-many-args.type.ts b/packages/query-graphql/src/types/create-many-args.type.ts index f85b20aed..b9c19530b 100644 --- a/packages/query-graphql/src/types/create-many-args.type.ts +++ b/packages/query-graphql/src/types/create-many-args.type.ts @@ -1,5 +1,7 @@ import { Class } from '@nestjs-query/core'; -import { Field, ArgsType } from 'type-graphql'; +import { Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { ArgsType, Field } from 'type-graphql'; export interface CreateManyArgsType { input: C[]; @@ -8,6 +10,8 @@ export interface CreateManyArgsType { export function CreateManyArgsType(ItemClass: Class): Class> { @ArgsType() class CreateManyArgs implements CreateManyArgsType { + @Type(() => ItemClass) + @ValidateNested({ each: true }) @Field(() => [ItemClass], { description: 'Array of records to create' }) input!: C[]; } diff --git a/packages/query-graphql/src/types/create-one-args.type.ts b/packages/query-graphql/src/types/create-one-args.type.ts index e1c379f79..46b62abc2 100644 --- a/packages/query-graphql/src/types/create-one-args.type.ts +++ b/packages/query-graphql/src/types/create-one-args.type.ts @@ -1,5 +1,7 @@ import { Class } from '@nestjs-query/core'; -import { Field, ArgsType } from 'type-graphql'; +import { Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { ArgsType, Field } from 'type-graphql'; export interface CreateOneArgsType { input: C; @@ -8,6 +10,8 @@ export interface CreateOneArgsType { export function CreateOneArgsType(ItemClass: Class): Class> { @ArgsType() class CreateOneArgs implements CreateOneArgsType { + @Type(() => ItemClass) + @ValidateNested() @Field(() => ItemClass, { description: 'The record to create' }) input!: C; } diff --git a/packages/query-graphql/src/types/delete-many-args.type.ts b/packages/query-graphql/src/types/delete-many-args.type.ts index aef3d3203..192135602 100644 --- a/packages/query-graphql/src/types/delete-many-args.type.ts +++ b/packages/query-graphql/src/types/delete-many-args.type.ts @@ -9,8 +9,8 @@ export interface DeleteManyArgsType { export function DeleteManyArgsType(FilterType: Class>): Class> { @ArgsType() class DeleteManyArgs implements DeleteManyArgsType { - @Field(() => FilterType, { description: 'Filter to find records to delete' }) @IsNotEmptyObject() + @Field(() => FilterType, { description: 'Filter to find records to delete' }) input!: Filter; } return DeleteManyArgs; diff --git a/packages/query-graphql/src/types/delete-one-args.type.ts b/packages/query-graphql/src/types/delete-one-args.type.ts index e98a2f208..98fc33fb7 100644 --- a/packages/query-graphql/src/types/delete-one-args.type.ts +++ b/packages/query-graphql/src/types/delete-one-args.type.ts @@ -1,5 +1,6 @@ import { Class } from '@nestjs-query/core'; import { Field, ID, ArgsType } from 'type-graphql'; +import { IsNotEmpty } from 'class-validator'; export interface DeleteOneArgsType { id: string | number; @@ -13,6 +14,7 @@ export function DeleteOneArgsType(): Class { } @ArgsType() class DeleteOneArgs implements DeleteOneArgsType { + @IsNotEmpty() @Field(() => ID, { description: 'The id of the record to delete.' }) id!: string | number; } diff --git a/packages/query-graphql/src/types/update-many-args.type.ts b/packages/query-graphql/src/types/update-many-args.type.ts index 2bb28c666..3f512e979 100644 --- a/packages/query-graphql/src/types/update-many-args.type.ts +++ b/packages/query-graphql/src/types/update-many-args.type.ts @@ -1,6 +1,7 @@ import { DeepPartial, Filter, Class } from '@nestjs-query/core'; import { Field, ArgsType } from 'type-graphql'; -import { IsNotEmptyObject } from 'class-validator'; +import { IsNotEmptyObject, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; export interface UpdateManyArgsType> { filter: Filter; @@ -13,10 +14,12 @@ export function UpdateManyArgsType>( ): Class> { @ArgsType() class UpdateManyArgs implements UpdateManyArgsType { - @Field(() => FilterType, { description: 'Filter used to find fields to update' }) @IsNotEmptyObject() + @Field(() => FilterType, { description: 'Filter used to find fields to update' }) filter!: Filter; + @Type(() => UpdateType) + @ValidateNested() @Field(() => UpdateType, { description: 'The update to apply to all records found using the filter' }) input!: U; } diff --git a/packages/query-graphql/src/types/update-one-args.type.ts b/packages/query-graphql/src/types/update-one-args.type.ts index 5b70ba6c4..ca57399ae 100644 --- a/packages/query-graphql/src/types/update-one-args.type.ts +++ b/packages/query-graphql/src/types/update-one-args.type.ts @@ -1,5 +1,7 @@ import { DeepPartial, Class } from '@nestjs-query/core'; import { Field, ID, ArgsType } from 'type-graphql'; +import { IsNotEmpty, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; export interface UpdateOneArgsType> { id: string | number; @@ -9,9 +11,12 @@ export interface UpdateOneArgsType> { export function UpdateOneArgsType>(UpdateType: Class): Class> { @ArgsType() class UpdateOneArgs implements UpdateOneArgsType { + @IsNotEmpty() @Field(() => ID, { description: 'The id of the record to update' }) id!: string | number; + @Type(() => UpdateType) + @ValidateNested() @Field(() => UpdateType, { description: 'The update to apply.' }) input!: U; } From b822fc240652b61e493aef6fedec1a6d03d495ee Mon Sep 17 00:00:00 2001 From: doug-martin Date: Sun, 23 Feb 2020 19:22:08 -0600 Subject: [PATCH 7/7] Fix for issue #19 * Added tests * More validators! --- HISTORY.md | 4 + .../src/todo-item/args/custom-args.types.ts | 15 ---- .../src/todo-item/todo-item.resolver.ts | 8 +- .../resolvers/create.resolver.spec.ts | 6 +- .../resolvers/delete.resolver.spec.ts | 4 +- .../types/delete-many-args.type.spec.ts | 41 ++++++++- .../types/delete-one-args.type.spec.ts | 37 ++++++++ .../types/relation-args.type.spec.ts | 69 +++++++++++++++ .../types/relations-args.type.spec.ts | 87 +++++++++++++++++++ .../types/update-many-args.type.spec.ts | 49 ++++++++++- .../types/update-one-args.type.spec.ts | 67 +++++++++++++- .../src/resolvers/delete.resolver.ts | 2 +- .../src/types/create-many-args.type.ts | 3 +- .../src/types/delete-many-args.type.ts | 3 +- .../src/types/delete-one-args.type.ts | 4 +- .../src/types/relations-args.type.ts | 6 +- .../src/types/update-many-args.type.ts | 1 + 17 files changed, 370 insertions(+), 36 deletions(-) delete mode 100644 examples/nest-graphql-typeorm/src/todo-item/args/custom-args.types.ts diff --git a/HISTORY.md b/HISTORY.md index afa4341aa..ac6f915e1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,7 @@ +# 0.3.5 + +* [FIXED] Validate Input for Create & Update [#19](https://github.com/doug-martin/nestjs-query/issues/19) + # v0.3.4 * [FIXED] Can't remove on Many-To-Many relations [#31](https://github.com/doug-martin/nestjs-query/issues/31) diff --git a/examples/nest-graphql-typeorm/src/todo-item/args/custom-args.types.ts b/examples/nest-graphql-typeorm/src/todo-item/args/custom-args.types.ts deleted file mode 100644 index d4ebed621..000000000 --- a/examples/nest-graphql-typeorm/src/todo-item/args/custom-args.types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CreateOneArgsType } from '@nestjs-query/query-graphql'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; -import { ArgsType, Field } from 'type-graphql'; -import { TodoItemInputDTO } from '../dto/todo-item-input.dto'; - -@ArgsType() -export class CreateOneTodoItemArgs extends CreateOneArgsType(TodoItemInputDTO) { - @Type(() => TodoItemInputDTO) - @ValidateNested() - @Field({ - description: 'The ToDo Item to be created', - }) - input!: TodoItemInputDTO; -} diff --git a/examples/nest-graphql-typeorm/src/todo-item/todo-item.resolver.ts b/examples/nest-graphql-typeorm/src/todo-item/todo-item.resolver.ts index 6f71c8539..740224c15 100644 --- a/examples/nest-graphql-typeorm/src/todo-item/todo-item.resolver.ts +++ b/examples/nest-graphql-typeorm/src/todo-item/todo-item.resolver.ts @@ -8,18 +8,14 @@ import { TodoItemInputDTO } from './dto/todo-item-input.dto'; import { TodoItemDTO } from './dto/todo-item.dto'; import { TodoItemService } from './todo-item.service'; import { TodoItemConnection, TodoItemQuery } from './types'; -import { CreateOneTodoItemArgs } from './args/custom-args.types'; const guards = [AuthGuard]; @Resolver(() => TodoItemDTO) export class TodoItemResolver extends CRUDResolver(TodoItemDTO, { + CreateDTOClass: TodoItemInputDTO, UpdateDTOClass: TodoItemInputDTO, - create: { - guards, - CreateOneArgs: CreateOneTodoItemArgs, - CreateDTOClass: TodoItemInputDTO, - }, + create: { guards }, update: { guards }, delete: { guards }, relations: { diff --git a/packages/query-graphql/__tests__/resolvers/create.resolver.spec.ts b/packages/query-graphql/__tests__/resolvers/create.resolver.spec.ts index 6e55df203..f8009d0fc 100644 --- a/packages/query-graphql/__tests__/resolvers/create.resolver.spec.ts +++ b/packages/query-graphql/__tests__/resolvers/create.resolver.spec.ts @@ -2,7 +2,7 @@ import 'reflect-metadata'; import { ID, ObjectType } from 'type-graphql'; import * as nestGraphql from '@nestjs/graphql'; -import { instance, mock, when, deepEqual } from 'ts-mockito'; +import { instance, mock, when, objectContaining } from 'ts-mockito'; import { CanActivate, ExecutionContext } from '@nestjs/common'; import { QueryService, DeepPartial } from '@nestjs-query/core'; import { IsString } from 'class-validator'; @@ -133,7 +133,7 @@ describe('CreateResolver', () => { stringField: 'foo', }; const resolver = new TestResolver(instance(mockService)); - when(mockService.createOne(deepEqual(args.input))).thenResolve(output); + when(mockService.createOne(objectContaining(args.input))).thenResolve(output); const result = await resolver.createOne(args); return expect(result).toEqual(output); }); @@ -190,7 +190,7 @@ describe('CreateResolver', () => { }, ]; const resolver = new TestResolver(instance(mockService)); - when(mockService.createMany(deepEqual(args.input))).thenResolve(output); + when(mockService.createMany(objectContaining(args.input))).thenResolve(output); const result = await resolver.createMany(args); return expect(result).toEqual(output); }); diff --git a/packages/query-graphql/__tests__/resolvers/delete.resolver.spec.ts b/packages/query-graphql/__tests__/resolvers/delete.resolver.spec.ts index 1b0e165db..b91ff8a8c 100644 --- a/packages/query-graphql/__tests__/resolvers/delete.resolver.spec.ts +++ b/packages/query-graphql/__tests__/resolvers/delete.resolver.spec.ts @@ -117,14 +117,14 @@ describe('DeleteResolver', () => { it('should call the service deleteOne with the provided input', async () => { const mockService = mock>(); const input: DeleteOneArgsType = { - id: 'id-1', + input: 'id-1', }; const output: TestResolverDTO = { id: 'id-1', stringField: 'foo', }; const resolver = new TestResolver(instance(mockService)); - when(mockService.deleteOne(input.id)).thenResolve(output); + when(mockService.deleteOne(input.input)).thenResolve(output); const result = await resolver.deleteOne(input); return expect(result).toEqual(output); }); diff --git a/packages/query-graphql/__tests__/types/delete-many-args.type.spec.ts b/packages/query-graphql/__tests__/types/delete-many-args.type.spec.ts index 56273e70f..450ded7db 100644 --- a/packages/query-graphql/__tests__/types/delete-many-args.type.spec.ts +++ b/packages/query-graphql/__tests__/types/delete-many-args.type.spec.ts @@ -1,16 +1,55 @@ import 'reflect-metadata'; import * as typeGraphql from 'type-graphql'; +import { plainToClass } from 'class-transformer'; +import { validateSync } from 'class-validator'; import { DeleteManyArgsType } from '../../src'; describe('DeleteManyArgsType', (): void => { const argsTypeSpy = jest.spyOn(typeGraphql, 'ArgsType'); const fieldSpy = jest.spyOn(typeGraphql, 'Field'); + class FakeFilter {} it('should create an args type with an array field', () => { - class FakeFilter {} DeleteManyArgsType(FakeFilter); expect(argsTypeSpy).toBeCalledWith(); expect(argsTypeSpy).toBeCalledTimes(1); expect(fieldSpy.mock.calls[0]![0]!()).toEqual(FakeFilter); }); + + describe('validation', () => { + it('should validate the filter is defined', () => { + const Type = DeleteManyArgsType(FakeFilter); + const input = {}; + const it = plainToClass(Type, input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmptyObject: 'input must be a non-empty object', + }, + property: 'input', + target: input, + }, + ]); + }); + + it('should validate the filter is not empty', () => { + const Type = DeleteManyArgsType(FakeFilter); + const input = { input: {} }; + const it = plainToClass(Type, input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmptyObject: 'input must be a non-empty object', + }, + property: 'input', + target: input, + value: input.input, + }, + ]); + }); + }); }); diff --git a/packages/query-graphql/__tests__/types/delete-one-args.type.spec.ts b/packages/query-graphql/__tests__/types/delete-one-args.type.spec.ts index 52dcdc562..43fb93939 100644 --- a/packages/query-graphql/__tests__/types/delete-one-args.type.spec.ts +++ b/packages/query-graphql/__tests__/types/delete-one-args.type.spec.ts @@ -1,5 +1,7 @@ import 'reflect-metadata'; import * as typeGraphql from 'type-graphql'; +import { plainToClass } from 'class-transformer'; +import { validateSync } from 'class-validator'; import { DeleteOneArgsType } from '../../src'; describe('DeleteOneArgsType', (): void => { @@ -13,4 +15,39 @@ describe('DeleteOneArgsType', (): void => { expect(fieldSpy).toBeCalledTimes(1); expect(fieldSpy.mock.calls[0]![0]!()).toEqual(typeGraphql.ID); }); + + describe('validation', () => { + it('should validate the id is defined', () => { + const input = {}; + const it = plainToClass(DeleteOneArgsType(), input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'input should not be empty', + }, + property: 'input', + target: input, + }, + ]); + }); + + it('should validate the id is not empty', () => { + const input = { input: '' }; + const it = plainToClass(DeleteOneArgsType(), input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'input should not be empty', + }, + property: 'input', + target: input, + value: '', + }, + ]); + }); + }); }); diff --git a/packages/query-graphql/__tests__/types/relation-args.type.spec.ts b/packages/query-graphql/__tests__/types/relation-args.type.spec.ts index 19cdbb5e2..c69ce23e2 100644 --- a/packages/query-graphql/__tests__/types/relation-args.type.spec.ts +++ b/packages/query-graphql/__tests__/types/relation-args.type.spec.ts @@ -1,6 +1,7 @@ import 'reflect-metadata'; import * as typeGraphql from 'type-graphql'; import { plainToClass } from 'class-transformer'; +import { validateSync } from 'class-validator'; import { RelationArgsType } from '../../src'; describe('RelationArgsType', (): void => { @@ -22,4 +23,72 @@ describe('RelationArgsType', (): void => { expect(it.id).toEqual(input.id); expect(it.relationId).toEqual(input.relationId); }); + + describe('validation', () => { + it('should validate the id is defined', () => { + const input = { relationId: 1 }; + const it = plainToClass(RelationArgsType(), input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'id should not be empty', + }, + property: 'id', + target: input, + }, + ]); + }); + + it('should validate the id is not empty', () => { + const input = { id: '', relationId: 1 }; + const it = plainToClass(RelationArgsType(), input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'id should not be empty', + }, + property: 'id', + target: input, + value: '', + }, + ]); + }); + + it('should validate that relationId is defined', () => { + const input = { id: 1 }; + const it = plainToClass(RelationArgsType(), input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'relationId should not be empty', + }, + property: 'relationId', + target: input, + }, + ]); + }); + + it('should validate that relationId is not empty', () => { + const input: RelationArgsType = { id: 1, relationId: '' }; + const it = plainToClass(RelationArgsType(), input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'relationId should not be empty', + }, + property: 'relationId', + target: input, + value: input.relationId, + }, + ]); + }); + }); }); diff --git a/packages/query-graphql/__tests__/types/relations-args.type.spec.ts b/packages/query-graphql/__tests__/types/relations-args.type.spec.ts index 360449aca..d5439d6bc 100644 --- a/packages/query-graphql/__tests__/types/relations-args.type.spec.ts +++ b/packages/query-graphql/__tests__/types/relations-args.type.spec.ts @@ -1,6 +1,7 @@ import 'reflect-metadata'; import * as typeGraphql from 'type-graphql'; import { plainToClass } from 'class-transformer'; +import { validateSync } from 'class-validator'; import { RelationsArgsType } from '../../src'; describe('RelationsArgsType', (): void => { @@ -22,4 +23,90 @@ describe('RelationsArgsType', (): void => { expect(it.id).toEqual(input.id); expect(it.relationIds).toEqual(input.relationIds); }); + + describe('validation', () => { + it('should validate the id is defined', () => { + const input = { relationIds: [2, 3, 4] }; + const it = plainToClass(RelationsArgsType(), input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'id should not be empty', + }, + property: 'id', + target: input, + }, + ]); + }); + + it('should validate the id is not empty', () => { + const input = { id: '', relationIds: [2, 3, 4] }; + const it = plainToClass(RelationsArgsType(), input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'id should not be empty', + }, + property: 'id', + target: input, + value: '', + }, + ]); + }); + + it('should validate that relationsIds is not empty', () => { + const input: RelationsArgsType = { id: 1, relationIds: [] }; + const it = plainToClass(RelationsArgsType(), input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + arrayNotEmpty: 'relationIds should not be empty', + }, + property: 'relationIds', + target: input, + value: input.relationIds, + }, + ]); + }); + + it('should validate that relationsIds is unique', () => { + const input: RelationsArgsType = { id: 1, relationIds: [1, 2, 1, 2] }; + const it = plainToClass(RelationsArgsType(), input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + arrayUnique: "All relationIds's elements must be unique", + }, + property: 'relationIds', + target: input, + value: input.relationIds, + }, + ]); + }); + + it('should validate that relationsIds does not contain an empty id', () => { + const input: RelationsArgsType = { id: 1, relationIds: [''] }; + const it = plainToClass(RelationsArgsType(), input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'each value in relationIds should not be empty', + }, + property: 'relationIds', + target: input, + value: input.relationIds, + }, + ]); + }); + }); }); diff --git a/packages/query-graphql/__tests__/types/update-many-args.type.spec.ts b/packages/query-graphql/__tests__/types/update-many-args.type.spec.ts index e96042988..0e3090634 100644 --- a/packages/query-graphql/__tests__/types/update-many-args.type.spec.ts +++ b/packages/query-graphql/__tests__/types/update-many-args.type.spec.ts @@ -2,13 +2,17 @@ import 'reflect-metadata'; import { Filter } from '@nestjs-query/core'; import * as typeGraphql from 'type-graphql'; import { plainToClass } from 'class-transformer'; +import { MinLength, validateSync } from 'class-validator'; import { UpdateManyArgsType } from '../../src'; describe('UpdateManyArgsType', (): void => { const argsTypeSpy = jest.spyOn(typeGraphql, 'ArgsType'); const fieldSpy = jest.spyOn(typeGraphql, 'Field'); - class FakeType {} + class FakeType { + @MinLength(5) + name!: string; + } class FakeFilter implements Filter {} it('should create an args type with an array field', () => { UpdateManyArgsType(FakeFilter, FakeType); @@ -25,4 +29,47 @@ describe('UpdateManyArgsType', (): void => { const it = plainToClass(Type, input); expect(it.input).toEqual(input.input); }); + + describe('validation', () => { + it('should validate the filter is not empty', () => { + const Type = UpdateManyArgsType(FakeFilter, FakeType); + const input = { input: { name: 'hello world' } }; + const it = plainToClass(Type, input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmptyObject: 'filter must be a non-empty object', + }, + property: 'filter', + target: input, + }, + ]); + }); + + it('should validate the update input', () => { + const Type = UpdateManyArgsType(FakeFilter, FakeType); + const input = { filter: { name: { eq: 'hello world' } }, input: {} }; + const it = plainToClass(Type, input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [ + { + children: [], + constraints: { + minLength: 'name must be longer than or equal to 5 characters', + }, + property: 'name', + target: {}, + }, + ], + property: 'input', + target: it, + value: it.input, + }, + ]); + }); + }); }); diff --git a/packages/query-graphql/__tests__/types/update-one-args.type.spec.ts b/packages/query-graphql/__tests__/types/update-one-args.type.spec.ts index 0a9793c8b..e8521bc5d 100644 --- a/packages/query-graphql/__tests__/types/update-one-args.type.spec.ts +++ b/packages/query-graphql/__tests__/types/update-one-args.type.spec.ts @@ -1,13 +1,17 @@ import 'reflect-metadata'; import * as typeGraphql from 'type-graphql'; import { plainToClass } from 'class-transformer'; +import { validateSync, MinLength } from 'class-validator'; import { UpdateOneArgsType } from '../../src'; describe('UpdateOneInputType', (): void => { const argsTypeSpy = jest.spyOn(typeGraphql, 'ArgsType'); const fieldSpy = jest.spyOn(typeGraphql, 'Field'); - class FakeType {} + class FakeType { + @MinLength(5) + name!: string; + } it('should create an args type with the field as the type', () => { UpdateOneArgsType(FakeType); expect(argsTypeSpy).toBeCalledWith(); @@ -23,4 +27,65 @@ describe('UpdateOneInputType', (): void => { const it = plainToClass(Type, input); expect(it.input).toEqual(input.input); }); + + describe('validation', () => { + it('should validate id is defined is not empty', () => { + const Type = UpdateOneArgsType(FakeType); + const input = { input: { name: 'hello world' } }; + const it = plainToClass(Type, input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'id should not be empty', + }, + property: 'id', + target: input, + }, + ]); + }); + + it('should validate id is not empty is defined is not empty', () => { + const Type = UpdateOneArgsType(FakeType); + const input = { id: '', input: { name: 'hello world' } }; + const it = plainToClass(Type, input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [], + constraints: { + isNotEmpty: 'id should not be empty', + }, + property: 'id', + target: input, + value: input.id, + }, + ]); + }); + + it('should validate the update input', () => { + const Type = UpdateOneArgsType(FakeType); + const input = { id: 'id-1', input: {} }; + const it = plainToClass(Type, input); + const errors = validateSync(it); + expect(errors).toEqual([ + { + children: [ + { + children: [], + constraints: { + minLength: 'name must be longer than or equal to 5 characters', + }, + property: 'name', + target: {}, + }, + ], + property: 'input', + target: it, + value: it.input, + }, + ]); + }); + }); }); diff --git a/packages/query-graphql/src/resolvers/delete.resolver.ts b/packages/query-graphql/src/resolvers/delete.resolver.ts index 1cfe324fd..f2eb10b27 100644 --- a/packages/query-graphql/src/resolvers/delete.resolver.ts +++ b/packages/query-graphql/src/resolvers/delete.resolver.ts @@ -55,7 +55,7 @@ export const Deletable = (DTOClass: Class, opts: DeleteResolverOpts
DeleteOneResponse, { name: `deleteOne${baseName}` }, commonResolverOpts, opts.one ?? {}) async deleteOne(@Args() input: DO): Promise> { const deleteOne = await transformAndValidate(DO, input); - return this.service.deleteOne(deleteOne.id); + return this.service.deleteOne(deleteOne.input); } @ResolverMutation(() => DMR, { name: `deleteMany${pluralBaseName}` }, commonResolverOpts, opts.many ?? {}) diff --git a/packages/query-graphql/src/types/create-many-args.type.ts b/packages/query-graphql/src/types/create-many-args.type.ts index b9c19530b..cac2fc707 100644 --- a/packages/query-graphql/src/types/create-many-args.type.ts +++ b/packages/query-graphql/src/types/create-many-args.type.ts @@ -1,6 +1,6 @@ import { Class } from '@nestjs-query/core'; import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; +import { ValidateNested, ArrayNotEmpty } from 'class-validator'; import { ArgsType, Field } from 'type-graphql'; export interface CreateManyArgsType { @@ -11,6 +11,7 @@ export function CreateManyArgsType(ItemClass: Class): Class { @Type(() => ItemClass) + @ArrayNotEmpty() @ValidateNested({ each: true }) @Field(() => [ItemClass], { description: 'Array of records to create' }) input!: C[]; diff --git a/packages/query-graphql/src/types/delete-many-args.type.ts b/packages/query-graphql/src/types/delete-many-args.type.ts index 192135602..4649881cb 100644 --- a/packages/query-graphql/src/types/delete-many-args.type.ts +++ b/packages/query-graphql/src/types/delete-many-args.type.ts @@ -1,6 +1,6 @@ import { Filter, Class } from '@nestjs-query/core'; import { Field, ArgsType } from 'type-graphql'; -import { IsNotEmptyObject } from 'class-validator'; +import { IsNotEmptyObject, ValidateNested } from 'class-validator'; export interface DeleteManyArgsType { input: Filter; @@ -10,6 +10,7 @@ export function DeleteManyArgsType(FilterType: Class>): Class { @IsNotEmptyObject() + @ValidateNested() @Field(() => FilterType, { description: 'Filter to find records to delete' }) input!: Filter; } diff --git a/packages/query-graphql/src/types/delete-one-args.type.ts b/packages/query-graphql/src/types/delete-one-args.type.ts index 98fc33fb7..0ca1f64b7 100644 --- a/packages/query-graphql/src/types/delete-one-args.type.ts +++ b/packages/query-graphql/src/types/delete-one-args.type.ts @@ -3,7 +3,7 @@ import { Field, ID, ArgsType } from 'type-graphql'; import { IsNotEmpty } from 'class-validator'; export interface DeleteOneArgsType { - id: string | number; + input: string | number; } /** @internal */ @@ -16,7 +16,7 @@ export function DeleteOneArgsType(): Class { class DeleteOneArgs implements DeleteOneArgsType { @IsNotEmpty() @Field(() => ID, { description: 'The id of the record to delete.' }) - id!: string | number; + input!: string | number; } deleteOneArgsType = DeleteOneArgs; return deleteOneArgsType; diff --git a/packages/query-graphql/src/types/relations-args.type.ts b/packages/query-graphql/src/types/relations-args.type.ts index ac8dc7617..3cbfc538c 100644 --- a/packages/query-graphql/src/types/relations-args.type.ts +++ b/packages/query-graphql/src/types/relations-args.type.ts @@ -1,6 +1,6 @@ import { Class } from '@nestjs-query/core'; import { Field, ArgsType, ID } from 'type-graphql'; -import { IsNotEmpty } from 'class-validator'; +import { IsNotEmpty, ArrayNotEmpty, ArrayUnique } from 'class-validator'; export interface RelationsArgsType { id: string | number; @@ -20,7 +20,9 @@ export function RelationsArgsType(): Class { id!: string | number; @Field(() => [ID], { description: 'The ids of the relations.' }) - @IsNotEmpty() + @ArrayNotEmpty() + @ArrayUnique() + @IsNotEmpty({ each: true }) relationIds!: (string | number)[]; } relationsArgsType = RelationsArgs; diff --git a/packages/query-graphql/src/types/update-many-args.type.ts b/packages/query-graphql/src/types/update-many-args.type.ts index 3f512e979..38dcd7251 100644 --- a/packages/query-graphql/src/types/update-many-args.type.ts +++ b/packages/query-graphql/src/types/update-many-args.type.ts @@ -15,6 +15,7 @@ export function UpdateManyArgsType>( @ArgsType() class UpdateManyArgs implements UpdateManyArgsType { @IsNotEmptyObject() + @ValidateNested() @Field(() => FilterType, { description: 'Filter used to find fields to update' }) filter!: Filter;