Skip to content

Commit

Permalink
fix(query-graphql): Fixed empty object accepted by required filters
Browse files Browse the repository at this point in the history
  • Loading branch information
TriPSs committed May 27, 2022
1 parent 45e38fc commit f162cf3
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 2 deletions.
23 changes: 23 additions & 0 deletions examples/filters/e2e/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Connection } from 'typeorm';
import { TodoItemEntity } from '../src/todo-item/todo-item.entity';
import { executeTruncate } from '../../helpers';

const tables = ['todo_item'];
export const truncate = async (connection: Connection): Promise<void> => executeTruncate(connection, tables);

export const refresh = async (connection: Connection): Promise<void> => {
await truncate(connection);

const todoRepo = connection.getRepository(TodoItemEntity);

await todoRepo.save([
{ title: 'Create Nest App', completed: true },
{ title: 'Create Entity', completed: false },
{ title: 'Create Entity Service', completed: false },
{ title: 'Add Todo Item Resolver', completed: false },
{
title: 'How to create item With Sub Tasks',
completed: false
}
]);
};
24 changes: 24 additions & 0 deletions examples/filters/e2e/graphql-fragments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const todoItemFields = `
id
title
completed
description
`;

export const pageInfoField = `
pageInfo{
hasNextPage
hasPreviousPage
startCursor
endCursor
}
`;

export const edgeNodes = (fields: string): string => `
edges {
node{
${fields}
}
cursor
}
`;
110 changes: 110 additions & 0 deletions examples/filters/e2e/todo-item.resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { CursorConnectionType } from '@ptc-org/nestjs-query-graphql';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { Connection } from 'typeorm';
import { AppModule } from '../src/app.module';
import { TodoItemDTO } from '../src/todo-item/dto/todo-item.dto';
import { refresh } from './fixtures';
import { edgeNodes, pageInfoField, todoItemFields } from './graphql-fragments';

describe('TodoItemResolver (filters - e2e)', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule]
}).compile();

app = moduleRef.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
skipMissingProperties: false,
forbidUnknownValues: true
})
);

await app.init();
await refresh(app.get(Connection));
});

afterAll(() => refresh(app.get(Connection)));

describe('query', () => {
it(`should require "completed" filter`, () =>
request(app.getHttpServer())
.post('/graphql')
.send({
operationName: null,
variables: {},
query: `{
todoItems {
${pageInfoField}
${edgeNodes(todoItemFields)}
}
}`
})
.expect(400)
.then(({ body }) => {
expect(body.errors[0].message).toBe(
'Field "todoItems" argument "filter" of type "TodoItemFilter!" is required, but it was not provided.'
);
}));

it(`should accepted "completed" filter`, () =>
request(app.getHttpServer())
.post('/graphql')
.send({
operationName: null,
variables: {},
query: `{
todoItems (filter: { completed: { is: true } }) {
${pageInfoField}
${edgeNodes(todoItemFields)}
}
}`
})
.expect(200)
.then(({ body }) => {
const { edges, pageInfo }: CursorConnectionType<TodoItemDTO> = body.data.todoItems;
expect(pageInfo).toEqual({
endCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'YXJyYXljb25uZWN0aW9uOjA='
});
expect(edges).toHaveLength(1);

expect(edges.map((e) => e.node)).toEqual([
{ id: '1', title: 'Create Nest App', completed: true, description: null }
]);
}));

it(`should not accepted empty "completed" filter`, () =>
request(app.getHttpServer())
.post('/graphql')
.send({
operationName: null,
variables: {},
query: `{
todoItems (filter: { completed: { } }) {
${pageInfoField}
${edgeNodes(todoItemFields)}
}
}`
})
.expect(200)
.then(({ body }) => {
expect(body.errors[0].extensions.response.message[0]).toBe(
'filter.There was no filter provided for "completed"!'
);
}));
});

afterAll(async () => {
await app.close();
});
});
18 changes: 18 additions & 0 deletions examples/filters/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TodoItemModule } from './todo-item/todo-item.module';
import { typeormOrmConfig, formatGraphqlError } from '../../helpers';

@Module({
imports: [
TypeOrmModule.forRoot(typeormOrmConfig('basic')),
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
formatError: formatGraphqlError
}),
TodoItemModule
]
})
export class AppModule {
}
25 changes: 25 additions & 0 deletions examples/filters/src/todo-item/dto/todo-item.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { FilterableField } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime } from '@nestjs/graphql';

