Skip to content

Commit

Permalink
Fixed all comments
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhik Mitra authored and Abhik Mitra committed Sep 2, 2018
1 parent 5e2bcf5 commit ac65c7a
Show file tree
Hide file tree
Showing 17 changed files with 312 additions and 13 deletions.
35 changes: 35 additions & 0 deletions @types/graphql-query-complexity/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

declare module "graphql-query-complexity" {
import {QueryComplexityOptions} from 'graphql-query-complexity/dist/QueryComplexity';
import QueryComplexity from 'graphql-query-complexity/dist/QueryComplexity';
import { ValidationContext } from 'graphql';
type returnType = (context: ValidationContext) => QueryComplexity ;
export default function createQueryComplexityValidator(options: QueryComplexityOptions): returnType;
}
declare module "graphql-query-complexity/dist/QueryComplexity" {
import { ValidationContext, FragmentDefinitionNode, OperationDefinitionNode, FieldNode, InlineFragmentNode } from 'graphql';
import { GraphQLUnionType, GraphQLObjectType, GraphQLInterfaceType, GraphQLError } from 'graphql';
export interface QueryComplexityOptions {
maximumComplexity: number;
variables?: Object;
onComplete?: (complexity: number) => void;
createError?: (max: number, actual: number) => GraphQLError;
}
export default class QueryComplexity {
context: ValidationContext;
complexity: number;
options: QueryComplexityOptions;
fragments: {
[name: string]: FragmentDefinitionNode;
};
OperationDefinition: Object;
constructor(context: ValidationContext, options: QueryComplexityOptions);
onOperationDefinitionEnter(operation: OperationDefinitionNode): void;
onOperationDefinitionLeave(): GraphQLError | undefined;
nodeComplexity(node: FieldNode | FragmentDefinitionNode | InlineFragmentNode | OperationDefinitionNode, typeDef: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType, complexity?: number): number;
createError(): GraphQLError;
getDefaultComplexity(args: Object, childScore: number): number;
}

}

15 changes: 15 additions & 0 deletions examples/query-complexity/examples.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

query GetRecipesWithComplexityError {
recipes {
title
averageRating,
ratingsCount
}
}

mutation GetRecipesWithoutComplexityError {
recipes {
title
averageRating
}
}
Empty file.
46 changes: 46 additions & 0 deletions examples/query-complexity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import "reflect-metadata";
import { GraphQLServer, Options } from "graphql-yoga";
import { buildSchema } from "../../src";
import queryComplexity from "graphql-query-complexity";
import { RecipeResolver } from "./recipe-resolver";

async function bootstrap() {
// build TypeGraphQL executable schema
const schema = await buildSchema({
resolvers: [RecipeResolver],
});

// Create GraphQL server
const server = new GraphQLServer({ schema });

// Configure server options
const serverOptions: Options = {
port: 4000,
endpoint: "/graphql",
playground: "/playground",
validationRules: req => [
/**
* This provides GraphQL query analysis to reject complex queries to your GraphQL server.
* This can be used to protect your GraphQL servers
* against resource exhaustion and DoS attacks.
* More doucmentation can be found (here)[https://github.com/ivome/graphql-query-complexity]
*/
queryComplexity({
maximumComplexity: 8,
variables: req.query.variables,
onComplete: (complexity: number) => {
console.log("Query Complexity:", complexity);
},
}),
],
};

// Start the server
server.start(serverOptions, ({ port, playground }) => {
console.log(
`Server is running, GraphQL Playground available at http://localhost:${port}${playground}`,
);
});
}

bootstrap();
11 changes: 11 additions & 0 deletions examples/query-complexity/recipe-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Recipe } from "./recipe-type";
import { InputType, Field } from "../../src";

@InputType()
export class RecipeInput implements Partial<Recipe> {
@Field()
title: string;

@Field({ nullable: true })
description?: string;
}
51 changes: 51 additions & 0 deletions examples/query-complexity/recipe-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
Resolver,
Query,
FieldResolver,
Arg,
Root,
Mutation,
Float,
Int,
ResolverInterface,
} from "../../src";
import { plainToClass } from "class-transformer";

import { Recipe } from "./recipe-type";
import { RecipeInput } from "./recipe-input";
import { createRecipeSamples } from "./recipe-samples";

@Resolver(of => Recipe)
export class RecipeResolver implements ResolverInterface<Recipe> {
private readonly items: Recipe[] = createRecipeSamples();

@Query(returns => Recipe, { nullable: true })
async recipe(@Arg("title") title: string): Promise<Recipe | undefined> {
return await this.items.find(recipe => recipe.title === title);
}

@Query(returns => [Recipe], { description: "Get all the recipes from around the world " })
async recipes(): Promise<Recipe[]> {
return await this.items;
}

@Mutation(returns => Recipe, { complexity: 10 })
async addRecipe(@Arg("recipe") recipeInput: RecipeInput): Promise<Recipe> {
const recipe = plainToClass(Recipe, {
description: recipeInput.description,
title: recipeInput.title,
ratings: [],
creationDate: new Date(),
});
await this.items.push(recipe);
return recipe;
}

@FieldResolver()
ratingsCount(
@Root() recipe: Recipe,
@Arg("minRate", type => Int, { nullable: true }) minRate: number = 0.0,
): number {
return recipe.ratings.filter(rating => rating >= minRate).length;
}
}
26 changes: 26 additions & 0 deletions examples/query-complexity/recipe-samples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { plainToClass } from "class-transformer";

import { Recipe } from "./recipe-type";

export function createRecipeSamples() {
return plainToClass(Recipe, [
{
description: "Desc 1",
title: "Recipe 1",
ratings: [0, 3, 1],
creationDate: new Date("2018-04-11"),
},
{
description: "Desc 2",
title: "Recipe 2",
ratings: [4, 2, 3, 1],
creationDate: new Date("2018-04-15"),
},
{
description: "Desc 3",
title: "Recipe 3",
ratings: [5, 4],
creationDate: new Date(),
},
]);
}
49 changes: 49 additions & 0 deletions examples/query-complexity/recipe-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Field, ObjectType, Int, Float } from "../../src";

@ObjectType({ description: "Object representing cooking recipe" })
export class Recipe {
/*
By default, every field gets a complexity of 1.
Which can be customized by passing the complexity parameter
*/
@Field({ complexity: 6 })
title: string;

@Field(type => String, { nullable: true, deprecationReason: "Use `description` field instead" })
get specification(): string | undefined {
return this.description;
}

@Field({ complexity: 5, nullable: true, description: "The recipe" })
description?: string;

@Field(type => [Int])
ratings: number[];

@Field()
creationDate: Date;

@Field(type => Int)
ratingsCount: number;

@Field(type => Float, {
nullable: true,
/*
By default, every field gets a complexity of 1.
You can also pass a calculation function in the complexity option
to determine a custom complexity.
This function will provide the complexity of
the child nodes as well as the field input arguments.
That way you can make a more realistic estimation of individual field complexity values:
*/
complexity: (args, childComplexity) => childComplexity + 1,
})
get averageRating(): number | null {
const ratingsCount = this.ratings.length;
if (ratingsCount === 0) {
return null;
}
const ratingsSum = this.ratings.reduce((a, b) => a + b, 0);
return ratingsSum / ratingsCount;
}
}
1 change: 0 additions & 1 deletion examples/simple-usage/graphql-query-complexity.d.ts

This file was deleted.

4 changes: 2 additions & 2 deletions examples/simple-usage/recipe-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { Field, ObjectType, Int, Float } from "../../src";

@ObjectType({ description: "Object representing cooking recipe" })
export class Recipe {
@Field({ complexity: 5 })
@Field()
title: string;

@Field(type => String, { nullable: true, deprecationReason: "Use `description` field instead" })
get specification(): string | undefined {
return this.description;
}

@Field({ complexity: 5, nullable: true, description: "The recipe" })
@Field({ nullable: true, description: "The recipe description with preparation info" })
description?: string;

@Field(type => [Int])
Expand Down
4 changes: 2 additions & 2 deletions src/decorators/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GraphQLScalarType } from "graphql";
import { ValidatorOptions } from "class-validator";

import { ResolverFilterData, ClassType } from "../interfaces";
import { ResolverFilterData, ClassType, ComplexityResolver } from "../interfaces";

