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

New features for GraphQL typescript plugin #1757

Merged
merged 20 commits into from Sep 1, 2022

Conversation

thekip
Copy link
Contributor

@thekip thekip commented Sep 22, 2021

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

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

What is the current behavior?

Currently GraphQL Plugin does not understand @deprecated tag in JsDoc comments for fields.

Issue Number: #1346

What is the new behavior?

This PR adds support for @deprecated tag. It parses it in following fashion:

  1. If just tag is presented, with no message then 'deprecated' default message would be used
  2. If user specify a deprecation message after a tag:
  /**
  * @deprecated consult docs for better alternative!
  */
  breed: string;

Message would be used.

I'm using built-in typescript jsdoc support (may be need to check from what version typescript started support it) to parse jsdoc and get information of tags.

Does this PR introduce a breaking change?

[ ] Yes
[x] No

There are no general breaking changes, but schema might be fulfilled with unintended deprecation messages, and this could be quite surprising for some.

Other information

This PR is rethinking of #1751 Which is affecting bigger scope and might introduce breaking changes. I decided to roll out features one by one in small PR's instead.

Stay updated, i have many new useful features in my plan :)

@thekip thekip changed the title Feature/graphql plugin Support @deprecated in GraphQL plugin Sep 22, 2021
@thekip
Copy link
Contributor Author

thekip commented Sep 22, 2021

About refactoring

Generally using all ts.createXYZ method of the compiler is deprecated. We should use corresponding methods from NodeFactory passed as context to transformation eq 'ctx.factory.createXYZ'

Some of the methods has 1 to 1 replacement, some of them have slightly changed name and few no more exists (createLiteral(string | bolean | number), createMutableClone, and etc)

@thekip
Copy link
Contributor Author

thekip commented Sep 29, 2021

@kamilmysliwiec any feedback on this?

@kamilmysliwiec
Copy link
Member

I'll take a look as soon as I can

Prior to this change typescript transform plugin processed all classes in
files with a matching names. This is probably not desirable behaviour
because there might be classes not exposed to graphql and they should not be processed

This change makes processing a bit smarter, so it only processes classes
decorated with ObjectType / InterfaceType and InputType decorators.
@thekip
Copy link
Contributor Author

thekip commented Oct 17, 2021

2 new features added:

  1. Plugin now supports extracting description on a type level. Example:
/** Test1 Description */
@ObjectType()
class Test1Model {}
  1. Plugin automatically creates "implicit" enums for inline string unions:
@ObjectType()
class PersonModel {
  type: 'driver' | 'manager' | 'client'; 
}

// equivalent of 

export type PersonModelTypeEnum {
 driver = 'driver'
 manager = 'manager'
 client = 'client'
}

registerEnumType(PersonModelTypeEnum, {name: 'PersonModelTypeEnum'});
 
@ObjectType()
class PersonModel {
  type: PersonModelTypeEnum; 
}

This is drastically reduce a boilerplate in cases where full-featured enum is not needed.

The name for enum created as ObjectTypeName + capitalize(fieldName) + Enum.
Because GraphQL doesn't support spaces in enums, it replace it with underscore symbol:

@ObjectType()
class PersonModel {
  type: 'truck driver' | 'manager'; 
}

// will produce: 
export type PersonModelTypeEnum {
 truck_driver = 'truck driver'
 manager = 'manager'
}

@thekip
Copy link
Contributor Author

thekip commented Oct 18, 2021

3 more features added:

  1. Auto discover name in registerEnumType()
// previously
registerEnumType(MyAwesomeEnum, {name: 'MyAwesomeEnum' })

// now (name is inhereted from Enum name)
registerEnumType(MyAwesomeEnum)
  1. Auto discover name for createUnionType()
// previously
const MyAwesomeUnion = createUnionType({name: 'MyAwesomeUnion', types: [Foo, Bar]})

// now (name is inhereted from variable name)
const MyAwesomeUnion = createUnionType({types: [Foo, Bar]})
  1. Auto discover enums (disabled by default). With this feature enabled all enums in processed files would be automatically exposed to graphql schema (registered with registerEnumType). All comments introspection features such as description and deprecation works for enums as well.
/** This enum represents possible statuses */
enum Status {
  /** This is status by default */
  ENABLED,
  DISABLED,
  /** @deprecated please use other options */
  OTHER,
}

// This will produce under the hood: 

registerEnumType(Status, {
  name: 'Status',
  description: 'This enum represents possible statuses',
  valuesMap: {
    ENABLED: {
      description: 'This is status by default'
    },
    OTHER: {
      deprecationReason: 'please use other options',
    }
  }
})

If you want to exclude particalure enum from exposing you need to add @private or @HideEnum jsDoc tag (the tags TBD).

/**
* @private
*/
enum Status {
    ENABLED,
    DISABLED
}
// OR

/**
* @HideEnum
*/
enum Status2 {
    ENABLED,
    DISABLED
}

@thekip thekip changed the title Support @deprecated in GraphQL plugin New features for GraphQL typescript plugin Oct 18, 2021
@thekip thekip changed the title New features for GraphQL typescript plugin WIP: New features for GraphQL typescript plugin Oct 18, 2021
@thekip thekip changed the title WIP: New features for GraphQL typescript plugin New features for GraphQL typescript plugin Oct 18, 2021
@thekip
Copy link
Contributor Author

thekip commented Oct 18, 2021

I checked plugin on our codebase found couple issues which fixed in previous two commits and it works without any issue even with { autoRegisterEnums: true }.
Duplicated calls of registerEnumTypes does not brake a runtime.

When code is downleveled to ES5 it automatically change all
usages of imported members except those which was added by transform.

This breaks `registerEnumType()` calls added by transform.

The solution is create an explicit dedicated import only for this method in the
transform and not relay on exisitng imports at all.
@thekip
Copy link
Contributor Author

thekip commented Oct 19, 2021

@kamilmysliwiec please take a look at this, i need a feedback. Am i moving in the right direction and is maintainers interested in such kind of changes in the project.

…eck something

Previous test used transpile-only code, which produced code not showing
actual transformation therefore this test didn't actually check that
imports correctly added
Nested types introspection was useless because runtime did not understand this code.
There is no way to use "anonymous" type literals in graphql schema

Add tests for replaceImportPath util, because current test suite didn't cover
all branches in it.
@thekip
Copy link
Contributor Author

thekip commented Oct 20, 2021

Changes since last time:

  1. Added more tests, and actually check "import management" when code is downleveled. Previously test used transpileModule which processes files in isolation one by one and not taking into account relation between them and therefore not covering "import management" at all.

  2. Improved heuristic of fields metadata gathering and now it will not even try to get a type of a field if user specified type in a decorator manually. This a) make execution slightly faster as we not need to execute additional checks, b) allow users to override behavior of a plugin if it's working incorrectly in some reason see introspectComments for graphql plugin causes wrong require paths in compiled js code #1643

@thekip
Copy link
Contributor Author

thekip commented Oct 20, 2021

Side topic: I'd love to have a chat with you on LN or elsewhere!

What is LN? 😅 We can actually plan a quick call, where i can explain how everethyng works and what thoughts and plans i have related to this.

@kamilmysliwiec
Copy link
Member

kamilmysliwiec commented Oct 20, 2021

What is LN? 😅

I meant Linkedin! Apologies

Do nestjs still support typescript version prior 4.2? Where i can find a minimal system requirements?

There should be "no minimal version", in theory of course. Practically, everyone should use >= 3.x (and if you use @nestjs/config then you need >= 4.1 as it relies on the Template Literal Types feature)

Will respond to other comments as soon as I can!

@thekip
Copy link
Contributor Author

thekip commented Oct 21, 2021

Well, for such things as a typescript transformation plugin we should define a "minimal version" because we a heavily relay on typescript compiler API which changes over time. So at least for the plugin we should define it and state it clearly in the docs (something like if you want to use typescript plugin you should have typescript > 4.2)

It also would be nice to have to setup test matrix with different typescript versions on different OS (i saw branches in source code which relay on different path windows/unix).

Side topic Error handling in transformation

Right now there are no such thing as error handling. If a transformation could not figure out a type for property it's just silently leave () => Object instead of type. This doesn't have any sense for runtime consumer and it shows error while trying to generate a scheme. Usually errors from scheme generator is quite inaccurate and may point user in wrong direction.

During compiling we have more contextual information (for example inline type literal is used) and we can generate more accurate errors which would point to exact place in source file.

Example:

GraphqlPluginError: Property test in CatDto class has unsupported type (TypeLiteralExpression). __ additional message helping to solve the issue __

The main problem here is how to throw this exceptions. We could not just throw it from the transformation because it will exit a typescript compilation. (not a desirable behaviour in watch mode). Any way need to investigate on topic "How to properly report errors from typesctipt transform"

For me obvious option is generate throw new Error(...) and insert it into sourcecode and throwing errors from the runtime. (errors should be inserted in top level of a module, to be executed as early as possible)

What do you think? Does it make any sense?

@kamilmysliwiec
Copy link
Member

Well, for such things as a typescript transformation plugin we should define a "minimal version" because we a heavily relay on typescript compiler API which changes over time. So at least for the plugin we should define it and state it clearly in the docs (something like if you want to use typescript plugin you should have typescript > 4.2)

For me obvious option is generate throw new Error(...) and insert it into sourcecode and throwing errors from the runtime. (errors should be inserted in top level of a module, to be executed as early as possible)

Both suggestions sound good to me

@thekip
Copy link
Contributor Author

