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

feat: Add Schema Directives to SDL #952

Merged
merged 12 commits into from
May 15, 2022
Merged

Conversation

tgriesser
Copy link
Member

@tgriesser tgriesser commented Jul 21, 2021

Closes: #947, #53

Helps with: #148

WIP: Still needs docs / additional tests / cleanup of a bit of the builder layer / fixing types

Adds a directives property to all definition props which takes an array of directives. Adds them to an astNode which are then printed via a custom schema printer.

Definition:

const someDirective = directive({
  name: 'SomeDirective',
  locations: ['OBJECT'],
  args: {
    bool: nonNull(booleanArg())
  }
})

Use:

objectType({
  name: 'SomeType',
  directives: [someDirective({bool: true})]
})

or

import { addDirective } from 'nexus'

objectType({
  name: 'SomeType',
  directives: [addDirective('SomeDirective', {bool: true})]
})

Prints:

type SomeType @SomeDirective(bool: true) {
  # ... fields
}

See changes in the kitchenSink/__app.ts for more example uses.

@Sytten
Copy link
Collaborator

Sytten commented Jul 21, 2021

Dont close #148, still needs a plugin for it to make is nice for people to use (should be trivial once this is merged)

@tgriesser tgriesser force-pushed the tgriesser/feat/sdl-directives branch from 62577bb to 8524a96 Compare July 21, 2021 16:20
directiveUse: NexusDirectiveUse,
directiveDef: GraphQLDirective
): ReadonlyArray<ArgumentNode> {
return mapObj(directiveUse.args ?? {}, (val, key) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe args in NexusDirectiveUse is an array since it uses MaybeArgsFor which maps to an array, I might be missing something though.
It is typed as any in my editor currently.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm... Need to check on that

* @example
* directives: [useDirective('ExampleDirective', { arg: true })]
*/
directives?: Directives
Copy link
Collaborator

Choose a reason for hiding this comment

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

General comment: Do you think it would be possible to type the Directives here to make sure the location matches the type? Currently it would fail at build time so I guess it's not too bad.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I had initially tried this api as:

directives(t) {
  t.MyDirective(args)
  t.OtherDirective(args)
}

Where the members were dynamically based on directive declarations & where they can be positioned. It generally worked, but the types were pretty gnarly & seemed to be a lot of complexity for something that could just be a simple runtime error.

export function directive<DirectiveName extends string>(
config: NexusDirectiveConfig<DirectiveName>
): NexusDirectiveDef<DirectiveName> {
assertValidName(config.name)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We also need to assert the name of the arguments too, is that done somewhere else?

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually recently found that we don't do this for field names, but we should 😬

* locations: ['FIELD_DEFINITION'],
* })
*/
args?: ArgsRecord
Copy link
Collaborator

Choose a reason for hiding this comment

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

General question since I am not sure, are we dealing with loops (direct or indirect) inside the directive?
https://spec.graphql.org/June2018/#sec-Type-System.Directives (see the validation at the end)
Say:

export const TestDirective = directive({
    name: 'TestDirective',
    args: {
      someArg: arg({ type: 'TestType'})
    },
    locations: ['FIELD_DEFINITION'],
})

export const TestType = objectType({
  name: 'TestType',
  definition(t) {
    t.string('myField', { directives: [TestDirective({ myField: "bob" })]})
  }
})

Copy link
Member Author

Choose a reason for hiding this comment

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

No, good catch - need to do that


export function printSchemaWithDirectives(schema: GraphQLSchema): string {
return printFilteredSchemaWithDirectives(schema, (n) => !isSpecifiedDirective(n), isDefinedType)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we need a custom printer, all graphql versions that nexus support have a schema printer that does print the directives:
https://github.com/graphql/graphql-js/blob/v14.5.8/src/utilities/schemaPrinter.js
https://github.com/graphql/graphql-js/blob/v15.5.1/src/utilities/printSchema.js
And they should be exported by graphql/utilities.

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe it only prints the directives declarations, not the directive uses on the SDL types/fields

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good point, bit annoying that we have to copy paste/maintain all that code. We should reference the commit it was taken from and maybe the diff vs the original to reduce upgrade burden.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I'm not too worried about it in the sense that since it's just printing code the paths are pretty simple and easy to validate correctness of (not a lot of business logic that you'd otherwise be worried about with copy/paste).

)
assertNoMissingTypes(schema, missingTypes)
runAbstractTypeRuntimeChecks(schema, finalConfig.features)
return { schema, schemaTypes, tsTypes }
}

/** Builds the schema, we may return more than just the schema from this one day. */
export function makeSchemaInternal(config: SchemaConfig) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we want to export that anymore?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I think I'm using it in the tests, I don't believe it's publicly exported on the module

nexus: schemaExtension,
},
directives: [...specifiedDirectives, ...Object.values(customDirectives)],
...schemaDirectives,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have a TS error in editor for that line Property 'operationTypes' is missing in type 'ScalarTypeDefinitionNode' but required in type 'SchemaDefinitionNode'

