-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Setting directives using ObjectType constructor (not IDL) #1343
Comments
After 3 days, ping @leebyron as an author of the quotes and ping @IvanGoncharov as an author of #746 that have added support for directives 😃 |
@19majkel94 My position is the same as I wrote in #1262
|
The new Apollo Server v2 introduce requirements for graphql-compose (and graphql-js) users to somehow provide directive via Another use case was with graphql-cost-analysis which allows rejecting complex queries. It provides a comfortable way to add a complexity calculation logic without modifying resolvers. It will be cool if Lee changes its position according to directives. Repeat here a desired way for declaration directives from #1262: const UserType = new GraphQLObjectType({
name: 'User',
fields: {
name: {
type: GraphQLString,
description: '...',
resolve: () => ...,
directives: [
{ name: 'cost', args: { useMultipliers: false, complexity: 2 } },
{ name: 'unique' },
],
// or maybe simpler if graphql spec does not allow duplicated names
directives: {
cost: { useMultipliers: false, complexity: 2 },
unique: true,
},
},
},
}); |
@IvanGoncharov @taion maybe you take care about this feature in the upcoming WG meetup 2018-09-20? My English does not alow to me to do it myself 😊 |
The cost analysis case example is interesting, because in the context of using the programmatic API, I think it's suboptimal to set up costing via directives. Instead of the above, a better way to express the same would be to add a myConnection: {
type: FooConnection,
getCost: (childCost, { first }) => first * childCost,
}, |
Directives are purely a feature of the GraphQL language and are not part of type definitions - this is because different tools will use and interpret directives in different ways. I assume the Apollo tools translate these GraphQL language directives to some other kind of metadata on the underlying schema and type objects - it would be best to use whatever API the apollo tools expect directly. |
@leebyron Now I understood what you mean. It can be explained in another way: For the query: query {
authors {
id
name @uppercase
}
} needs to create such directive: const UppercaseDirective = new GraphQLDirective({
name: 'uppercase',
description: 'Provides default value for input field.',
locations: [DirectiveLocation.FIELD],
});
const schema = new GraphQLSchema({
directives: [UppercaseDirective],
...
}); and such Author type with const UppercaseDirective = new GraphQLDirective({
name: 'uppercase',
description: 'Provides default value for input field.',
locations: [DirectiveLocation.FIELD],
});
const AuthorType = new GraphQLObjectType({
name: 'Author',
fields: () => ({
id: { type: GraphQLInt },
name: {
type: GraphQLString,
resolve: (source, args, context, info) => {
if (info.fieldNodes?.[0].directives?.[0]?.name?.value === 'uppercase') {
return source.name.toUpperCase();
}
return source.name;
},
},
}),
}); For now Anyway with current |
BUT what if can use directive as middleware?For example, we create a new kind of directive: const ChangeCaseDirective = new GraphQLMiddlewareDirective({
name: 'changecase',
// locations: [...], // no need, because this directive works only `DirectiveLocation.FIELD` level
args: {
fieldName: { type: new GraphQLNonNull(GraphQLString) },
upper: { type: new GraphQLNonNull(GraphQLBoolean) },
lower: { type: new GraphQLNonNull(GraphQLBoolean) },
},
resolve: async (directiveArgs, resolve, source, args, context, info) => {
const { fieldName, upper, lower} = directiveArgs;
let value = await resolve(source, args, context, info) || source[fieldName];
if (typeof value === 'string') {
if (directiveArgs.upper) value = value.toUpperCase();
else if (directiveArgs.lower) value = value.toLowerCase();
}
return value;
},
});
const schema = new GraphQLSchema({
directives: [ChangeCaseDirective],
...
}); This directive can be defined only on schema build step via SDL or ObjectType constructor. It cannot be defined in GraphQL query, otherwise
So with such directive we can custruct type in such way: const AuthorType = new GraphQLObjectType({
name: 'Author',
fields: () => ({
id: { type: GraphQLInt },
name: { type: GraphQLString },
lowerName: {
type: GraphQLString,
directives: [
[ChangeCaseDirective, { fieldName: 'name', lower: true} ]
],
},
upperName: {
type: GraphQLString,
directives: [
[ChangeCaseDirective, { fieldName: 'name', upper: true} ]
],
},
}),
}); With such case, it is almost similar to @maticzav aproach in graphql-middleware.And maybe no reason to make such const AuthorType = new GraphQLObjectType({
name: 'Author',
fields: () => ({
id: { type: GraphQLInt },
name: { type: GraphQLString },
lowerName: {
type: GraphQLString,
middlewares: [
changeCase({ fieldName: 'name', lower: true }),
],
},
upperName: {
type: GraphQLString,
middlewares: [
changeCase({ fieldName: 'name', upper: true }),
],
},
}),
}); @leebyron It will be great if |
With current implementation of const changeCaseMW = changeCase({ fieldName: 'name', lower: true });
const queryCostMW = queryCost({ ...someSettings });
const AuthorType = new GraphQLObjectType({
name: 'Author',
fields: () => ({
id: { type: GraphQLInt },
name: { type: GraphQLString },
lowerName: {
type: GraphQLString,
resolve: async (source, args, context, info) => {
return queryCostMW(
() =>changeCaseMW(
() => { return ...somehow resolved data... },
source, args, context, info
),
source, args, context, info
);
},
},
}),
}); |
@leebyron For me it looks like they read the directives from AST directly: So there's no way to pass them as an const Query = new GraphQLObjectType({
name: 'Query',
fields: () => ({
posts: {
type: new GraphQLList(Post),
complexity: ({args, childComplexity}) => childComplexity * args.count,
args: {
count: {
type: GraphQLInt,
defaultValue: 10
}
}
},
}),
}); I get your point that when we are responsible for executing the schema, we don't need to add metadata by directives as we can just use kinda middlewares in resolvers. But for integrating our schema with 3rd party tools, I believe that it's better to expose |
Apollo sticks it on the The difference in implementation is pretty trivial, but enough to break it. For example, here's what a And here's how the same thing looks using JavaScript objects instead of the IDL: Frankly, I think it's an error that directives are put directly on the FWIW, over the last couple years I've implemented a handful of custom directives & each time I do, I regret it. The logic usually fits much better down the chain in a HOF around For example: |
And nobody denies it. Support for registering directives is only for 3rd party libs like All custom code can be implemented by HOC, middlewares, and custom properties in |
@IvanGoncharov We still need a way to register directives that are present in |
As an author of #746 that added
I didn't fully understood it but was happy that he would at least agree to add Directives were added as an extension point for creating new syntax, and they were never intended to be a part of the type system since attaching metadata is not the only use case for directives and in many cases directives are used to create completely new syntax, e.g.: I think we can all agree that such directives make sense only during schema building and shouldn't be present in result schema. It would be even more confusing if such directives would reappear when printing result schema back to SDL. For a long time, I thought that the correct solution would be to whitelist directives that should be passed to the schema and ignore the rest, but after several years I understood that this workaround wouldn't solve the fundamental issue: To explain it closer to code lets take my initial version of #746 and imagine it was merged as is and all schema object have
Both these changes are fit original intention for directives so ATM they are on a fast track to be included in the next spec release. But they break my origin idea for @nodkz proposal of serializing directives as an array of objects with There are many other potential problems, but I don't want to convert this comment into the blog post :) @19majkel94 Not sure that above explanation clearly explains my reasoning so I will also try to use the analogy from your domain. Imagine how restrictive it would be if instead of calling a function JS decorators would add an item to class Test {
@someDecorator
test() {
}
}
const modifiedTest = Test.prototype.test.bind(...) Should Similar to JS decorators that make sense only as syntax sugar during class creation, GraphQL directives are syntax sugar that should be applied only during Hope now you understand why I strongly oppose adding applied directives to GraphQL schema even though I was promoting this idea just a few years ago.
Ideally, 3rd party tools should use I think This is a better solution since you can always write JS code that transforms any syntax sugar you invented (top level directives, directive chains, duplicated directives) into a single Plus
We can always add some mechanism (e.g., callback) to P.S. I also working on spec RFC to add @nodkz If you have time read this super long comment it would be great to hear your opinion on the topic. |
@IvanGoncharov BUT we should consider unification of Consider using a new proposed // add field via FieldConfig
UserTC = TypeComposer.create('User');
UserTC.addField('name', {
type: 'String',
extensions: {
cost: { complexity: 2 },
}
});
// create type via SDL
ArticleTC = TypeComposer.create(`
type Article {
title @cost(complexity: 2)
}
`); The question is: Can be Let's make one step back.ClientsWhen clients need to tune BackendersFor now become quite popular to use middlewares for resolvers. It helps to drastically reduce amount of resolvers code. But need to have a nice way to provide such "backenders" arguments from schema to the middlewares. For now backenders cannot setup any So we need to think how can be unified using of resolve(source, args, context, info) {
info.fieldExtensions
} Also we should not forget about modularity and schema stitching. Some part of schemas may be realized via It will be nice to have such implementation which helps to use Anyway we need to take a fresh look on problem of Thoughts? |
@IvanGoncharov you may ping me via Telegram by nickname @nodkz or via Skype nodkz1985 |
The community created a couple of packages to support that without any change in the GraphQL reference impl. Example of the library that reinvents some of the concepts: EDIT: However, as GraphQL started becoming more and more popular many developers wanted to collocate their metadata closer to the schema. The best way to handle this is to build your own preprocessor on top of the SDL or rely on description field which actually intends to provide human-readable info (but I can provide also machine-readable as well) |
My use case is.. We have service with schema composed with
To quote the spec - "Directives can be used to describe additional information for types, fields, fragments and operations.". If this project is a "reference implementation for GraphQL", then shouldn't I be able to define directive on a field as spec states? If I can use it in SDL, why I can't do this programmatically with an object definition? If argument is about strict types & interpretation, then language has scalars.. those are interpreted differently. And even GraphQL type system is also vague, for example Integer can be unsigned, 32-base or 64-bit.. Like @mike-marcacci mentioned, if |
follows gatsby and graphql-compose convention but allow customization see: graphql/graphql-js#1343 (comment)
* enhance(utils): read directives from extensions follows gatsby and graphql-compose convention but allow customization see: graphql/graphql-js#1343 (comment) * add tests for code-first stitching directives * add changeset
For what it's worth, graphql-tools now reads "directive" metadata from extensions by default when present instead of AST following graphql-compose/Gatsby convention above. We don't yet write directives to extensions, as unclear which directives are metadata... |
I'd like to be able to construct a gql schema with a code-first tool like typegraphql, print the schema, and see what I constructed. For example, I'd like to be able to write this: @ObjectType()
class Bar {
@Directive("@auth(requires: USER)")
@Field()
field: string;
} and get this: type Bar {
field: String! @auth(requires: USER)
} however, that isn't possible today, due to this issue. Sure, my directives will work fine at runtime, but I'd like to be able to look at my generated It seems there's some reticence to add support for storing directives because runtime tooling should ideally handle the functionality of directives in another manner, but this would be useful outside of runtime behavior, like looking at a textual schema with human eyeballs or a linter. |
hi, this ticket has been open for 5 years already, is it safe to assume it's not going to be worked on? I have a similar situation to @rattrayalex, I'm using AWS AppSync and I need to set some auth directives that are enforced by the cloud service, so I don't need to implement them myself, just declare them. using a code-first tool like thanks,. |
There has been a lot of issues with directives not being applied and registered. Most of those cases have been fixed by simply forcing the IDL (like explained here graphql/graphql-js#1343). Unfortunately, arguments have been forgotten, meaning that no directive applied on arguments were actually run or even detected. This PR stands to correct that and properly add directive to needed fields.
There has been a lot of issues with directives not being applied and registered. Most of those cases have been fixed by simply forcing the IDL (like explained here graphql/graphql-js#1343). Unfortunately, arguments have been forgotten, meaning that no directive applied on arguments were actually run or even detected. This PR stands to correct that and properly add directive to needed fields.
There has been a lot of issues with directives not being applied and registered. Most of those cases have been fixed by simply forcing the IDL (like explained here graphql/graphql-js#1343). Unfortunately, arguments have been forgotten, meaning that no directive applied on arguments were actually run or even detected. This PR stands to correct that and properly add directive to needed fields.
There has been a lot of issues with directives not being applied and registered. Most of those cases have been fixed by simply forcing the IDL (like explained here graphql/graphql-js#1343). Unfortunately, arguments have been forgotten, meaning that no directive applied on arguments were actually run or even detected. This PR stands to correct that and properly add directive to needed fields.
i'm interested into knowing how to bring those directives back. I understand it's not a bug, but it's a soft requirement in order to compute a superschema with rover (from apollo). The process with using Apollo Studio includes publishing a schema to their platform with their cli, which uses a schema file. The said file is the The issue appears when a schema doesn't have the @key or @extends directive because a shared Object type is seen as competing (in the absence of @extends/@Shareable) and the superschema fails to compute... and therefore, there's a need for the schema to have these directives printed on the schema. I'm not sure this is the best place, as i totally understand it is implementation dependant. Pothos has a working one, while Nest.JS does not. But i wish to ask from here what's the best course of action regarding graphql spec rather than anything else. Is Apollo rightful in their way? Is Pothos implementation good or Nest.JS rather following it? Should we really not have directives in the schema? If not, how could we ever resolve a superschema with schemas having directives missing? Thanks a lot and sorry for the long message |
I am reviving #1262 as I have an use case that is related to that discussion:
I agree that "directives are a tool for supplying additional intent" and "you have the full power of the host programming environment". But since the GraphQL ecosystem has grown, I think we should think about interoperability with 3rd-party libraries. The examples that I am thinking about is Apollo cache control and Prisma, which are using directives to provide some metadata to make awesome underlaying features work.
As
graphql-js
doesn't allow for registering directives using imperative way (ObjectType constructor), to be able to use Prisma, we are forced to use IDL to create our types. But this way might be not available when you are using the imperative way as you build a library on top ofgraphql-js
, as well as when you would have to refactor your whole app by migrating to IDL to add apollo-cache integration.So I think that now directives are a much more widespread feature and we should allow for interoperability with this 3rd-party tools.
The text was updated successfully, but these errors were encountered: