Open AndrewIngram opened 8 years ago
Thanks for proposing this! During the GraphQL redesign last year, we actually considered adding this!
There are two issues that seemed to result in more complication than would be worth the value we would get from this feature, but I'm curious what you think or if you have ideas:
ConnectionEdge
type and look at its fields, what will the type
of node
be? The best answer we could come up with was to change Field.type
from Type
to Type | GenericParameter
which is a bit of a bummer as it makes working with the introspection API more complicated. We could also expand Type
to include the possibility of defining a generic param itself. Either way, it also has some rippling effects on the difficulty of implementing GraphQL, which would need to track type parameter context throughout most of it's operations.__typename
respond with? What should { bars { __typename } }
return? This one is pretty tricky. { "bars": { "__typename": "Connection" } }
? That describes the type, but you're missing info about the type parameter, that that ok? { "bars": { "__typename": "Connection<Bar>" } }
Is also problematic as now to use the __typename
field you need to be able to parse it. That also adds some overhead if you were hoping to use it as a lookup key in a list of all the types you know about.Not to say these problems doom this proposal, but they're pretty challenging.
Another thing we considered is how common type generics would actually be in most GraphQL schema. We struggled to come up with more than just Connections. It seemed like over-generalization to add all this additional complexity into GraphQL just to make writing Connections slightly nicer. I think if there were many other compelling examples that it could motivate revisiting.
You're right about the number of use cases being relatively small, i'll need to think on that point.
To be honest, this feels like sugar for developers of schemas rather than clients. In the simplest case, i'd just expect the introspection result to be the same as it is now, i.e the generics get de-sugared. To that end, it could just be something that parsers of the schema definition language end up supporting, but it's up to library authors how to handle the generated AST.
In graphql-js land, there are numerous examples of libraries (apollo-server, my own graphql-helpers, and a few others I can't remember) which use the parser provided to vastly simplify the process of building schemas (having done it both ways, I'd say it's pretty close to an order of magnitude more productive), and i'd personally be happy to add additional support for tokens related to generics to my library.
However, it does feel weird supporting a syntax that's not actually reflected in the final generated schema, so i'm unsure about this approach.
I really wish something like this would be reconsidered. Connections may just be a single use-case, but it's a big one, in my opinion. The length of my current schema would cut in half with generics.
Currently I have 24 copies of basically this:
type TypeXConnectionEdge {
node: TypeX
cursor: String
}
type TypeXConnection {
edges: TypeXConnectionEdge
pageInfo: PageInfo
}
That's nearly 200 lines of code that could easily be expressed in 8 lines of generics. I'm seriously considering writing my own preprocessor just to hack on my own generics capability...
Hmm, in graphql-tools you could do something like:
type MyType {
hello: String
world: String
}
${ connectionAndEdgeFor('MyType') }
Is there something the syntax could have that would be better than that JS-style approach?
And what if you're not using JS? 😞
I want my schema to be pure graphql schema language so it doesn't need preprocessing.
Yeah I definitely sympathize. I guess the real question is, is the generic thing just something for the server to be written more conveniently, or does the client somehow know that these types/fields are generic and acts accordingly?
If the client doesn't know, then I feel like it should be a preprocessor feature or a macro thing. The spec is all about the contract between client and server IMO.
However, there are definitely implementations for generics where the client could actually take advantage of knowledge that something is a generic thing. For example, in the connection example, there's no way to make an interface that says that a TypeXConnectionEdge
should have a node
of type X
, so you can't really enforce that without relying on conventions.
Perhaps this could be done as some sort of intersection of interfaces and type modifiers? So basically, it's a way of creating your own type modifiers - if you squint hard enough, [ ... ]
and !
are kind of like List<T>
and NonNull<T>
.
So building on that, in introspection:
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
}
}
You could get:
{
kind: "GENERIC",
name: "Connection",
ofType: {
kind: "OBJECT",
name: "Photo"
}
}
Perhaps this should come with a change of kind: "LIST"
to kind: "GENERIC", name: "LIST"
.
I mean, it seems hugely valuable for the client to understand generic concepts too, but that could probably be expressed as simply differently named types, since the client doesn't generally need to be too concerned about the actual name of types so much as what is in them. It seems to me like it'd be really valuable to be able to, in a type-safe way, express concepts like pagination while retaining the safety of recognizing that a given type encapsulates objects of a specific type. Without generics or preprocessing you can sorta-kinda do this with unions, but then you're throwing away the promise that all items in a list are of a specific type...
I guess in my mind, whether or not the client should worry about it is a very important factor in determining whether it should be in the spec or simply live as some server-side library tooling.
Hi everybody! I've definitely been looking to solve the Connection use-case here, and another similar case specific to my project. I have a slightly different strategy which I've laid out in #295, which is a much smaller change, and in no way mutually exclusive with this proposal.
Basically, if an interface were able to explicitly implement another interface, the client would be aware of the hierarchy. That is, it would be provided similar context to what generics might provide, but without adding new semantics to the protocol.
This wouldn't solve the issue of verbosity within the schema, but leverage the existing types to convey "generic" type information to the client. In this way, a preprocessor might be able to compile generics into hierarchal interfaces, achieving both goals here.
Given that we now have at least one authoring-only syntax extension, i.e. extend type
, is it worth reconsidering generics in the same light?
Hmmm, that's true - extend type
is not accessible through introspection at all, I didn't think about that.
One use case I have run into for generics is having something resembling Maybe<T>
to handle error messages. I'd like to do so without having to mess with the network layer and introduce some sort of global state, or refer to a disjointed part of the response. Currently, I am defining a separate union type for each object (ie MaybeUser
is a User | Error
) but it would be nice to be able to do this as simply as Maybe<User>
and define the structure once.
An alternative to avoid the extra complexity of the generic union would be something as simple as
generic Maybe<T> {
left: Error
right: T
}
Another use case is
generic Pagination<T> {
total: Int!
limit: Int!
offset: Int!
results: [T!]!
}
type Person {
id
name
# ...
}
type Query {
search_person(limit: Int = 20, offset: Int = 0, q: String): Pagination<Person>!
}
generic Maybe<T> { ... }
and generic Pagination<T> { ... }
look great to me, though I'd drop the generic
keyword as it seems redundant by the use of angle brackets.
Addressing problems @leebyron mentioned in the 2nd post here, generics don't need to appear in introspection at all as they are abstract helpers. Concrete types extending generics could be introspected with all the precision of graphql
generic Pagination<T> {
total: Int!
limit: Int!
offset: Int!
results: [T!]!
}
type Person {
id
name
# ...
}
type PaginatedPersons extends Pagination<Person> {
extraField: Int! // maybe
}
type Query {
search_person(limit: Int = 20, offset: Int = 0, q: String): PaginationPersons
}
In this case the type PaginatedPersons could have in introspection this shape
type PaginatedPersons {
total: Int!
limit: Int!
offset: Int!
results: [Person!]!
extraField: Int!
}
I think there is a potential use case for client-side generics in enabling re-usable components. Example, using react-apollo, some hypothetical syntax to inject a fragment into another fragment, and the Pagination<T>
type from the above post:
const PaginatedListFragment = gql`
# some purely hypothetical syntax on how you might inject
# another fragment into this one
fragment PaginatedList($itemFragment on T) on Pagination<T> {
total
offset
limit
results {
id
...$itemFragment
}
}
`;
const PaginatedList = ({ data, renderItem, onChangeOffset }) => (
<div>
{data.results.map(item => <div key={item.id}>{renderItem(item)}</div>)}
<PaginationControls
total={data.total}
offset={data.offset}
limit={data.limit}
onChangeOffset={onChangeOffset}
/>
</div>
);
const PeoplePageQuery = gql`
query PeoplePage($limit: Int, $offset: Int) {
people(limit: $limit, offset: $offset) {
# again, hypothetical parametric fragment syntax
...PaginatedList($itemFragment: PersonItemFragment)
}
}
${PaginatedListFragment}
fragment PersonItemFragment on Person {
id
name
}
`;
const PeoplePage = ({ data }) => (
<div>
<h1>People</h1>
<PaginatedList
data={data.people}
renderItem={item => <a href={`/person/${item.id}`}>{item.name}</a>}
onChangeOffset={offset => data.setVariables({ offset })}
/>
</div>
);
]
This would be something very powerful for component-based frameworks!
@dallonf this was actually one of the other benefits I envisaged :)
It's the primary reason why I don't think it's enough for this to just be syntactic sugar for the existing capabilities, allowing clients to know about generics could be a very powerful feature.
I also have another use case for generics. I want to define an array of distinct elements, commonly knows as a Set. So instead of defining a field like myValues: [String]
, I would like to define it as myValues: Set<String>
to accomplish this.
To add another use case, I've found myself making custom scalars when I would otherwise define a Map<SomeType>
. In these cases SomeType
has always been a scalar so this has been acceptable, but there have been other times when I chose to use [SomeObjectType]
when a real Map<SomeObjectType>
would have been preferable.
-- edit --
I do want to note, though, that this would be quite a challenge to implement, since the generic's definition in the schema would need to fully describe the final structure. Otherwise, there's no way for a client to know what to expect.
Another use, also tied with pagination is when different interfaces need to be paginated.
Suppose we have an interface Profile
, and two implementations: UserProfile
and TeamProfile
Without generics, I cannot see a solution to be able to deduct that UserProfilePagnated
and TeamProfilePaginated
can be supplied where we are expecting PaginedProfiles
.
With generics - although not trivial, one can make a complex analyzer that understands the notion of co and contravariance, and can deduct that PaginatedProfiles
is an interface of UserPaginatedProfiles
, although one might need to signal this somehow.
Hi @axos88, can you add your comment on #295? Your use case is exactly the kind of problem it is designed to address.
@mike-marcacci I'm not sure how interfaces implementing interfaces is enough to have a solution for the problem
Although I agree, the problem is related.
Oh I see. :)
@leebyron
What if generic types can not be returned by fields without type specification. They are just for boilerplate reduction. As you can see in the following definition (from @AndrewIngram's post) the field bars does not return the generic type over T
, but Bar
:
generic ConnectionEdge<T> {
node: T
cursor: String
}
generic Connection<T> {
edges: ConnectionEdge<T>
pageInfo: PageInfo
}
type Foo {
id: ID!
bars: Connection<Bar>
}
And the generic
types are not visible in introspection.
Result of this could be:
type ConnectionEdgeBar {
node: Bar
cursor: String
}
type ConnectionBar {
edges: ConnectionEdgeBar
pageInfo: PageInfo
}
type Foo {
id: ID!
bars: ConnectionBar
}
As far as I know type name duplication is not allowed in graphql schema and all implementation raise errors in case of duplication. So when you already have ConnectionBar somewhere, then you would get error.
You do not have to drop the information when schema is parsed, so you could point out that ConnectionBar being generated from generic Connection<T> is already defined. In introspection the schema is already processed and the there only exists the ConnectionBar.
I can't speak for others, but I have a lot of boilerplate, which would be saved if I could use generics like that. I have noticed, that on field-level (except generic type fields) I do not need to be generic in this case. I can defined specific type there. No need to return generic<T,V> etc.
@akomm's recommendation is the style that I've gone with in my project.
I currently implemented it with @generic(...)
as a directive, that I preprocess out to generate the types:
interface Page @generic(over: [DataType]) {
items: [DataType]
meta: PageMeta
}
type PageMeta {
page: Int
totalPages: Int
totalItems: Int
}
# Elsewhere:
type PopularItems_ implements Page
@generic(using: [PopularItem_]) {
searchSectionTitle: String!
}
type PopularItem_ {
id: ID!
title: String!
iconUrl: String
narrowPhotoUrl: String!
slug: String!
}
After pre-processing, the fields from the Page interface are merged in to the fields of PopularTemplates_
after distributing the using
-parameter over all instances of DataType
So basically graphql need a way to define opaque types/templates to reduce boilerplate, which are not directly exposed as API. For generic results we have interfaces.
Those templates could have an optional annotation to define how the final type's name is composed - or if it makes sense, a fixed method how it is composed.
As I mentioned in a previous comment, there are reasons why we might not just want this as definition-time syntactic sugar. Exposing the concept of generics to API consumers would allow for highly-reusable client-side code that exploits them. It's currently relatively verbose to implement different connection pagination containers with Relay even though the structure is almost identical every time. Generics are an incredibly powerful construct, so it'd be nice to be able to enhance the GraphQL type system with them.
The main argument against them seems to be that it would increase the difficulty of implementing GraphQL libraries (eg graphql-js, sangria, graphene, absinthe). If the client-side value is limited, i'd agree that it's not worth the complexity. But i'd argue that it's not worth adding at all if it's just a syntactic sugar thing -- there's nothing stopping people implementing these patterns (if slightly less elegantly) in user-land today.
Generics could also be useful to model type-safe IDs:
type Person {
id: ID<Person>!,
name: String!
}
type Query {
person(id: ID<Person>!): Person!
}
@alamothe
Depends on how you define ID. If it is relay-like opaque string, what would the generic ID
@akomm It is still opaque to the client.
Type-safety only prevents the client to pass an ID of the wrong type. For example, client tries to pass ID<Book>
in a mutation where ID<Person>
is expected it will get a compile time error
Just adding another use case, where I handle form validation errors in a repetitive way, and having generics would improve client-side reusability:
type InvalidField<T> {
fieldName: T!
errors: [String!]!
}
type InvalidInput<T> {
nonFieldErrors: [String!]
fieldErrors: [InvalidField<T>!]
}
enum RegisterUserFieldName {
NAME
EMAIL
PASSWORD
}
type RegisterUserSuccess {
user: User!
token: String!
}
union RegisterUserPayload = InvalidInput<RegisterUserFieldName> | RegisterUserSuccess
So, to recap what I see here so far:
The actual approach we're mainly discussing here is adding generics to the language and that would require support from the clients. Some of them will require hacks depending on the language they're coded in.
@akomm mentioned templates, and that's the approach @fbartho used. I could get by with that approach. In the end it reduces boilerplate in the schema definition, and clients require no change. The only downside I see is that clients may be exposed to many identical classes with different names, which can be cumbersome when coding.
@leebyron could you tell me how the language's RPF process works, and what information would be required to actually push forward this proposal here?
I'm still concerned that this adds a lot of additional complexity to GraphQL libraries and client tools. I definitely understand and see the potential value, but I think it should be made very clear that the added value is well worth the complexity cost.
Next steps to do so would be to implement this as a PR in GraphQL.js to better understand complexity, and write the spec text to understand changes to schema and query validation algorithms
I think there's a fundamental underlying conversation that seems to be going on here, and I've had it with a few developers now:
GraphQL aims to be "a complete and understandable description of the data in your API" (taken from the graphql website homepage). Graphql also aims to be simple. These two seem to directly collide when you have a complex API (any enterprise-level API really). But I think the answer to this depends on the angle of approach. (And the angle will likely depend on your team and implementation).
Here are two angles of approach to using GraphQL: Approach A
Approach B
Personally, I think both approaches are valid. However, I think that when discussing more complex syntax, it's worth mentioning that Approach A requires stronger features like Generics, whereas Approach B does not.
GraphQL (and the GraphQL community ahem Graphcool/Prisma) both heavily advertise and encourage Approach A. Approach A also has some great pros for highly collaborative environments. The paradigm of having a unified language in which to describe web services that is concise and can be understood by the whole team is very lucrative for many organizations. Hence you see phrases like "a complete and understandable description of the data in your API" on the GraphQL website.
In my opinion, if GraphQL is going to be a long-term viable option for approach A, it needs to be able to describe complex environments, which means supporting things like generics and (:trollface:) custom directives.
I love where the conversation is going, I just wanted to point out that complexity isn't necessarily a bad thing in this case, and really, I think both approaches can be supported in the long run. This is ultimately what gives GraphQL the potential to become an industry standard, and what differentiates it from things like Swagger.
My TLDR 2 cents.
Excellent analysis!
I agree that GraphQL (intentionally) tries to fulfill competing ideals of fully describing your data and being as simple as possible. That often forces compromise.
Also I like your framing of how to build schema, but it lacks a third approach I’ll call “C” which is what most are doing today: First write my business logic in my language of choice, then manually write a GraphQL schema in that same language.
While I agree all approaches are valid, out of the three I strongly prefer your “approach B”. It turns out that programming languages are great for describing business logic and GraphQL isn’t a programming language. In cases where your programming language of choice comes with a type system as or more capable than GraphQL’s then generating it the same way you might generate a Swagger schema is ideal. In fact: generate both GraphQL and Swagger and have multiple API facades to the exact same business logic!
I'd also like to add my perspective on the issue, which is that generics in GraphQL would be largely a client-side benefit rather than server-side (although it could certainly be that as well), allowing for much more flexible fragments.
Since query generation on the client side seems to be strongly discouraged (which I at least partly understand as wanting to keep the queries explicit and easy to read), there's really no other way to enable things like a pagination component fragment as I mentioned above (https://github.com/facebook/graphql/issues/190#issuecomment-343487643).
@leebyron I worry about approach B, as it tends to leak internal implementation details without producing a holistically designed API. Internally, a backend often ends up with different Partial<Foo>
of the same data model, but what should be exposed to the client needs to be a bit more structured and consistent.
Especially since the firm recommendation of GraphQL API best practices is "never deprecate, never delete".
Writing GraphQL first manually helps you express naming/structure cleanly in a way that makes things easier for Client Developers to work with an API. Then you get a source of truth that both client & backend can refer to. In specific, GraphQL generics is a key piece for avoiding noisy repetition in that phase.
I worry about approach B, as it tends to leak internal implementation details without producing a holistically designed API
I agree that poorly developed this could definitely occur. But also a naive translation of existing business logic to a manually written GraphQL schema would have the same outcome. To be successful, the parts of the internal logic exposed to GraphQL needs to be intensional and designed with API exposure in mind. This advice applies equally to exposing a REST API with generated Swagger
I think there is a long history in APIs of having clean, simple IDLs. In fact, that is what attracts a lot of people to GraphQL in the first-place.
It is also what attracts a lot of people to gRPC / Protocol Buffers.
In a polyglot, multi-team world, I want the ability to have a clean, language-agnostic interface description of my API.
Not only does it allow tooling support, but more importantly it allows the humans who write the software in different languages, tools, and teams, to come together and agree in a shared way on the API.
This is one of the strongest reasons why I choose to use GraphQL.
I think there is a long history in APIs of having clean, simple IDLs. In fact, that is what attracts a lot of people to GraphQL in the first-place.
It is also what attracts a lot of people to gRPC / Protocol Buffers.
In a polyglot, multi-team world, I want the ability to have a clean, language-agnostic interface description of my API.
Not only does it allow tooling support, but more importantly it allows the humans who write the software in different languages, tools, and teams, to come together and agree in a shared way on the API.
This is one of the strongest reasons why I choose to use GraphQL.
I totally agree. That's why we need a clean, simple way to describe generic types.
What if we only have generic types on "compile time" so they don't exist in the runtime, then we only need something lige a "template expansion mechanism" which "compiles" to concrete types.
template Connection<TEdge> {
edges: [TEdge]!
}
template Edge<T> {
node: T
}
type ProfileEdge = Edge<Profile>
type ProfileConnection = Connection<ProfileEdge>
type Profile {
id: ID!
}
type Query {
profiles: ProfileConnection!
}
You can then implement higher order functions to deal with the "generic types" and call them in the resolvers on specific fields.
@thetrompf have you seen graphql-s2s? It basically does exactly what you're suggesting.
@AlecAivazis yeah I know of the project, and you're right. s2s compiles to GraphQL SDL compliant schemas. But it would be great if the official languages supported this. s2s does not play well if you want to combine it with other tools operating on the schema like graphql-code-generator.com, so I have to defined a pre-compile step before invoking the code generator, because I'm now working with a superset of the SDL.
Yeah I can see there are shortcomings with this solution. The paginated list problem could be solved with interfaces though.
Med venlig hilsen / Best regards Brian Kejlberg
ons. 5. jun. 2019 11.27 skrev Alec Aivazis notifications@github.com:
@thetrompf https://github.com/thetrompf have you seen graphql-s2s https://github.com/nicolasdao/graphql-s2s? It basically does exactly what you're suggesting.
However, not exposing generics to the client prevents a whole family of components built around recurring structures in an API from being possible. For example, a component that knows how to render a paginated list with page numbers. Not to mention it would mean that a subset of the SDL only applies in a particular context which I'm not sure is appropriate.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/graphql/graphql-spec/issues/190?email_source=notifications&email_token=AAK5EZV6Z4U5ZGS3VPX4OV3PY6BGXA5CNFSM4CIAZHUKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODW7EJZY#issuecomment-499008743, or mute the thread https://github.com/notifications/unsubscribe-auth/AAK5EZVSVJ77IQFFQ7ZR3ALPY6BGXANCNFSM4CIAZHUA .
@thetrompf paginated can almost be solved with interfaces... and can totally be solved once #373 lands. (See this comment.)
The longer this proposal languishes, the more bad duplicated code is written. We can think of generics as adding complexity to the spec and implementations or we can think of them as reducing complexity for graphql's users. Given that just about every modern typed language has or will soon have generics (cough go), the choice is clear as day.
For generics at runtime to be most useful you need to be able to talk about T
.
Ord<T>
should mean T implements Ord
where Ord
is defined as:
interface Ord<T> {
compare(a: T!, b: T!): Ordering!
}
enum Ordering {
GT
EQ
LT
}
Generics would improve the readability of GraphQL APIs.
From the Shopify graphql design tutorial that I really like, the one part that bothers me is the return types suggested for mutations:
type CollectionCreatePayload {
userErrors: [UserError!]!
collection: Collection
}
Having to create a new type for every mutation and requiring the user look up that type to know what it is seems excessive when you could simplify types with:
type CreatePayload<T> {
userErrors: [UserError!]!
result: T
}
And we could use the CreatePayload<Collection>
return type instead.
What can we do to help move this issue up from a stage 0 RFC? Looking at the notes (recent notes, anyway) from the GraphQL working group, it seems like issues like this aren't under discussion. Is it just a matter of opening PRs?
Based on earlier comments, I agree with @robbyemmert 's Approach A.
GraphQL is pretty much dead at this point — no progress is being made for years. Same situation is with other RFCs (input union, empty types).
As projects like Relay have shown, it's relatively common to repeat the same generic structures of types multiple times within a project. In the case of Relay, I'm talking about Connections.
The GraphQL definition language already has explicit support for one particular form of generic type, arrays:
I'd like to start discussion about being able to do something similar for user-defined structures:
The overall goal is to reduce the amount of boilerplate in creating schemas with repetitive structures.