是否可以指定 GraphQL 中的字段应该是黑盒,类似于 Flow 具有“任何”类型的方式?我的架构中有一个字段应该能够接受任何任意值,可以是字符串、布尔值、对象、数组等。
GraphQL 黑盒/“任何”类型?
我想出了一个折衷的解决方案。我没有尝试将这种复杂性推到 GraphQL 上,而是选择只使用String类型并JSON.stringify在将数据设置到字段上之前对其进行 ing。所以一切都被字符串化,然后在我的应用程序中,当我需要使用这个字段时,我JSON.parse的结果是取回所需的对象/数组/布尔值/等。
@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!
}
是的。只需创建一个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;
    }
}
对于大多数用例,您可以使用 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", or4.2或[12, 20])的文字值。该parseValue方法告诉 GraphQL 如何处理传递给参数的变量值。
For parseValueandserialize我们可以只返回给定的值。因为parseLiteral给定了一个代表字面值的 AST 节点对象,我们必须做一些工作才能将其转换为适当的格式。
您可以通过根据需要添加验证逻辑来获取上述标量并根据您的需要对其进行自定义。在这三种方法中的任何一种中,您都可以抛出错误以指示无效值。例如,如果我们想允许大多数值但不想序列化函数,我们可以这样做:
if (typeof value == 'function') {
  throw new TypeError('Cannot serialize a function!')
}
return value
在您的架构中使用上述标量很简单。如果您使用的香草GraphQL.js,然后使用它,就像你将任何其他标量类型(的GraphQLString,GraphQLInt等等),如果你使用的阿波罗,你需要包括标在您的解析器地图以及就像在您的 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
`
只需通过 GraphQL 发送一个字符串化的值并在另一端解析它,例如使用这个包装类。
export class Dynamic {
    @Field(type => String)
    private value: string;
    getValue(): any {
        return JSON.parse(this.value);
    }
    setValue(value: any) {
        this.value = JSON.stringify(value);
    }
}