Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support defaultValue for input types #203

Merged
merged 18 commits into from
Dec 15, 2018
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/simple-usage/recipe-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class RecipeResolver implements ResolverInterface<Recipe> {
@FieldResolver()
ratingsCount(
@Root() recipe: Recipe,
@Arg("minRate", type => Int, { nullable: true }) minRate: number = 0.0,
@Arg("minRate", type => Int, { nullable: true, defaultValue: 0.0 }) minRate: number,
): number {
return recipe.ratings.filter(rating => rating >= minRate).length;
}
Expand Down
2 changes: 1 addition & 1 deletion examples/simple-usage/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Recipe {
description: String
ratings: [Int!]!
creationDate: DateTime!
ratingsCount(minRate: Int): Int!
ratingsCount(minRate: Int = 0): Int!
averageRating: Float
}

Expand Down
1 change: 1 addition & 0 deletions src/decorators/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type SubscriptionTopicFunc = (

export interface DecoratorTypeOptions {
nullable?: boolean;
defaultValue?: any;
}
export interface TypeOptions extends DecoratorTypeOptions {
array?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function wrapWithTypeOptions<T extends GraphQLType>(
if (typeOptions.array) {
gqlType = new GraphQLList(new GraphQLNonNull(gqlType));
}
if (!typeOptions.nullable) {
if (!typeOptions.nullable && typeOptions.defaultValue === undefined) {
gqlType = new GraphQLNonNull(gqlType);
}
return gqlType as T;
Expand Down
32 changes: 32 additions & 0 deletions src/schema/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,22 @@ export abstract class SchemaGenerator {
}
}

private static getDefaultValue(instance: any, name: string, { defaultValue }: TypeOptions) {
const implicitDefaultValue = instance[name];
if (implicitDefaultValue === undefined) {
return defaultValue;
} else if (defaultValue === undefined) {
return implicitDefaultValue;
} else if (defaultValue !== implicitDefaultValue) {
throw new Error(
// tslint:disable-next-line:max-line-length
`The field ${name} has conflicting default values ${defaultValue} !== ${implicitDefaultValue}`,
);
}

return undefined;
}

private static buildTypesInfo() {
this.unionTypesInfo = getMetadataStorage().unions.map<UnionTypeInfo>(unionMetadata => {
return {
Expand Down Expand Up @@ -276,6 +292,7 @@ export abstract class SchemaGenerator {
);
return superClassTypeInfo ? superClassTypeInfo.type : undefined;
};
const inputInstance = new (inputType.target as any)();
return {
target: inputType.target,
type: new GraphQLInputObjectType({
Expand All @@ -284,9 +301,16 @@ export abstract class SchemaGenerator {
fields: () => {
let fields = inputType.fields!.reduce<GraphQLInputFieldConfigMap>(
(fieldsMap, field) => {
field.typeOptions.defaultValue = this.getDefaultValue(
inputInstance,
field.name,
field.typeOptions,
);

fieldsMap[field.schemaName] = {
description: field.description,
type: this.getGraphQLInputType(field.name, field.getType(), field.typeOptions),
defaultValue: field.typeOptions.defaultValue,
};
return fieldsMap;
},
Expand Down Expand Up @@ -410,6 +434,7 @@ export abstract class SchemaGenerator {
args[param.name] = {
description: param.description,
type: this.getGraphQLInputType(param.name, param.getType(), param.typeOptions),
defaultValue: param.typeOptions.defaultValue,
};
} else if (param.kind === "args") {
const argumentType = getMetadataStorage().argumentTypes.find(
Expand All @@ -433,10 +458,17 @@ export abstract class SchemaGenerator {
argumentType: ClassMetadata,
args: GraphQLFieldConfigArgumentMap = {},
) {
const argumentInstance = new (argumentType.target as any)();
argumentType.fields!.forEach(field => {
field.typeOptions.defaultValue = this.getDefaultValue(
argumentInstance,
field.name,
field.typeOptions,
);
args[field.schemaName] = {
description: field.description,
type: this.getGraphQLInputType(field.name, field.getType(), field.typeOptions),
defaultValue: field.typeOptions.defaultValue,
};
});
}
Expand Down
146 changes: 143 additions & 3 deletions tests/functional/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ValidationContext,
visit,
visitWithTypeInfo,
IntrospectionInputObjectType,
} from "graphql";
import * as path from "path";
import { plainToClass } from "class-transformer";
Expand Down Expand Up @@ -56,6 +57,8 @@ describe("Resolvers", () => {
let mutationType: IntrospectionObjectType;
let sampleObjectType: IntrospectionObjectType;
let argMethodField: IntrospectionField;
let sampleInputType: IntrospectionInputObjectType;
let sampleInputChildType: IntrospectionInputObjectType;

beforeAll(async () => {
getMetadataStorage().clear();
Expand All @@ -64,6 +67,18 @@ describe("Resolvers", () => {
class SampleInput {
@Field()
field: string;
@Field({ defaultValue: "defaultStringFieldDefaultValue" })
defaultStringField?: string;
@Field()
implicitDefaultStringField?: string = "implicitDefaultStringFieldDefaultValue";
}

@InputType()
class SampleInputChild extends SampleInput {
@Field({ defaultValue: "defaultValueOverwritten" })
defaultStringField?: string;
@Field()
implicitDefaultStringField?: string = "implicitDefaultValueOverwritten";
}

@ArgsType()
Expand All @@ -74,6 +89,10 @@ describe("Resolvers", () => {
numberArg: number;
@Field()
inputObjectArg: SampleInput;
@Field({ defaultValue: "defaultStringArgDefaultValue" })
defaultStringArg?: string;
@Field()
implicitDefaultStringArg?: string = "implicitDefaultStringArgDefaultValue";
}

@ObjectType()
Expand All @@ -100,10 +119,13 @@ describe("Resolvers", () => {
@Arg("booleanArg") booleanArg: boolean,
@Arg("numberArg") numberArg: number,
@Arg("inputArg") inputArg: SampleInput,
@Arg("inputChildArg") inputChildArg: SampleInputChild,
@Arg("explicitNullableArg", type => String, { nullable: true }) explicitNullableArg: any,
@Arg("stringArrayArg", type => String) stringArrayArg: string[],
@Arg("explicitArrayArg", type => [String]) explicitArrayArg: any,
@Arg("nullableStringArg", { nullable: true }) nullableStringArg?: string,
@Arg("defaultStringArg", { defaultValue: "defaultStringArgDefaultValue" })
defaultStringArg?: string,
benawad marked this conversation as resolved.
Show resolved Hide resolved
): any {
return "argMethodField";
}
Expand Down Expand Up @@ -224,6 +246,12 @@ describe("Resolvers", () => {
type => type.name === "SampleObject",
) as IntrospectionObjectType;
argMethodField = sampleObjectType.fields.find(field => field.name === "argMethodField")!;
sampleInputType = schemaIntrospection.types.find(
benawad marked this conversation as resolved.
Show resolved Hide resolved
field => field.name === "SampleInput",
)! as IntrospectionInputObjectType;
sampleInputChildType = schemaIntrospection.types.find(
field => field.name === "SampleInputChild",
)! as IntrospectionInputObjectType;
});

// helpers
Expand Down Expand Up @@ -270,7 +298,7 @@ describe("Resolvers", () => {
const argMethodFieldInnerType = argMethodFieldType.ofType as IntrospectionNamedTypeRef;

expect(argMethodField.name).toEqual("argMethodField");
expect(argMethodField.args).toHaveLength(8);
expect(argMethodField.args).toHaveLength(10);
expect(argMethodFieldType.kind).toEqual(TypeKind.NON_NULL);
expect(argMethodFieldInnerType.kind).toEqual(TypeKind.SCALAR);
expect(argMethodFieldInnerType.name).toEqual("String");
Expand Down Expand Up @@ -375,6 +403,92 @@ describe("Resolvers", () => {
expect(inputArgInnerType.kind).toEqual(TypeKind.INPUT_OBJECT);
expect(inputArgInnerType.name).toEqual("SampleInput");
});

it("should generate nullable string arg type with defaultValue for object field method", async () => {
const inputArg = argMethodField.args.find(arg => arg.name === "defaultStringArg")!;
const defaultValueStringArgType = inputArg.type as IntrospectionNamedTypeRef;

expect(inputArg.defaultValue).toBe('"defaultStringArgDefaultValue"');
benawad marked this conversation as resolved.
Show resolved Hide resolved
expect(defaultValueStringArgType.kind).toEqual(TypeKind.SCALAR);
expect(defaultValueStringArgType.name).toEqual("String");
});
});

describe("Input object", () => {
it("should generate nullable string arg type with defaultValue for input object field", async () => {
const defaultValueStringField = sampleInputType.inputFields.find(
arg => arg.name === "defaultStringField",
)!;
const defaultValueStringFieldType = defaultValueStringField.type as IntrospectionNamedTypeRef;

expect(defaultValueStringField.defaultValue).toBe('"defaultStringFieldDefaultValue"');
expect(defaultValueStringFieldType.kind).toEqual(TypeKind.SCALAR);
expect(defaultValueStringFieldType.name).toEqual("String");
});

it("should generate nullable string arg type with implicit defaultValue for input object field", async () => {
const implicitDefaultValueStringField = sampleInputType.inputFields.find(
arg => arg.name === "implicitDefaultStringField",
)!;
const implicitDefaultValueStringFieldType = implicitDefaultValueStringField.type as IntrospectionNamedTypeRef;

expect(implicitDefaultValueStringField.defaultValue).toBe(
'"implicitDefaultStringFieldDefaultValue"',
);
expect(implicitDefaultValueStringFieldType.kind).toEqual(TypeKind.SCALAR);
expect(implicitDefaultValueStringFieldType.name).toEqual("String");
});

it("should overwrite defaultValue in child input object", async () => {
const defaultValueStringField = sampleInputChildType.inputFields.find(
arg => arg.name === "defaultStringField",
)!;
const defaultValueStringFieldType = defaultValueStringField.type as IntrospectionNamedTypeRef;

expect(defaultValueStringField.defaultValue).toBe('"defaultValueOverwritten"');
expect(defaultValueStringFieldType.kind).toEqual(TypeKind.SCALAR);
expect(defaultValueStringFieldType.name).toEqual("String");
});

it("should overwrite implicit defaultValue in child input object", async () => {
const implicitDefaultValueStringField = sampleInputChildType.inputFields.find(
arg => arg.name === "implicitDefaultStringField",
)!;
const implicitDefaultValueStringFieldType = implicitDefaultValueStringField.type as IntrospectionNamedTypeRef;

expect(implicitDefaultValueStringField.defaultValue).toBe(
'"implicitDefaultValueOverwritten"',
);
expect(implicitDefaultValueStringFieldType.kind).toEqual(TypeKind.SCALAR);
expect(implicitDefaultValueStringFieldType.name).toEqual("String");
});

it("should throw error when defaultValue in decorator doesn't match implicit defaultValue in input type", async () => {
expect.assertions(2);

@InputType()
class InputWithDefaultField {
@Field({ defaultValue: "explicitDefaultValue" })
twoDefaultValuesField: string = "implicitDefaultValue";
}

try {
@Resolver()
class SampleResolver {
@Query(() => String)
sampleQuery(@Arg("inputArg") inputArg: InputWithDefaultField): string {
return "argQuery";
}
}
await buildSchema({
resolvers: [SampleResolver],
});
} catch (err) {
expect(err).toBeInstanceOf(Error);
const error = err as Error;
expect(error.message).toContain("conflicting default values");
}
});
});

describe("Args object", () => {
Expand All @@ -389,6 +503,32 @@ describe("Resolvers", () => {
expect(stringArgInnerType.name).toEqual("String");
});

it("should generate nullable type arg with defaultValue from args object field", async () => {
const argsQuery = getQuery("argsQuery");
const defaultStringArg = argsQuery.args.find(arg => arg.name === "defaultStringArg")!;
const defaultStringArgType = defaultStringArg.type as IntrospectionNamedTypeRef;

expect(defaultStringArg.name).toEqual("defaultStringArg");
expect(defaultStringArg.defaultValue).toEqual('"defaultStringArgDefaultValue"');
expect(defaultStringArgType.kind).toEqual(TypeKind.SCALAR);
expect(defaultStringArgType.name).toEqual("String");
});

it("should generate nullable type arg with implicit defaultValue from args object field", async () => {
const argsQuery = getQuery("argsQuery");
const implicitDefaultStringArg = argsQuery.args.find(
arg => arg.name === "implicitDefaultStringArg",
)!;
const implicitDefaultStringArgType = implicitDefaultStringArg.type as IntrospectionNamedTypeRef;

expect(implicitDefaultStringArg.name).toEqual("implicitDefaultStringArg");
expect(implicitDefaultStringArg.defaultValue).toEqual(
'"implicitDefaultStringArgDefaultValue"',
);
expect(implicitDefaultStringArgType.kind).toEqual(TypeKind.SCALAR);
expect(implicitDefaultStringArgType.name).toEqual("String");
});

it("should generate nullable type arg from args object field", async () => {
const argsQuery = getQuery("argsQuery");
const numberArg = argsQuery.args.find(arg => arg.name === "numberArg")!;
Expand Down Expand Up @@ -588,14 +728,14 @@ describe("Resolvers", () => {
const argsQuery = getQuery("argsQuery");

expect(argsQuery.name).toEqual("argsQuery");
expect(argsQuery.args).toHaveLength(3);
expect(argsQuery.args).toHaveLength(5);
});

it("should generate proper definition for query with both @Arg and @Args", async () => {
const argAndArgsQuery = getQuery("argAndArgsQuery");

expect(argAndArgsQuery.name).toEqual("argAndArgsQuery");
expect(argAndArgsQuery.args).toHaveLength(4);
expect(argAndArgsQuery.args).toHaveLength(6);
});
});

Expand Down