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

fix(graphql): avoid partial field declarations from inherited metadata with cli plugin #3270

Merged
merged 2 commits into from
Oct 23, 2024

Conversation

CarsonF
Copy link
Contributor

@CarsonF CarsonF commented Jul 24, 2024

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

The former logic was accessing the inherited metadata factory, I guess when the current class didn't have one.
The parent class could have fields that the plugin cannot determine, and are explicitly declared with @Field decorators.
The partial info & the type from decorator are merged together for that base type.
But here when the subclass was redefining this field it only had partial info, no type.
This caused the complication logic to fail, because the typeFn would be missing.
Instead, we only want to look for the own metadata factory, and ignore if there is none.
Just to note: inherited fields are merged elsewhere (in factories), so this doesn't need to try.

A minimal reproduction is

// Base class that the plugin will generate metadata for, but it will be missing
// the `type` because it cannot be resolved by TS.
@ObjectType({ isAbstract: true })
export class Entity {
  @Field(() => String)
  id: unknown;
}

// In the middle is a non-exported / or dynamic class that the plugin ignores
// MetadataLoader will not assign a _GRAPHQL_METADATA_FACTORY property to this object
// because of that.
export function LivingBeing() {
  @ObjectType({ isAbstract: true })
  class LivingBeingClass extends Entity {}
  return LivingBeingClass;
}

// A concrete type that acts as the entrypoint for the bug.
@ObjectType()
export class Person extends LivingBeing() {}

With this example, Person & Entity will have metadata loaded / _GRAPHQL_METADATA_FACTORY property assigned.
The problem is that

LivingBeingClass.constructor[METADATA_FACTORY_NAME]

resolves to Entity's _GRAPHQL_METADATA_FACTORY 💥 , but it's registered as the LivingBeingClass type.
⭐ The logic should be to skip because no _GRAPHQL_METADATA_FACTORY is declared for LivingBeingClass.

Entity's metadata is merged with the decorator, so the plugin doesn't provide the type, but the decorator does.
However, LivingBeingClass does not have this field metadata merged since the Entity.id decorator is not its own (guessing on why).
The factories throw because LivingBeingClass.id does not have a type even though its parent class does.

The metadata should ignore this field for this intermediate class since it doesn't do anything with it - it just inherits.

What is the new behavior?

Error is avoided. Fields are still merged/inherited as expected.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

I also added a check to avoid merging metadata for common parents.
i.e. TypeMetadataStorage.addClassFieldMetadata would previous be called for Entity.id for every concrete type extending Entity. Which is redundant work after the first time.

…a with cli plugin

The former logic was accessing the inherited metadata factory, I guess when the current class didn't have one.
The parent class could have fields that the plugin cannot determine, and are explicitly declared with `@Field` decorators.
The partial info & the type from decorator are merged together for that base type.
But here when the subclass was redefining this field it only had partial info, no type.
This caused the complication logic to fail, because the `typeFn` would be missing.
Instead, we only want to look for the _own_ metadata factory, and ignore if there is none.
Just to note: inherited fields are merged elsewhere (in factories), so this doesn't need to try.
}
seen.add(prototype.constructor);

const metadata = Object.getOwnPropertyDescriptor(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: any specific reason why we use getOwnPropertyDescriptor now?

Copy link
Contributor Author

@CarsonF CarsonF Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is basically the fix. We only want the metadata factory from the referenced class. Static properties (of ES6 classes) are inherited, and we want to avoid this here.
This could be an Object.hasOwn check. I used this to do that check and get the own value at the same time so the constant METADATA_FACTORY_NAME only had to be referenced once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I clarified the PR description. The minimal reproduction is still a lot of boilerplate.
I looked at updating tests, but I didn't find a spot where I could using the plugin metadata and instantiate a GraphQLModule using it.
What would you like me to do?

@CarsonF
Copy link
Contributor Author

CarsonF commented Sep 26, 2024

Bump 🙏

@kamilmysliwiec kamilmysliwiec merged commit 2bf8f8b into nestjs:master Oct 23, 2024
1 check passed
@CarsonF CarsonF deleted the bugfix/inherited-metadata branch October 23, 2024 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants