-
-
Notifications
You must be signed in to change notification settings - Fork 671
fix: prevent enumerable undefined properties in InputType instances (complete fix for #475) #1789
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
base: master
Are you sure you want to change the base?
fix: prevent enumerable undefined properties in InputType instances (complete fix for #475) #1789
Conversation
Fixes MichalLytek#475 Previously, InputType instances created by convertToType() had all optional @field() decorated properties as enumerable, even when set to undefined and not provided in the GraphQL query. This caused Object.keys(), for...in, and spread operators to include properties that weren't actually provided. Root cause: The function used `Object.assign(new Target(), data)`, which called the constructor that initialized ALL decorated properties as enumerable undefined values. Solution: The fix now: 1. Calls the constructor to properly initialize instance fields 2. Removes undefined properties that weren't in the input data 3. Assigns the provided data via Object.assign() This ensures only properties present in the GraphQL input are enumerable, while preserving proper instance initialization.
|
I can't imagine a better prepared PR ❤️ Will try to take a look at the changed and tests today 🙌 |
| // 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]; |
Check failure
Code scanning / CodeQL
Remote property injection High
user-provided value
|
Thanks @MichalLytek! In full transparency I did have AI help with this. Still, I did dive deep to find the real root cause, and aimed to be as elaborate as possible. This issue surfaced for me after upgrading to Prisma 6 and using the fix by @ahmedhosnypro (mentioned in MichalLytek/typegraphql-prisma#475). Note on Implementation ApproachThe suggested solution in the current PR is a second attempt. - return Object.assign(new Target(), data);
+ // Create instance without calling constructor to avoid initializing
+ // all decorated properties as enumerable undefined values
+ const instance = Object.create(Target.prototype);
+ return Object.assign(instance, data);However, this broke existing tests because it prevented all constructor initialization, including legitimate instance fields that aren't GraphQL fields (e.g., instanceField = Math.random() used in tests). The suggested solution here calls the constructor but removes only the undefined properties that weren't provided. |
|
Update: I noticed that properties with undefined values may still appear in nested objects, causing: I'm looking into integrating a fix for that, but it might incur an overhead. |
TL;DR: False alarm - the fix works. @MichalLytek after further investigation, I can confirm that my concern about nested objects retaining undefined properties was a false alarm (I mistakenly tested with the latest release code instead of testing with my fix). The fix handles nested InputTypes correctly, and here's why: The
I verified this by testing with a real-world scenario involving nested InputTypes - the fix works correctly throughout the object tree. Edge case (not a practical concern)The only scenario where nested undefined properties could persist is if someone explicitly pre-instantiates nested objects in their InputType class definition: @InputType()
class Parent {
child: ChildInput = new ChildInput(); // Anti-pattern
}However, this is an anti-pattern for InputTypes since GraphQL inputs should come from the query, not defaults. This doesn't represent a real-world use case. Regarding the GitHub Code Scanning security warningI noticed GitHub flagged a "Remote property injection" warning on the PR. When trying to view details, the link leads to a 404 (https://github.com/MichalLytek/type-graphql/security/code-scanning/11), and the repo doesn't seem to have a SECURITY.md configured (https://github.com/MichalLytek/type-graphql/security). I believe this is a false positive - the code iterates over Please advise on how to proceed with this. Thanks. |
Description
Fixes #475 (and completes the partial fix from commit 6f64ac4)
TypeGraphQL's
@InputType()decorated classes were creating instances where ALL optional fields became enumerable properties set toundefined, even when not provided in the GraphQL query. This caused issues when usingObject.keys(),for...in, or spread operators on input objects.The Problem
Before This Fix
After This Fix
Root Cause
In
src/helpers/types.ts, theconvertToType()function was using:Calling
new Target()invokes the constructor, which initializes ALL@Field()decorated properties as enumerable withundefinedvalues.Object.assign()only overwrites properties present indata, leaving unprovided optional fields as enumerable undefined values.The Solution
The fix now:
This creates an object with only the properties from the GraphQL input as enumerable, while preserving proper instance field initialization.
Changes Made
Modified Files
src/helpers/types.ts- UpdatedconvertToType()function (added 9 lines)tests/functional/inputtype-enumerable-properties.ts- Added comprehensive testsCode Changes
export function convertToType(Target: Function, data: any) { if (data == null) { return data; } if (Target instanceof GraphQLScalarType) { return data; } if (simpleTypes.includes(data.constructor)) { return data; } if (data instanceof Target) { return data; } if (Array.isArray(data)) { 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); }Testing
New Test Cases
Added comprehensive test file
tests/functional/inputtype-enumerable-properties.tswith 4 test cases:Existing Tests
All existing tests pass (478 passed, 1 todo), confirming backward compatibility including tests that rely on instance field initialization.
Real-World Impact
This bug affects production code that:
Iterates over input properties:
Uses property existence checks:
Spreads input objects:
Deals with GraphQL client metadata:
__typenameRelated Issues and History
convertToType()convertToType()still had the issueBreaking Changes
None. This change makes the behavior more correct without breaking existing functionality:
input.optionalField) works identicallyVerification
The fix has been tested with:
__typename)