graphql / graphql-spec

GraphQL is a query language and execution engine tied to any backend service.
https://spec.graphql.org
14.31k stars 1.13k forks source link

Explore possibility of generic types #190

Open AndrewIngram opened 8 years ago

AndrewIngram commented 8 years ago

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:

type Foo {
   id: ID!
   bars: [Bar]
}

I'd like to start discussion about being able to do something similar for user-defined structures:

generic ConnectionEdge<T> {
   node: T
   cursor: String
}

generic Connection<T> {
   edges: ConnectionEdge<T>
   pageInfo: PageInfo
}

type Foo {
   id: ID!
   bars: Connection<Bar>
}

The overall goal is to reduce the amount of boilerplate in creating schemas with repetitive structures.

mike-marcacci commented 5 years ago

@alamothe when your message hit my inbox I genuinely thought it was satirical and laughed a bit. Whether it was serious or not, it’s pretty darn far from the truth. This year the GraphQL project has migrated to its own foundation and has really stepped up its pace after an admittedly long silence. The project has a strong and committed leadership team which has been making steady progress on a lot of these concerns. If you’d like to track the progress of these bigger conceptual changes, check out the notes from the monthly working group meetings – or better yet, if you have something to contribute, join one! If you aren’t able to be that involved but want to track progress, subscribe to this repo: the two issues you mentioned are both under active consideration.

alamothe commented 5 years ago

I hope things will change, but I'm not overly optimistic. Each of these RFCs was opened a few years back. Thanks for the reply!

wtrocki commented 4 years ago

Based on quick investigation I think could be a couple of approaches to resolving the problem with the type name

1) Preprocessor/Templating

Having extra type defined in the parsed schema object that is not defined in the SDL. This brings a number of challenges and probably it is not suitable for the spec implementation as it forces developers to use types that are not defined in SDL. From my point of view this could be the way how the community can implement generics now without involving changes in the spec.

An example was mentioned above: https://github.com/graphql/graphql-spec/issues/190#issuecomment-287686157

Pure hack for syntax using existing spec could look like this


type Page @generic {
  offset: Number
  total: Number
  """ items: [GenericField] """
}

type User @createGenericArray(name: "items", wrappingType: "Page") {
  id: ID
}

type Query {
  getUsers: UserPage
}

Result schema after processing could look like this:

 type User {
  id: ID
 }

type  UserPage {
  offset: Number
  total: Number
  items: [User]
}

type Query {
  getUsers: UserPage
}

This is a pure hack but I have used this to evaluate options and see some challenges

2) Explicit type definitions for generics

Any usage of generics could require an explicit definition of the type that do not have generics

Explained already in a couple of comments in this issue https://github.com/graphql/graphql-spec/issues/190#issuecomment-343376473 and https://github.com/graphql/graphql-spec/issues/190#issuecomment-498971680

Summary

