Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
13 changes: 12 additions & 1 deletion src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,18 @@
return data.map(item => convertToType(Target, item));
}

return Object.assign(new Target(), data);
// Create instance by calling constructor to initialize instance fields
const instance = new (Target as any)();

// Remove undefined properties that weren't provided in the input data
// This prevents optional @Field() decorated properties from being enumerable
for (const key of Object.keys(instance)) {
if (instance[key] === undefined && !(key in data)) {
delete instance[key];
}
}

return Object.assign(instance, data);
}

export function getEnumValuesMap<T extends object>(enumObject: T) {
Expand Down
168 changes: 168 additions & 0 deletions tests/functional/inputtype-enumerable-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import "reflect-metadata";
import { type GraphQLSchema, graphql } from "graphql";
import { Arg, Field, InputType, Query, Resolver, buildSchema } from "type-graphql";
import { getMetadataStorage } from "@/metadata/getMetadataStorage";

describe("InputType enumerable properties", () => {
let schema: GraphQLSchema;

beforeAll(async () => {
getMetadataStorage().clear();

@InputType()
class SampleInput {
@Field()
requiredField!: string;

@Field({ nullable: true })
optionalField?: string;

@Field({ nullable: true })
anotherOptional?: number;
}

@InputType()
class NestedInput {
@Field({ nullable: true })
optionalNested?: string;
}

@InputType()
class ParentInput {
@Field()
required!: string;

@Field(() => NestedInput, { nullable: true })
nested?: NestedInput;
}

@Resolver()
class SampleResolver {
@Query(() => String)
testSimpleInput(@Arg("input") input: SampleInput): string {
return JSON.stringify({
keys: Object.keys(input),
hasOptional: "optionalField" in input,
hasAnother: "anotherOptional" in input,
optionalValue: input.optionalField,
});
}

@Query(() => String)
testNestedInput(@Arg("input") input: ParentInput): string {
return JSON.stringify({
keys: Object.keys(input),
hasNested: "nested" in input,
});
}
}

schema = await buildSchema({
resolvers: [SampleResolver],
validate: false,
});
});

describe("optional fields not provided", () => {
it("should not create enumerable properties for undefined optional fields", async () => {
const query = `
query {
testSimpleInput(input: { requiredField: "test" })
}
`;

const result = await graphql({ schema, source: query });

expect(result.errors).toBeUndefined();
expect(result.data).toBeDefined();

const data = JSON.parse(result.data!.testSimpleInput as string);

// Only requiredField should be in Object.keys()
expect(data.keys).toEqual(["requiredField"]);

// Optional fields should not be enumerable
expect(data.hasOptional).toBe(false);
expect(data.hasAnother).toBe(false);

// But should still be accessible (undefined)
expect(data.optionalValue).toBeUndefined();
});

it("should handle nested InputTypes correctly", async () => {
const query = `
query {
testNestedInput(input: { required: "value" })
}
`;

const result = await graphql({ schema, source: query });

expect(result.errors).toBeUndefined();
expect(result.data).toBeDefined();

const data = JSON.parse(result.data!.testNestedInput as string);

// Only required field should be enumerable
expect(data.keys).toEqual(["required"]);

// Nested optional field should not be enumerable
expect(data.hasNested).toBe(false);
});
});

describe("optional fields provided", () => {
it("should include provided optional fields in Object.keys()", async () => {
const query = `
query {
testSimpleInput(input: { requiredField: "test", optionalField: "provided" })
}
`;

const result = await graphql({ schema, source: query });

expect(result.errors).toBeUndefined();
expect(result.data).toBeDefined();

const data = JSON.parse(result.data!.testSimpleInput as string);

// Both provided fields should be in Object.keys()
expect(data.keys).toContain("requiredField");
expect(data.keys).toContain("optionalField");

// Provided field should be enumerable
expect(data.hasOptional).toBe(true);

// Non-provided field should not be enumerable
expect(data.hasAnother).toBe(false);

// Value should be set
expect(data.optionalValue).toBe("provided");
});

it("should handle explicitly null values correctly", async () => {
const query = `
query {
testSimpleInput(input: { requiredField: "test", optionalField: null })
}
`;

const result = await graphql({ schema, source: query });

expect(result.errors).toBeUndefined();
expect(result.data).toBeDefined();

const data = JSON.parse(result.data!.testSimpleInput as string);

// Explicitly null field should be in Object.keys()
expect(data.keys).toContain("requiredField");
expect(data.keys).toContain("optionalField");

// Should be enumerable
expect(data.hasOptional).toBe(true);

// Value should be null (not undefined)
expect(data.optionalValue).toBeNull();
});
});
});
Loading