Skip to content

Support for Tuples #534

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
tsiege opened this issue Nov 16, 2018 · 17 comments
Open

Support for Tuples #534

tsiege opened this issue Nov 16, 2018 · 17 comments
Labels
👻 Needs Champion RFC Needs a champion to progress (See CONTRIBUTING.md) 💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md)

Comments

@tsiege
Copy link

tsiege commented Nov 16, 2018

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 }]
  }
}
@rmosolgo
Copy link

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!

@tsiege
Copy link
Author

tsiege commented Nov 19, 2018

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.

@mjmahone
Copy link
Contributor

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 mjmahone added 👻 Needs Champion RFC Needs a champion to progress (See CONTRIBUTING.md) 💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md) labels Nov 20, 2018
@tsiege
Copy link
Author

tsiege commented Jan 22, 2019

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?

@XxZhang2017
Copy link

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

@glen-84
Copy link

glen-84 commented Jan 7, 2020

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

@excitedbox
Copy link

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.

@glen-84
Copy link

glen-84 commented Dec 30, 2020

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

@rintaun
Copy link

rintaun commented Apr 1, 2021

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.

@thekuom
Copy link

thekuom commented Apr 1, 2021

@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

@r0kk3rz
Copy link

r0kk3rz commented Jul 1, 2021

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 #423 (comment)

@thespacedeck
Copy link

+1 for supporting GeoJSON spec

@mike-marcacci
Copy link
Contributor

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

@r0kk3rz
Copy link

r0kk3rz commented Jul 12, 2021

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

@ArtemKislov
Copy link

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

@thespacedeck
Copy link

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

@Voldemat
Copy link

Any news here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
👻 Needs Champion RFC Needs a champion to progress (See CONTRIBUTING.md) 💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md)
Projects
None yet
Development

No branches or pull requests