graphql-spec
graphql-spec copied to clipboard
Support for Tuples
I work with a datatype, mobiledoc cards, that's represented as a tuple of a string followed by an object. Currently, to make it work in graphql I need to denormalize it to an object, then normalize to a tuple, and so on back and forth between our backend and frontend. Ideally I'd like a way to represent that the data structure as a tuple.
I would like to be able to do the following
type CardPayload {
foo: String!
bar: Int!
}
type Content {
cards: [String!, CardPayload!]
}
query {
content {
cards [name, { foo }]
}
}
Glad you found a workaround for it! Just to make sure I understood, instead of a tuple, you're doing:
type MobiledocCard {
name: String!
payload: CardPayload!
}
# This is all the different kinds of cards:
union CardPayload = SomeCardPayload | AnotherCardPayload # | ... | ...
type SomeCardPayload {
# a concrete kind of card
}
type AnotherCardPayload {
# another concrete kind of card
}
Then, you can query it like
cards {
name
... on SomeCardPayload { ... }
... on AnotherCardPayload { ... }
}
Is that right? Anyhow, it sounds like you found a nice way to provide meaningful names to that tuple!
Yes, that's correct on how we're handling it. Unfortunately, querying it in this way does not give us valid mobiledoc since tuples are a core part of its datastructure. This results in us having to map over the graphql responses and convert the pojos back to valid mobiledoc.
Tuple support is interesting. I think it's a less-generalized version of user-defined parameterized types. I could see parameterized types both adding a lot of value and adding a lot of pain for implementers, as well as confusion for people using them.
If someone wants to champion tuple support, I'd encourage doing it in a way that in the future could also unlock parameterized types. Tuples could also be used to solve the Map-type question as well.
I'm marking this as "needs champion", but whoever takes this on should also read through the map-types discussion at #101. I think they have a lot of problems and solution-spaces in common.
Hey @mjmahone, I'm looking to take this, but I have a few questions.
As for the proposed change: Should you be able to query for specific items in a tuple, and/or skip items in a tuple. E.g. get only the 2nd item? What does the result look like in this case? Would it be best to keep it simple and worry about that as a follow up feature?
Is a tuple a new type of Value? Or does it belong in the Selection Sets section?
Do you have any suggestions on how to implement this in a way that would allow for parameterized types?
As for the proposal: It looks like I should open a PR on this repo for the spec, and a pr on graphql-js for the implementation?
I want to give an example of the use case of tuple. I am building a project of which background is Math. We need query something like coordinate, size dimension..
Another use case:
3-tuples for representing conditions:
[fieldPath, operator, value]
e.g. ["profile.age", GREATER_THAN, 18]
I see that frameworks like Prisma append the operator to the field name, like title_contains, which doesn't seem ideal.
Something like {fieldPath: "x", operator: GREATER_THAN, value: 18} is too verbose.
Edit:
And ordering:
[fieldPath, direction] -> ["profile.age", DESC].
This sounds like a case for not just tuples but associative arrays and multi dimensional arrays in general. That way you are not bound to just 2 values.
@excitedbox Tuples are not limited to 2 elements. Or do you mean something else?
I have a use case where we have several ranges which are optional, but cannot be unbounded if given. Essentially, range: [Int!, Int!] -- either the whole thing is null or it requires exactly two integers.
This is not correctly satisfied by two columns, e.g. upperBound and lowerBound, because they would both have to be nullable, which erroneously suggests you can have one without the other.
It could be semi-correctly represented as a nullable array of integers [Int!], but that does not guarantee exactly two elements and would require additional checks in the client everywhere it's used.
Latitude and longitude are a similar use case, which we are currently working around with two columns and jumping through a bunch of hoops.
@rintaun for these cases, you can create a new type
type MyType {
range: Range
}
type Range {
lowerBound: Int!
upperBound: Int!
}
That way they can both be null by having range be nullable but if range is not null, you guarantee both bounds are there
A +1 for including tuples is for incorporating the GeoJSON IETF standard as a type where the Geometry part is specified in float arrays [[float, float], ... ]
Instead of having to convert the nested arrays to objects, and then convert them back into arrays to maintain compatibility with the standard
Full example:
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 1.0],
[100.0, 0.0]
]
]
}
Originally posted by @r0kk3rz in https://github.com/graphql/graphql-spec/issues/423#issuecomment-871794081
+1 for supporting GeoJSON spec
FWIW, I've been following along here for years specifically because I required the use of GeoJSON types.
However, as the project progressed and I became more familiar with how to "think in GraphQL," I realized that from the GraphQL perspective these coordinates were actually "scalar". That is, I was never interested in "part" of the field, but always treated it as "all or nothing."
Accordingly, we created custom scalars with their own validation logic in the "serialization" steps, which are otherwise just passthrough/JSON operations:
GeoJSONMultiPolygonCoordinates
GeoJSONPointCoordinates
GeoJSONLineStringCoordinates
GeoJSONMultiLineStringCoordinates
GeoJSONPolygonCoordinates
I found the simplest way of tackling this was to add a JSON type and just put the GeoJSON into that.
https://dev.to/trackmystories/how-to-geojson-with-apollo-graphql-server-27ij
Maybe it will be useful for apollo-graphql users. I solved this problem by using scalar types. I created a Range scalar type
scalar Range
Then I specified a resolver for it (it is not fully ready to use, but that explains the idea)
import { GraphQLScalarType, Kind } from 'graphql';
import { UserInputError } from 'apollo-server-fastify';
export const rangeScalar = new GraphQLScalarType({
name: 'Range',
description: 'Range custom scalar type',
serialize(value: [number, number]) {
return value;
},
parseValue(value: [number, number]) {
if (!Array.isArray(value)) throw new UserInputError('Must be array')
if (value.length !== 2) throw new UserInputError('Must be [Int!, Int!]')
return value;
},
parseLiteral(ast) {
if (ast.kind === Kind.LIST) {
if (ast.values.length !== 2) throw new UserInputError('Range must be tuple [Int, Int]')
return ast.values.map(field => {
if (field.kind !== 'IntValue') throw new UserInputError('Range must be int, but received' + field.kind)
return Number(field.value)
});
}
return null;
},
});
Also I added type mapping in the code generator config for supporting generation of Typescript interfaces (but I think that it would be better to import exact typescript interface/type or some another approach for situation when you have ыщьу complex type)
config:
scalars:
Range: "[number, number]"
So now these types look like this
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
Date: any;
JSON: object;
Range: [number, number];
};
export type GSomeType = {
someRange: Scalars['Range'];
};
As a result, now I can pass tuples like [number, number] in terms of TypeScript with validation inside the resolver.
I think it is not the best approach, but it works for me
Hi @ArtemKislov, thanks for the Range scalar, this works great thanks!!
Any news here?