web-dev-qa-db-fra.com

GraphQL Blackbox / Type "Any"?

Est-il possible de spécifier qu'un champ dans GraphQL doit être une boîte noire, similaire à la façon dont Flow a un type "any"? J'ai un champ dans mon schéma qui devrait pouvoir accepter n'importe quelle valeur arbitraire, qui pourrait être une chaîne, un booléen, un objet, un tableau, etc.

19
Jon Cursi

La réponse de @ mpen est excellente, mais j'ai opté pour une solution plus compacte:

const { GraphQLScalarType } = require('graphql')
const { Kind } = require('graphql/language')

const ObjectScalarType = new GraphQLScalarType({
  name: 'Object',
  description: 'Arbitrary object',
  parseValue: (value) => {
    return typeof value === 'object' ? value
      : typeof value === 'string' ? JSON.parse(value)
      : null
  },
  serialize: (value) => {
    return typeof value === 'object' ? value
      : typeof value === 'string' ? JSON.parse(value)
      : null
  },
  parseLiteral: (ast) => {
    switch (ast.kind) {
      case Kind.STRING: return JSON.parse(ast.value)
      case Kind.OBJECT: throw new Error(`Not sure what to do with OBJECT for ObjectScalarType`)
      default: return null
    }
  }
})

Alors mes résolveurs ressemblent à:

{
  Object: ObjectScalarType,
  RootQuery: ...
  RootMutation: ...
}

Et mon .gql ressemble à:

scalar Object

type Foo {
  id: ID!
  values: Object!
}
12
a paid nerd

J'ai trouvé une solution intermédiaire. Plutôt que d'essayer de pousser cette complexité sur GraphQL, j'opte simplement d'utiliser le type String et JSON.stringifying mes données avant de les mettre sur le terrain. Tout est donc stratifié, et plus tard dans mon application, lorsque j'ai besoin de consommer ce champ, je JSON.parse le résultat pour récupérer l'objet/tableau/booléen/etc. souhaité.

11
Jon Cursi

Oui. Créez simplement un nouveau GraphQLScalarType qui permet tout.

Voici celui que j'ai écrit qui autorise les objets. Vous pouvez l'étendre un peu pour autoriser plus de types de racine.

import {GraphQLScalarType} from 'graphql';
import {Kind} from 'graphql/language';
import {log} from '../debug';
import Json5 from 'json5';

export default new GraphQLScalarType({
    name: "Object",
    description: "Represents an arbitrary object.",
    parseValue: toObject,
    serialize: toObject,
    parseLiteral(ast) {
        switch(ast.kind) {
            case Kind.STRING:
                return ast.value.charAt(0) === '{' ? Json5.parse(ast.value) : null;
            case Kind.OBJECT:
                return parseObject(ast);
        }
        return null;
    }
});

function toObject(value) {
    if(typeof value === 'object') {
        return value;
    }
    if(typeof value === 'string' && value.charAt(0) === '{') {
        return Json5.parse(value);
    }
    return null;
}

function parseObject(ast) {
    const value = Object.create(null);
    ast.fields.forEach((field) => {
        value[field.name.value] = parseAst(field.value);
    });
    return value;
}

function parseAst(ast) {
    switch (ast.kind) {
        case Kind.STRING:
        case Kind.BOOLEAN:
            return ast.value;
        case Kind.INT:
        case Kind.FLOAT:
            return parseFloat(ast.value);
        case Kind.OBJECT: 
            return parseObject(ast);
        case Kind.LIST:
            return ast.values.map(parseAst);
        default:
            return null;
    }
}
7
mpen

Pour la plupart des cas d'utilisation, vous pouvez utiliser un type scalaire JSON pour obtenir ce type de fonctionnalité. Il existe un certain nombre de bibliothèques existantes que vous pouvez simplement importer plutôt que d'écrire votre propre scalaire - par exemple, graphql-type-json .

Si vous avez besoin d'une approche plus précise, vous voudrez écrire votre propre type scalaire. Voici un exemple simple avec lequel vous pouvez commencer:

const { GraphQLScalarType, Kind } = require('graphql')
const Anything = new GraphQLScalarType({
  name: 'Anything',
  description: 'Any value.',
  parseValue: (value) => value,
  parseLiteral,
  serialize: (value) => value,
})

function parseLiteral (ast) {
  switch (ast.kind) {
    case Kind.BOOLEAN:
    case Kind.STRING:  
      return ast.value
    case Kind.INT:
    case Kind.FLOAT:
      return Number(ast.value)
    case Kind.LIST:
      return ast.values.map(parseLiteral)
    case Kind.OBJECT:
      return ast.fields.reduce((accumulator, field) => {
        accumulator[field.name.value] = parseLiteral(field.value)
        return accumulator
      }, {})
    case Kind.NULL:
        return null
    default:
      throw new Error(`Unexpected kind in parseLiteral: ${ast.kind}`)
  }
}

Notez que les scalaires sont utilisés à la fois comme sorties (lorsqu'ils sont retournés dans votre réponse) et comme entrées (lorsqu'ils sont utilisés comme valeurs pour les arguments de champ). La méthode serialize indique à GraphQL comment sérialiser une valeur renvoyée dans un résolveur dans le data renvoyé dans la réponse. La méthode parseLiteral indique à GraphQL ce qu'il faut faire avec une valeur littérale passée à un argument (comme "foo", ou 4.2 ou [12, 20]). La méthode parseValue indique à GraphQL quoi faire avec la valeur d'une variable transmise à un argument.

Pour parseValue et serialize, nous pouvons simplement renvoyer la valeur qui nous est donnée. Étant donné que parseLiteral reçoit un objet nœud AST représentant la valeur littérale, nous devons faire un peu de travail pour le convertir au format approprié.

Vous pouvez prendre le scalaire ci-dessus et le personnaliser selon vos besoins en ajoutant une logique de validation au besoin. Dans l'une des trois méthodes, vous pouvez générer une erreur pour indiquer une valeur non valide. Par exemple, si nous voulons autoriser la plupart des valeurs mais ne voulons pas sérialiser les fonctions, nous pouvons faire quelque chose comme:

if (typeof value == 'function') {
  throw new TypeError('Cannot serialize a function!')
}
return value

L'utilisation du scalaire ci-dessus dans votre schéma est simple. Si vous utilisez Vanilla GraphQL.js, utilisez-le comme vous le feriez pour n'importe quel autre type scalaire (GraphQLString, GraphQLInt, etc.) Si vous utilisez Apollo, vous ' ll faudra inclure le scalaire dans votre carte résolveur ainsi que dans votre SDL:

const resolvers = {
  ...
  // The property name here must match the name you specified in the constructor
  Anything,
}

const typeDefs = `
  # NOTE: The name here must match the name you specified in the constructor
  scalar Anything

  # the rest of your schema
`
2
Daniel Rearden