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

Support for Tuples

Open tsiege opened this issue 7 years ago • 17 comments

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 }]
  }
}

tsiege avatar Nov 16 '18 21:11 tsiege

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!

rmosolgo avatar Nov 19 '18 18:11 rmosolgo

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.

tsiege avatar Nov 19 '18 19:11 tsiege

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.

mjmahone avatar Nov 20 '18 17:11 mjmahone

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?

tsiege avatar Jan 22 '19 17:01 tsiege

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..

XxZhang2017 avatar Apr 24 '19 00:04 XxZhang2017

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].

glen-84 avatar Jan 07 '20 09:01 glen-84

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 avatar Dec 30 '20 17:12 excitedbox

@excitedbox Tuples are not limited to 2 elements. Or do you mean something else?

glen-84 avatar Dec 30 '20 17:12 glen-84

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 avatar Apr 01 '21 18:04 rintaun

@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

thekuom avatar Apr 01 '21 18:04 thekuom

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

r0kk3rz avatar Jul 01 '21 02:07 r0kk3rz

+1 for supporting GeoJSON spec

thespacedeck avatar Jul 07 '21 20:07 thespacedeck

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

mike-marcacci avatar Jul 08 '21 16:07 mike-marcacci

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

r0kk3rz avatar Jul 12 '21 23:07 r0kk3rz

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

ArtemKislov avatar Jul 14 '21 20:07 ArtemKislov

Hi @ArtemKislov, thanks for the Range scalar, this works great thanks!!

thespacedeck avatar Jul 22 '21 06:07 thespacedeck

Any news here?

Voldemat avatar Sep 21 '23 08:09 Voldemat