thekip commented Nov 4, 2021

New feature

Plugin now can introspect type from getters:

@ObjectType()
class ObjectTypeModel {
  get prop(): string {
    return 'something';
  }
}

@kamilmysliwiec
Copy link
Member

Would you be able to rebase your changes @thekip? If you're busy, I can look into this instead

@thekip
Copy link
Contributor Author

thekip commented Jan 24, 2022

I will

…lugin

# Conflicts:
#	.prettierignore
#	packages/graphql/lib/plugin/utils/ast-utils.ts
#	packages/graphql/lib/plugin/visitors/model-class.visitor.ts
#	packages/graphql/tests/plugin/fixtures/nullable.dto.ts
#	packages/graphql/tests/plugin/model-class-visitor.spec.ts
@thekip
Copy link
Contributor Author

thekip commented Feb 11, 2022

@kamilmysliwiec i synchonized the branch with current master. Unfortunately i was not able to rebase and keep branch clean, due to file structure changes and re-formatting files. I hope i didn't miss anything.

Please take a look, and let's discuss future of the branch and typescript plugin overall.

@thekip
Copy link
Contributor Author

thekip commented Feb 11, 2022

I also have quite interesting and 'brave' ideas how to improve DX with code-first approach avoiding sharp edges with typescript compiler and etc. I would like to share it with you because scope of changes might be quite big.

@ggagosh
Copy link

ggagosh commented Jun 28, 2022

Hey @kamilmysliwiec is there any chance that this request merged near future? Without this, the plugin only works with primitive types and can't use ObjectId for example. 😞

@thekip
Copy link
Contributor Author

thekip commented Jun 29, 2022

@kamilmysliwiec if some features are controversial, we can drop them and merge only those which are more predictable and can bring more value.

@thekip
Copy link
Contributor Author

thekip commented Jul 4, 2022

I synchronized the branch with the latest upstream, but e2e tests still failing with Killed 137 reason. Quick googling pointing that this is most likely out of memory on CircleCi PetroFit/petrofit#63 (locally all tests passed just fine)

I have no idea what changes exactly causing this out of memory error. I've checked the code and failing package (packages/apollo) even not using typescript plugin at all, so my changes shouldn't cause any issue with these tests.
Or probably it's because they run in parallel and together consume more memory than before and apollo package just have a bad luck.

Anyway, is it something actionable from my side?

@thekip
Copy link
Contributor Author

thekip commented Jul 8, 2022

I linked forked plugin to our app, and found an issue introduced with this commit 3fe7cdb

Looks like something was changed in the mechanism of merging factories provided from the cli plugin and runtime decorators. Latest commit fixes the issue. Now it works on our moderate-sized codebase.

@kamilmysliwiec
Copy link
Member

I'd love to merge this PR but I feel like we're trying to bring too many features at once. If we could cherry-pick the following changes for now:

  1. Support for the @deprecated tag
  2. Migration to use ctx.factory instead of ts.[..]
  3. Support extracting description on a type level

that would be fantastic! Afterward, we could think about changes around enums/unions (preferably in separate PRs - for example - auto-generating implicit enums for inline string unions sounds very useful as well and I'd love to get this one released under a flag for now). I understand that this (reverting other changes) might be somewhat time-consuming so let me know if you're willing to work on this one and I can try to cherry-pick the rest by myself! @thekip

@thekip
Copy link
Contributor Author

thekip commented Aug 26, 2022

Ok, i will take care of that

@kamilmysliwiec kamilmysliwiec merged commit 6f2860c into nestjs:master Sep 1, 2022
@kamilmysliwiec
Copy link
Member

I went ahead and removed features that I've mentioned here #1757 (comment) Feel free to create separate PRs with those individual features 🙌

Fantastic PR, thanks for your contribution!

@EdouardBougon
Copy link
Contributor

EdouardBougon commented Sep 1, 2022

Since this new version "@nestjs/graphql": "10.1.1", it seems that the graphQL plugin doesn't introspect anymore the fields on Args class.

I just updated my code with the version, and all field on args disappear from the graphQL schema.

Anyone with the same issue ?

@kamilmysliwiec
Copy link
Member

@EdouardBougon could you create a new issue with a minimum reproduction repository?

@kamilmysliwiec
Copy link
Member

@EdouardBougon this line likely causes this issue https://github.com/nestjs/graphql/blob/master/packages/graphql/lib/plugin/visitors/model-class.visitor.ts#L33

Can you modify this array locally (adding ArgsType) and check if it fixes your issue?

@EdouardBougon
Copy link
Contributor

Doing it right now

@EdouardBougon
Copy link
Contributor

yes I confirm, it fix the issue

@EdouardBougon
Copy link
Contributor

@kamilmysliwiec Do you still need a repo to test the issue ?

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.

None yet

4 participants