For some of the user cases, code generation can be employed to add some form of templating capabilities to schema and generate schema that contains types explicitly defined (For example https://www.opencrud.org). This is a pretty well-known workaround for the moment.

As for the spec having the ability to define an explicit type for generics could resolve this problem on the spec level - although it could look too verbose.

justinfagnani commented 3 years ago

I'm just learning GraphQL, but already I'm seeing the need for generic types in my use cases. In particular I need to deal with a Reference type in my backend, where the value can reference any other type. There can be generic fetch operations that can retrieve the value of a ref.

Something like so:

type User {
  manager: Ref<User>;
}

type Query {
  getRef(ref: Ref<T>): T
}
benjie commented 3 years ago

@justinfagnani That seems like a use case for interfaces or unions.

xialvjun commented 3 years ago

I don't think it's a tech problem for not implementing this for 5 years. It's just there is no one can decide should we make it. @alamothe is right.

dncrews commented 3 years ago

Just to add a use-case that I've been struggling with, which is the "other side" of "discoverable query successes" (Connections) is "discoverable mutation failures":

Mutation Errors

In the schema, we're trying to call out all of the potential failure cases when they're "the user can do something to fix this, it's not a critical failure". To do this, we're introducing an error code that is an enum rather than a string. This one change makes what would've been a shared generic response type into triple the amount of schema. It feels wrong that making my schema self-documenting makes it significantly noisier.

Here's what I wish I could do:

interface MutationError<CodeEnum> {
  path: [String!]
  message: String!
  code: <CodeEnum>!
}

enum USER_CREATE_ERROR_CODE {
  USERNAME_TAKEN
  PASSWORD_REQUIREMENTS_NOT_MET
  …
}

interface MutationFailure<CodeEnum> {
  errors: [MutationError<CodeEnum>!]!
}

type UserCreatePayload = UserCreateSuccess | MutationFailure<USER_CREATE_ERROR_CODE>

type Mutation {
  userCreate(…): UserCreatePayload!
}

With the request

mutation CreateUser {
  userCreate(…) {
    ... on UserCreateSuccess {
      ... UserInfo
    }
    ... on UserCreateFailure {
      errors {
        path
        message
        code
      }
    }
  }
}

Adding extra pain to this is this related enum issue, so I can't even do this with interfaces at all, as you'd expect.

akomm commented 3 years ago

GraphQL libraries in different languages usually provide at least two methods of defining the schema.

  1. Using graphql semantics to write the types and a resolver map that maps fields
  2. Using some sort of alternative configuration format, like yaml or write code to create schema

I'd prefer the first method, but actually use the latter one. The reason hereby is the lack of abstraction/generics. With code solution I can write a factory that creates a non-generic type using arguments representing generics. This way I can abstract.

Would be nice if I could use the 1. method without losing the abstraction

fluidsonic commented 3 years ago

We also lack generics in various situations, like this one:

type Change<Value> {
   old: Value
   new: Value
}

type Contact {
   companyName: String
   firstName: String!
   id: ID!
   lastName: String!
}

type ContactUpdatedJournalItem {
   companyName: Change<String>
   firstName: Change<String!>
   id: ID!
   lastName: Change<String!>
   // etc.
}

We commonly use this pattern to create journal items for an object's change history.

For us it's important that the use-site of the generic type specifies if T is nullable or not.

But there may be other scenarios. The question is what are the possible combinations and use cases?

type Foo<T> {
   t1: T      // String or String! for Foo<String!>  ??
   t2: T!     // String!
   t3: T?     // String   (if nullability of T can be stripped)
}

type Bar {
   foo1: Foo<String>
   foo2: Foo<String!>
}

And should it be possible to force non-null types?

type Foo<T!> { // must be non-nullable
   t1: T      // String, String!, or forbidden  ??
   t2: T!     // String!
}

type Bar {
   foo1: Foo<String>   // not allowed
   foo2: Foo<String!>  // ok
}

Regarding other generic scenarios, similar to edges, we have something like this in Kotlin:

class SearchResult<out Element : Any>(
    val elements: List<Element>,
    val nextOffset: Int?,
    val totalCount: Int,
)

class SearchQueryOutput(
    val cityResult: SearchResult<City>?,
    val contactResult: SearchResult<Contact>?,
    val locationResult: SearchResult<Location>?,
    val processResult: SearchResult<Process>?,
)

Our own Kotlin GraphQL library translates it to something like this:

type CitySearchResult {
    elements: [City]!
    nextOffset: Int
    totalCount: Int!
}

type ContactSearchResult {
    elements: [Contact]!
    nextOffset: Int
    totalCount: Int!
}

type LocationSearchResult {
    elements: [Location]!
    nextOffset: Int
    totalCount: Int!
}

type ProcessSearchResult {
    elements: [Process]!
    nextOffset: Int
    totalCount: Int!
}

type SearchQueryOutput {
    cityResult: CitySearchResult
    contactResult: ContactSearchResult
    locationResult: LocationSearchResult
    processResult: ProcessSearchResult
}

The transformation automatically specializes generic types. That quickly blows up the GraphQL type system and makes it quite cumbersome to use on the client side. It also makes the server's GraphQL library implementation more complicated.

fluidsonic commented 3 years ago

Regarding the templating approach it quickly gets out of hand when Lists and NonNull types are involved. From my example above, there might be:

Which would expand to an increasingly weird type system, e.g.:

type StringChange { … }
type NonNullStringChange { … }
type NonNullStringChange { … }
type StringListChange { … }
type NonNullStringListChange { … }
type StringNonNullListChange { … }
type NonNullStringNonNullListChange { … }

(add more List nesting for more fun)

Another question: Would it be allowed to use generic types as a generic argument?

type Foo<T> {
   foo: T
}

type Bar<T> {
   bar: T
}

type Baz {
   baz: Bar<Foo<String>>
}

// { baz: { bar: { foo: "xyz" } } }
n614cd commented 3 years ago

In many ways GraphQl lends itself to data driven architectures; except the lack of generics, it pretty much kills the ability to use a common set of queries and mutations. Technically, you can almost solve this with interfaces.

In my current system, I have roughly fifty entities, which are flattened down to twenty five objects consumed by the UI. The effectively means instead of four query/mutations for manage and perform all CRUD, I have a hundred. (search, create, update and delete per UI object). This makes the API very unwieldy, and is before we get into any transactional specific APIs.

Tim

dan-turner commented 2 years ago

Any progress or developments on this?

ghost commented 2 years ago

I would recommend going off of the TS Spec because it has almost everything GraphQL needs for generics.

Regarding implementation, I think I have an idea for a new typed API. Here's how it would look in JavaScript:

const Change = GQLAlias('Change', ['T'], T => GQLObject([
    GQLProperty('old', T.NotNull()),
    GQLProperty('new', T.NotNull()),
]))

// usage:
const schema = Schema([
    Property('getLatestChange', Change(GQLString)),
    // ...
])

That is just an idea; I am going to try to implement a better schema/resolver API for a smaller library to test this example.

Also, I recommend replacing the ! operator with a sort of Maybe<T> for better type safety.

rgolea commented 1 year ago

I can't wait for this to become a reality. It's the main reason I had to move away from graphql as it become so hard to maintain and work with. I had to remember all these properties I had to distribute everywhere. And yes, implements works pretty well but at the end of the day I had to copy all the properties around.

However, thank you all for the awesome contribution. I can't stress enough how much I love graphql.

DeveloperTheExplorer commented 1 year ago

It has been almost 7 years. Any updates? This is very much needed for schema/resolver definitions. Almost all endpoints need this whenever pagination is involved. And almost all systems implement some sort of pagination/cursor.

akomm commented 1 year ago

Going schema DSL in the graphql implementations (libs, not gql itself) is one of the biggest mistakes IMO. You can see it by all those "solutions" cooked around it by now, to fix problem A, B, C and problems still being present to this day. Whoever stayed on the code side without all the mess around it, has much less problems with graphql.

Like always, I try out new tech and evaluate it. It feels tempting at first glance, but the price vs. benefit is hugely disproportional.

You can have quite clean schema definitions also in code. Without all the primitive constructors provided by the impl. libraries. And you can eliminate repetition that you try to solve with generics in schema, just by using code.

The examples I've seen so far here, that should prove the code approach without generics leads to bad naming, feels artificial. Most of the time its a matter of Change<String> vs. ChangeString. The example variants with nullability are not really useful. To talk about whether its a problem or not needs some real world example to see whether its even needed for the type in the specific case to have the nullability encoded in the name this way. I can just imagine RL examples for that where you don't actually need it, unless you make some weird design decision. If you want to avoid collision in name, the first question is why do you have those two variants, what is the intent of the data structure you try to describe and isn't the nullability something that is rather implied from a different type name as a base (OptionalChange<T> vs. Change<T>) instead.

I'm not saying there is no problem, but just that the examples I've seen here so far are IMO to artificial to be convincing.

jamietdavidson commented 1 year ago

Would like to reup this. Seems relevant and I stumble into Enum / Interface related issues every few months that would be solved by this. It's kind of the last missing piece to making GraphQL not leaving something to be desired, IMO.

n614cd commented 10 months ago

Is there a specific blocker to making this happen? Resources to push it through? Some unsolved problem?

varvay commented 9 months ago

At first I was excited to the idea about generic type support in GraphQL, since it might introduces high degree of Intuitiveness for developer to translate the GraphQL schema into code implementation in various language and framework. It's the same concept as having scalar data types as much as possible to match all the possible implementor languages e.g., think how intuitive is it to implement the schema when it supports data type like hashmap, JSON node, etc.

But I've been thinking to myself about this and end up by accepting the current GraphQL specification without generic type support. The question i've been asking myself are,

So I started from the mindset that GraphQL is a specification used as contract between frontend and backend on what data they will exchanging and how are they structured. There will be complication introduced with generic type implementation on these information received by the client, for example how does the client knows the structure of the object defined as generic? There must be an information to communicate it right? you'll ended up by sending some kind of metadata, which is redundant to __typename.

The next question is,

I don't think so. The client's frameworks will still need to implement the data resolution abstraction and the quality of the information received by the client doesn't necessarily increased since the only additional information are "this object is generic and it might be in type A, B or C", which is communicated already through the usage of union type definition.

I think it does fulfill them with two possible solutions on the table,

  1. using interface.
  2. using union data type, which is the one I prefer.

Discussing the 2nd solution further,

type Post {
   # some type definition
}

type Profile {
   # some type definition
}

type Tag {
   # some type definition
}

type Setting {
   # some type definition
}

union Pageable = Post | Profile | Tag | Setting # Verbosity potential

type PagedResult {
   data: [Pageable]
   page: Int
   # some other pagination related definition
}

type Query {
   getPosts(): PagedResult
   getProfiles(): PagedResult
   getTags(): PagedResult
   getSettings(): PagedResult
}

Such query is resolvable in the code implementation by mapping the type based on the __typename metadata, for example like using discriminated union in typescript. I also still consider the schema definition verbosity is acceptable since the only verbosity came from Pageable union type definition, since for new type extending pagination functionality will goes to this list. But the tradeoff with strong data typing and information quality build upon the contract are reasonable.

Or what you really asking is some kind of no-type data and omitting the type enforcement feature? I think this is oppose to the main goal of GraphQL itself.

Finally, based on those thinking, I concluded for myself that in use cases I've seen so far, the only benefit I'm going after is syntactic sugar. This might also be your case. In my opinion, If there are any work should be done regarding this, they should be on the client side (developer and implementor framework).

mathroc commented 9 months ago

for example how does the client knows the structure of the object defined as generic? There must be an information to communicate it right? you'll ended up by sending some kind of metadata, which is redundant to __typename.

no, if you have:

type Post {
  name: String!
}

type List<T> {
  nodes: [T!]!
}

type Query {
  posts: List<Post>
}

then there is no doubt about what posts.nodes contains in query { posts { nodes {}}}

the only additional information are "this object is generic and it might be in type A, B or C"

This is not what generics do

and it's probably the same misunderstanding, but if you use generics in your example, it becomes:

type Post {
   # some type definition
}

type Profile {
   # some type definition
}

type Tag {
   # some type definition
}

type Setting {
   # some type definition
}

type PagedResult<T> {
   data: [T]
   page: Int
   # some other pagination related definition
}

type Query {
   getPosts(): PagedResult<Post>
   getProfiles(): PagedResult<Profile>
   getTags(): Paged<Tag>
   getSettings(): PagedResult<Setting>
}

And there's definitely readability improvements as-well as better typings than in the previous version (you know that query.getPosts.datawill always be of type [Post], not [Post|Profile|Tag|Setting]

nort3x commented 6 months ago

@mathroc

inspired by how C++ resolve templates, we can preprocess schema and produce this pipe:

input schema:


type PagedResult<T> {
   data: [T]
   page: Int
}

type Query {
   getPosts(): PagedResult<Post>
   getProfiles(): PagedResult<Profile>
}

processed schema:

type PagedResultPost{
   data: [Post]
   page: Int
}

type PagedResultProfile{
   data: [Profile]
   page: Int
}

type Query {
   getPosts(): PagedResultPost
   getProfiles(): PagedResultProfile
}

notice that we can't really mangle the outcome because it should be follow-able by consumer of the API
another issue is that the consumer should also use the same processor or re-implement them one by one

i used this in a small passion project and wasn't really an issue, but i understand the complication and integrity problems it could bring into the specification...

8 years is alot i think graphql specification should stay as is and task these improvements to it's successors