GraphQL Blackbox/ "Любой" тип?

Можно ли указать, что поле в GraphQL должно быть черным ящиком, аналогичным тому, как Flow имеет "любой" тип? У меня есть поле в моей схеме, которое должно принимать любое произвольное значение, которое может быть String, Boolean, Object, Array и т.д.

Ответ 1

Ответ на

@mpen велик, но я выбрал более компактное решение:

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

Тогда мои резольверы выглядят так:

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

И мой .gql выглядит так:

scalar Object

type Foo {
  id: ID!
  values: Object!
}

Ответ 2

Я придумал решение средней линии. Вместо того, чтобы пытаться выполнить эту сложность на GraphQL, я предпочитаю использовать только теги String и JSON.stringify для моих данных, прежде чем устанавливать их в поле. Итак, все становится строгим, а позже в моем приложении, когда мне нужно использовать это поле, я JSON.parse результат, чтобы вернуть желаемый объект/array/boolean/etc.

Ответ 3

Да. Просто создайте новый GraphQLScalarType, который позволяет что угодно.

Здесь я написал, что позволяет объекты. Вы можете немного расширить его, чтобы разрешить больше корневых типов.

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

Ответ 4

В большинстве случаев вы можете использовать скалярный тип JSON для достижения такого рода функциональности. Существует целый ряд существующих библиотек, которые вы можете просто импортировать, а не писать свой собственный скаляр - например, graphql-type-json.

Если вам нужен более точный подход, чем вы захотите написать свой собственный скалярный тип. Вот простой пример, с которого вы можете начать:

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}')
  }
}

Обратите внимание, что скаляры используются как в качестве выходных данных (когда возвращаются в вашем ответе), так и в качестве входных данных (когда используются в качестве значений для аргументов поля). Метод serialize сообщает GraphQL, как сериализовать значение, возвращенное в резольвере, в data, возвращаемые в ответе. Метод parseLiteral сообщает GraphQL, что делать с литеральным значением, передаваемым аргументу (например, "foo", или 4.2 или [12, 20]). Метод parseValue сообщает GraphQL, что делать со значением переменной, переданной аргументу.

Для parseValue и serialize мы можем просто вернуть нам заданное значение. Поскольку parseLiteral получает объект узла AST, представляющий буквальное значение, нам нужно немного поработать, чтобы преобразовать его в соответствующий формат.

Вы можете взять вышеупомянутый скаляр и настроить его под свои нужды, добавив при необходимости логику проверки. В любом из трех методов вы можете выдать ошибку, чтобы указать недопустимое значение. Например, если мы хотим разрешить большинство значений, но не хотим сериализовать функции, мы можем сделать что-то вроде:

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

Использовать скаляр выше в вашей схеме просто. Если вы используете vanilla GraphQL.js, то используйте его так же, как и любой другой скалярный тип (GraphQLString, GraphQLInt и т.д.). Если вы используете Apollo, вам необходимо включить скаляр в вашу карту преобразователя. а также в вашем 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
'