export type TypeValue = ClassType | GraphQLScalarType | Function | object | symbol;
export type ReturnTypeFuncValue = TypeValue | [TypeValue];
Expand All @@ -17,7 +17,7 @@ export type SubscriptionFilterFunc = (

export interface DecoratorTypeOptions {
nullable?: boolean;
complexity?: number;
complexity?: number | ComplexityResolver;
}
export interface TypeOptions extends DecoratorTypeOptions {
array?: boolean;
Expand Down
11 changes: 11 additions & 0 deletions src/interfaces/ComplexityTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { GraphQLFieldConfig, GraphQLError } from "graphql";

export type ComplexityResolver = (args: any, complexity: number) => number;

type ComplexityGraphQLFieldConfig<TSource, TContext> = GraphQLFieldConfig<TSource, TContext> & {
complexity?: ComplexityResolver | number;
};

export interface ComplexityGraphQLFieldConfigMap<TSource, TContext> {
[key: string]: ComplexityGraphQLFieldConfig<TSource, TContext>;
}
1 change: 1 addition & 0 deletions src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./Publisher";
export * from "./ResolverData";
export * from "./ResolverFilterData";
export * from "./ResolverInterface";
export * from "./ComplexityTypes";
10 changes: 5 additions & 5 deletions src/schema/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
} from "../resolvers/create";
import { BuildContext, BuildContextOptions } from "./build-context";
import { UnionResolveTypeError, GeneratingSchemaError } from "../errors";
import { ResolverFilterData } from "../interfaces";
import { ResolverFilterData, ComplexityGraphQLFieldConfigMap } from "../interfaces";
import { getFieldMetadataFromInputType, getFieldMetadataFromObjectType } from "./utils";

interface ObjectTypeInfo {
Expand Down Expand Up @@ -216,7 +216,7 @@ export abstract class SchemaGenerator {
return interfaces;
},
fields: () => {
let fields = objectType.fields!.reduce<GraphQLFieldConfigMap<any, any>>(
let fields = objectType.fields!.reduce<ComplexityGraphQLFieldConfigMap<any, any>>(
(fieldsMap, field) => {
const fieldResolverMetadata = getMetadataStorage().fieldResolvers.find(
resolver =>
Expand All @@ -225,7 +225,7 @@ export abstract class SchemaGenerator {
(resolver.resolverClassMetadata === undefined ||
resolver.resolverClassMetadata.isAbstract === false),
);
const fieldConfig: GraphQLFieldConfig<any, any> & { complexity?: number } = {
fieldsMap[field.schemaName] = {
type: this.getGraphQLOutputType(field.name, field.getType(), field.typeOptions),
complexity: field.typeOptions.complexity || 1,
args: this.generateHandlerArgs(field.params!),
Expand All @@ -235,7 +235,6 @@ export abstract class SchemaGenerator {
description: field.description,
deprecationReason: field.deprecationReason,
};
fieldsMap[field.schemaName] = fieldConfig;
return fieldsMap;
},
{},
Expand Down Expand Up @@ -345,7 +344,7 @@ export abstract class SchemaGenerator {
private static generateHandlerFields<T = any, U = any>(
handlers: ResolverMetadata[],
): GraphQLFieldConfigMap<T, U> {
return handlers.reduce<GraphQLFieldConfigMap<T, U>>((fields, handler) => {
return handlers.reduce<ComplexityGraphQLFieldConfigMap<T, U>>((fields, handler) => {
// omit emitting abstract resolver fields
if (handler.resolverClassMetadata && handler.resolverClassMetadata.isAbstract) {
return fields;
Expand All @@ -360,6 +359,7 @@ export abstract class SchemaGenerator {
resolve: createHandlerResolver(handler),
description: handler.description,
deprecationReason: handler.deprecationReason,
complexity: handler.returnTypeOptions.complexity,
};
return fields;
}, {});
Expand Down
13 changes: 13 additions & 0 deletions tests/functional/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { getMetadataStorage } from "../../src/metadata/getMetadataStorage";
import { getSchemaInfo } from "../helpers/getSchemaInfo";
import { ObjectType, Field, Query, Resolver } from "../../src";
import { FieldMetadata } from "../../src/metadata/definitions";

describe("Fields - schema", () => {
let schemaIntrospection: IntrospectionSchema;
Expand Down Expand Up @@ -55,6 +56,9 @@ describe("Fields - schema", () => {

@Field({ name: "overwrittenName", nullable: true })
overwrittenStringField: string;

@Field({ name: "complexField", complexity: 10 })
complexField: string;
}

@Resolver(of => SampleObject)
Expand Down Expand Up @@ -83,6 +87,15 @@ describe("Fields - schema", () => {
return (fieldType.type as IntrospectionNonNullTypeRef).ofType! as IntrospectionNamedTypeRef;
}

it("should add complexity to the metadata storage", async () => {
const metadatStorage = getMetadataStorage();
const sampleObj = metadatStorage.objectTypes.find(it => it.name === "SampleObject")!;
const complexField = (sampleObj.fields as FieldMetadata[]).find(
it => it.name === "complexField",
);
expect((complexField as FieldMetadata).typeOptions.complexity).toBe(10);
});

// tests
it("should generate schema without errors", async () => {
expect(schemaIntrospection).toBeDefined();
Expand Down
Loading

0 comments on commit ac65c7a

Please sign in to comment.