graphql-spec icon indicating copy to clipboard operation
graphql-spec copied to clipboard

Named Lists

Open nalchevanidze opened this issue 3 years ago • 13 comments

Named Lists

Motivation

Since GraphQL has only one type (List) for collections, we cannot represent collections with certain constraints.

examples of constrained lists:

  • Set: no duplicated elements
  • NonEmpty: contains at least 1 element.
  • Map: elements must provide unique field key (see #904).

if we could communicate this constraints, clients could eliminate invalid queries before they are sent and would not need to validate response types. e.g, the client would not need to check a resulting list is not empty.

Definition of Named Lists

"""
list with no duplicates
"""
list Set
list NonEmpty

type User {
  name: String!
}

type Query {
  ids:Set<Int>
  users: NonEmpty<User>
}

validation of Named Lists

const validateNonEmpty = list => {
  if (list.length < 1)
    throw new Error ("empty List!")
  }
  return list
}

const resolverMap = {
  NonEmpty: new GraphQLWrapperType({
    name: 'NonEmpty',
    kind: 'LIST',
    description: 'NonEmpty list',
    parseValue: validateNonEmpty,
    serialize: validateNonEmpty,
  })
}

Introspection

introspection of field ids.

"name": "ids",
"type": {
    "name": "Set",
    "kind": "LIST",
    "type": {
        "name":"Int",
        ...
    }
}

introspection of field users.

"name": "users",
"type": {
    "name": "NonEmpty",
    "kind": "LIST",
    "type": {
        "name":"User",
        ...
    }
}

Non-breaking change

Older clients that do not support named lists, can still recognise them as regular lists and will not break. However, they do not benefit from knowing this constraints.

references

the current proposal is already implemented in the experimental language iris

initial discussions about the proposal could be found at:

nalchevanidze avatar Dec 29 '21 19:12 nalchevanidze

Interesting idea; the concept of “adding additional type information” to an existing type is not dissimilar to the oneof proposal; however adding this to a wrapping type rather than a named type will bring its own interesting challenges I suspect.

I suggest you add this topic to an upcoming WG for discussion; you can do so by sending a PR to this (or any) agenda: https://github.com/graphql/graphql-wg/blob/main/agendas/2022/2022-01-06.md

benjie avatar Dec 29 '21 19:12 benjie

Definitely worth discussing! I believe this could be solved more generally with generic types, i.e. you should be able to create your own custom type Set<T> { values: [T] }. Lots of prior discussion in this space: required reading is probably @leebyron's explanation for why generics weren't added in the original redesign of GraphQL in 2015: https://github.com/graphql/graphql-spec/issues/190#issuecomment-230078626. Personally I think we've developed more motivating use cases in the last 6 years (error/result types, set and map types as you're proposing, and any other wrapper type people need generally) to revisit some form of generic typing beyond what we have with List and NonNull. Constrained list types are an interesting subset of the problem, and may in fact be a little orthogonal (as in your proposal, the JSON response will still be just a list the client knows is server-side validated rather than a wrapped type with an inner list).

mjmahone avatar Dec 31 '21 22:12 mjmahone

@mjmahone thanks. actually generic types does not solve problem with Sets. First, it would be more cumbersome to unwrap elements from set. However major issue will be following.

input Address { street : String, house: Int }
type User { name: String }

type Set<a> { values: [a] }

type Query {
   residents(addresses: Set<Address>): Set<User>
}

as we see, this schema will be invalid, since output type can't be used as input value. However, with Named Lists it is possible.

input Address { street : String, house: Int }
type User { name: String }
list Set

type Query {
   residents(addresses: Set<Address>): Set<User>
}

nalchevanidze avatar Jan 05 '22 13:01 nalchevanidze

this schema will be invalid, since output type can't be used as input value.

Heh we'd need an InputSet<T> and OutputSet<T> (personally I think we messed up, for instance, in not splitting enums by input/output, as input enums can evolve differently from output enums in terms of what's future/backwards compatible).