@ObjectType('TodoItem')
export class TodoItemDTO {
@FilterableField(() => ID)
id!: number;

@FilterableField()
title!: string;

@FilterableField({ nullable: true })
description?: string;

@FilterableField({
filterRequired: true
})
completed!: boolean;

@FilterableField(() => GraphQLISODateTime, { filterOnly: true })
created!: Date;

@FilterableField(() => GraphQLISODateTime, { filterOnly: true })
updated!: Date;
}
28 changes: 28 additions & 0 deletions examples/filters/src/todo-item/todo-item.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';

@Entity({ name: 'todo_item' })
export class TodoItemEntity {
@PrimaryGeneratedColumn()
id!: number;

@Column()
title!: string;

@Column({ nullable: true })
description?: string;

@Column()
completed!: boolean;

@CreateDateColumn()
created!: Date;

@UpdateDateColumn()
updated!: Date;
}
21 changes: 21 additions & 0 deletions examples/filters/src/todo-item/todo-item.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { Module } from '@nestjs/common';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoItemEntity } from './todo-item.entity';

@Module({
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity])],
resolvers: [
{
DTOClass: TodoItemDTO,
EntityClass: TodoItemEntity
}
]
})
]
})
export class TodoItemModule {
}
27 changes: 27 additions & 0 deletions packages/query-graphql/src/decorators/has-required.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FilterFieldComparison } from '@ptc-org/nestjs-query-core';
import { registerDecorator } from 'class-validator';

/**
* @internal
* Wraps Args to allow skipping decorating
* @param check - checker to run.
* @param decorators - The decorators to apply
*/
export function HasRequiredFilter<T>(): PropertyDecorator {
return function (object: object, propertyName: string): void {
registerDecorator({
name: 'hasRequiredFilter',
target: object.constructor,
propertyName: propertyName,
// constraints: [property],
options: {
message: 'There was no filter provided for "$property"!'
},
validator: {
validate(value: FilterFieldComparison<T>) {
return Object.keys(value).length > 0;
}
}
});
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export function getOrCreateBooleanFieldComparison(): Class<FilterFieldComparison
if (booleanFieldComparison) {
return booleanFieldComparison;
}

@InputType()
class BooleanFieldComparison implements FilterFieldComparison<boolean> {
@Field(() => Boolean, { nullable: true })
Expand All @@ -22,6 +23,8 @@ export function getOrCreateBooleanFieldComparison(): Class<FilterFieldComparison
@IsOptional()
isNot?: boolean | null;
}

booleanFieldComparison = BooleanFieldComparison;

return BooleanFieldComparison;
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export function getOrCreateDateFieldComparison(): Class<FilterFieldComparison<Da
@Type(() => DateFieldComparisonBetween)
notBetween?: DateFieldComparisonBetween;
}

dateFieldComparison = DateFieldComparison;

return dateFieldComparison;
}
6 changes: 4 additions & 2 deletions packages/query-graphql/src/types/query/filter.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { createFilterComparisonType } from './field-comparison';
import { getDTONames, getGraphqlObjectName } from '../../common';
import { getFilterableFields, getQueryOptions, getRelations, SkipIf } from '../../decorators';
import { isInAllowedList } from './helpers';
import { HasRequiredFilter } from '../../decorators/has-required.filter';

const reflector = new MapReflector('nestjs-query:filter-type');

Expand Down Expand Up @@ -47,8 +48,6 @@ function getOrCreateFilterType<T>(
class GraphQLFilter {
static hasRequiredFilters: boolean = hasRequiredFilters;

// TODO:: Add the required fields also in here and validate they are set!

@ValidateNested()
@SkipIf(() => isNotAllowedComparison('and'), Field(() => [GraphQLFilter], { nullable: true }))
@Type(() => GraphQLFilter)
Expand All @@ -70,6 +69,9 @@ function getOrCreateFilterType<T>(
});
const nullable = advancedOptions?.filterRequired !== true;
ValidateNested()(GraphQLFilter.prototype, propertyName);
if (advancedOptions?.filterRequired) {
HasRequiredFilter()(GraphQLFilter.prototype, propertyName);
}
Field(() => FC, { nullable })(GraphQLFilter.prototype, propertyName);
Type(() => FC)(GraphQLFilter.prototype, propertyName);
});
Expand Down

0 comments on commit f162cf3

Please sign in to comment.