Copy link
Collaborator

Choose a reason for hiding this comment

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

I feel this is misleading for future changes, this returns an AST node. If some would need to override another property in the astNode they might override the astNode set by schemaDirectives.

Copy link
Member Author

@tgriesser tgriesser Jul 22, 2021

Choose a reason for hiding this comment

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

Yeah agreed, meant to come back to this / mark this as a TODO

@@ -869,6 +931,7 @@ export class SchemaBuilder {
...config.extensions,
nexus: new NexusInputObjectTypeExtension(config),
},
...this.maybeAddDirectiveUses('INPUT_OBJECT', config.directives),
Copy link
Collaborator

Choose a reason for hiding this comment

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

I said it in the makeSchema, but I want to drive the point, since this adds a whole astNode I feel that someone that might want to modify the AST node for another purpose will get blocked by this code or might override it entirely. I think we should at the very least wrap that in an array and a merge function or even better if only the directives field of the node was returned by maybeAddDirectiveUses (free and member functions) and the astNode was generated by another method.

Copy link
Member Author

Choose a reason for hiding this comment

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

I feel that someone that might want to modify the AST node for another purpose will get blocked by this code or might override it entirely

Yeah, fair points - I had considered that and was thinking of just crossing that bridge if/when it came to it, since astNode isn't part of the public api for any of the nexus builder fn's, I wasn't sure if that would actually happen in practice.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In theory no, we should never touch the AST node since it is supposed to the result of the parsing of the schema.
I still think it should be explicit in the code that we are converting the directives to an AST node. Something like:

this.createAstNode([this.maybeAddDirectiveUses])

I suspect that a future release of graphql will add backend directives in the spec, so it will just be easier to move to that then.

Copy link

Choose a reason for hiding this comment

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

@tgriesser what would be the shortest path to unblock this pull request so it can go through?

Copy link

Choose a reason for hiding this comment

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

@zingerj this is the blocker.

Copy link
Member Author

Choose a reason for hiding this comment

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

Going to punt on this for now - going to assume that if you need to modify the ASTNode you can use something else more specialized like graphql-tools

@victoriris
Copy link

Is there an estimated time for this new feature? 👀

* main:
  feat: ability to rename root types (#976)
  docs: Remove `.ts` extension from import statement (#974)
  fix: revert declareInputs default to false (#975)
  fix: printer imports, follow up from #967 (#970)
  fix: plugin inputFieldDefTypes (#919)
  fix: add globalHeaders to imports for global def file (#969)
  fix: correct imports for changes in #967 (#968)
  feat: ConfiguredTypegen for splitting global types & configuring input type emission (#967)
  Revert "fix: #921 wrong typegen for nullable values w/ default (#965)" (#966)
  fix: #921 wrong typegen for nullable values w/ default (#965)
@tgriesser
Copy link
Member Author

Is there an estimated time for this new feature? 👀

Going to probably work on it a bit today/tomorrow.

@sooxt98
Copy link

sooxt98 commented Sep 30, 2021

Does it work on inputObjectType?

export const InputType = inputObjectType({
  name: 'InputType',
  directives: // here!
  definition(t) {
    t.nonNull.string('key', {
      directives: // here!
    })
    t.int('answer')
  },
})

@davidsoderberg
Copy link

Any progress on this one?

@yasaichi
Copy link

yasaichi commented Dec 27, 2021

What's a blocker of merging this PR? Failed checks?

@victoriris
Copy link

victoriris commented Dec 27, 2021

@davidsoderberg & @yasaichi just to let you know... I needed this for Apollo Federation support and ended up switching to type-graphql a month ago. Maybe it can help you too

@davidsoderberg
Copy link

I also ended up switching to type-graphql... :(

@zingerj
Copy link
Contributor

zingerj commented Feb 7, 2022

Hey all, what's the latest on getting this merged? Looks like there's one failing test; is that it to get this across the line? Thanks for your work here!

@YElyousfi
Copy link

When can we expect this to get merged? Federation relies on directives.

@meabed
Copy link

meabed commented Apr 30, 2022

This is great feature - thank you!!
Would it be ready to merge soon?

@yasaichi
Copy link

yasaichi commented May 4, 2022

[FYI] To those who can't wait for this PR merged
You have the following alternative libraries supporting directives:

@BahaaZidan
Copy link

Please merge this!

* main: (33 commits)
  fix: incorrect logic in backward pagination (#1084)
  fix: update snapshots for change in #1083
  chore: change facebook.github.io to relay.dev links (#1083)
  feat: allow specifying custom directives in makeSchema (#1065)
  Update comment for `shouldGenerateArtifacts` (#1057)
  feat: Run both formatTypegen and prettier formatter if given (#1042)
  feat: allow specifying custom directives in makeSchema (#1064)
  feat: Use ReadonlyArray in typings (#1041)
  chore: use dripip reusable workflow
  docs: Update npm badge
  v1.3.0
  chore: add test confirming v16 schema compat (#1054)
  chore: update github workflows (#1053)
  feat: add GraphQL 16 support (#977)
  chore(docs): fix typos (#1005)
  chore(docs): fix typo 06-chapter-5-persisting-data-via-prisma.mdx (#1007)
  chore(docs): fix typo 04-why-nexus.mdx (#1008)
  chore(docs): fix typo 030-neuxs-framework-prisma-users.mdx (#1016)
  chore(docs): fix typo 05-chapter-4-testing-your-api.mdx (#1023)
  chore(docs): fix typo in 07-chapter-6-testing-with-prisma.mdx (#1024)
  ...
* main:
  chore: attempt to fix the dripip workflow, again
  chore: update the dripip workflow, bump lint-staged
@tgriesser tgriesser changed the title feat: Add Directives / print SDL directives feat: Add Schema Directives May 15, 2022
@tgriesser tgriesser changed the title feat: Add Schema Directives feat: Add Schema Directives to SDL May 15, 2022
@tgriesser tgriesser merged commit 9875e90 into main May 15, 2022
@tgriesser tgriesser deleted the tgriesser/feat/sdl-directives branch May 15, 2022 20:59
@tgriesser
Copy link
Member Author

This is merged, and is now released under nexus@1.4.0-next.7, no docs yet other than in the PR description.

No official plugin for federation, but the primitives are there - feel free to open a PR if you make progress on something generic and would like to add a plugin to the core.

Additionally, please give any feedback or let me know if you run into issues with it, will get this into an official 1.4.0 soon

@meabed
Copy link

meabed commented Jun 11, 2022

@tgriesser - Thank you! any date we expecting this to be released?

@cqhung1412
Copy link

@tgriesser - Please release this soon with the docs.

@iamandrewluca
Copy link

iamandrewluca commented May 6, 2024

Hey folks! Thanks for working on this feature for Nexus!

Should there be any type of completion for the directive args when using it? 🤔

image image

I've augmented it using this

declare module 'nexus/dist/core' {
  export interface NexusDirectiveDef {
    (arg: CacheHint): NexusDirectiveUse<"cacheControl">;
  }
}

PS: Does anyone know when a new minor release will be done with this feature?

Not related: I'm using this feature to implement cache control in Strapi. I started with using resolvers, but I think the directive approach is much better.

strapi/strapi#20254

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for operation directives