mjmahone avatar Jan 06 '22 19:01 mjmahone

One use-case for named lists is that it makes it possible to apply directives to lists. Currently, this isn't possible for nested lists in the SDL.

fotoetienne avatar Jan 06 '22 19:01 fotoetienne

Similarly, it would allow doc-strings on lists

fotoetienne avatar Jan 06 '22 20:01 fotoetienne

Opened a discussion of some considerations related to this issue: https://github.com/graphql/graphql-wg/discussions/860

rivantsov avatar Jan 10 '22 02:01 rivantsov

@mjmahone technically output enums are union variants without fields. however GraphQL does not support it. you can checkout how it could theoretically work in iris

nalchevanidze avatar Jan 21 '22 14:01 nalchevanidze

NonEmpty lists.

Easily solved by a NonEmpty directive and does not require new syntax or new concepts. The directive does not need to be 'standard', defined in spec; a custom dir is enough. If the API designer thinks that expressing this constraint is important enough to express in SDL, then he can define @NonEmpty dir, add doc string to it, and then use it in SDL. Problem solved.

Set (no duplicated elements).

Same as NonEmpty, can be done with a custom directive. There is one more important aspect - when you talk about 'uniqueness', you need to talk about 'equality', because you catch non-uniqueness by finding 'equal' elements. In languages like c#, there are concepts like IComparable(IEquatable) interfaces, and Comparer objects etc. To create a dictionary or hash table, you have to provide a method for comparing values - which is not always obvious, and often there's some flexibility. For your unique lists, how to compare floats? strings equality - believe me it is not that obvious for some languages. Equality and comparison for sorting usually provided by language 'driver' (collation for databases).

And complex custom type with with a few properties - what does it mean that two objects are 'equal' - all property values are equal? You have to state it explicitly.

The whole issue of uniqueness gets messy very quickly, and I do not think adding this complexity to spec is worth limited potential benefit for not so many beneficiaries (I think there would be not many uses in practice). But in fact, with custom @unique dir defined by an API, the server/docs can specify what equality means for types allowed in Lists with this directive.

Map: elements must provide unique key - here 'unique' again, with the same 'comparable' challenge.

Summary:

NonEmpty and Unique lists can be perfectly handled by custom directives, no new concepts are needed.

Maps (through extra tuple type) - is a separate matter

rivantsov avatar Jan 25 '22 06:01 rivantsov

Easily solved by a NonEmpty directive and does not require new syntax or new concepts. The directive does not need to be 'standard', defined in spec; a custom dir is enough. If the API designer thinks that expressing this constraint is important enough to express in SDL, then he can define @nonempty dir, add doc string to it, and then use it in SDL. Problem solved.

If you define a schema like:

directive @nonEmpty on FIELD
type Query {
  numbers: [Int] @nonEmpty
}

Introspection will not reflect that the @nonEmpty directive applied to that field, therefore a consumer of the GraphQL API cannot know that field was defined to be non-empty - this applies to IDEs like GraphiQL, code generators, clients and the like.

Hopefully we'll add a way to expose user-defined meta-information via introspection API in form of directives (#300) at some point, but we're not there yet.

benjie avatar Feb 03 '22 09:02 benjie

@benjie - and that's the problem with introspection, we already discussed this briefly; and that SHOULD be fixed anyway, and SDL file and Introspection set should become one and the same thing, only in different formats.

rivantsov avatar Feb 03 '22 15:02 rivantsov

@benjie this is being explored. GraphQL-Java, GraphQL-DotNet, and HotChocolate have implemented a proposal that lets you introspect schema directives.

https://github.com/graphql-java/graphql-java/pull/2221

michaelstaib avatar Feb 28 '22 08:02 michaelstaib

Indeed, I'm a big proponent of #300 as linked in my comment above :+1:

benjie avatar Mar 01 '22 08:03 benjie