【问题标题】:GraphQL Blackbox / "Any" type?GraphQL 黑盒/“任何”类型?
【发布时间】:2018-01-17 19:45:49
【问题描述】:

是否可以指定 GraphQL 中的字段应该是黑盒,类似于 Flow 具有“任何”类型的方式?我的架构中有一个字段应该能够接受任意值,可以是字符串、布尔值、对象、数组等。

【问题讨论】:

    标签: javascript graphql graphql-js


    【解决方案1】:

    对于类似的问题,我创建了这样的架构:

    """`MetadataEntry` model"""
    type MetadataEntry {
      """Key of the entry"""
      key: String!
    
      """Value of the entry"""
      value: String!
    }
    
    """Object with metadata"""
    type MyObjectWithMetadata {
    
      """
      ... rest of my object fields
      """
    
      """
      Key-value entries that you can attach to an object. This can be useful for
      storing additional information about the object in a structured format
      """
      metadata: [MetadataEntry!]!
    
      """Returns value of `MetadataEntry` for given key if it exists"""
      metadataValue(
        """`MetadataEntry` key"""
        key: String!
      ): String
    }
    

    我的查询可能如下所示:

    query {
      listMyObjects {
        # fetch meta values by key
        meta1Value: metadataValue(key: "meta1")
        meta2Value: metadataValue(key: "meta2")
        # ... or list them all
        metadata {
          key
          value
        }
      }
    }
    

    【讨论】:

    • 如果我将 INT 值传递给 key->vals 之一,这会起作用吗?
    • @Gerfried 你需要更新架构
    【解决方案2】:

    只需通过 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);
        }
    }
    

    【讨论】:

      【解决方案3】:

      对于大多数用例,您可以使用 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 如何将解析器中返回的值序列化到响应中返回的dataparseLiteral 方法告诉 GraphQL 如何处理传递给参数的文字值(如"foo",或4.2[12, 20])。 parseValue 方法告诉 GraphQL 如何处理传递给参数的 变量 的值。

      对于parseValueserialize,我们可以只返回给定的值。因为parseLiteral 被赋予了一个代表字面量值的AST 节点对象,所以我们需要做一些工作来将它转换成合适的格式。

      您可以采用上述标量并根据需要通过添加验证逻辑对其进行自定义。在这三种方法中的任何一种中,您都可以抛出错误以指示无效值。例如,如果我们想允许大多数值但不想序列化函数,我们可以这样做:

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

      在您的架构中使用上述标量很简单。如果您使用的是普通 GraphQL.js,那么就像使用任何其他标量类型(GraphQLStringGraphQLInt 等)一样使用它。如果您使用的是 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
      `
      

      【讨论】:

        【解决方案4】:

        @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!
        }
        

        【讨论】:

        • 一个具有 uniq id 类型的对象呢? (id: String!, 其他文件...)
        • 你救了我。你救了我的项目!!谢啦!!
        【解决方案5】:

        我想出了一个折中的解决方案。我没有尝试将这种复杂性推到 GraphQL 上,而是选择在将数据设置到现场之前仅使用 String 类型和 JSON.stringifying 我的数据。所以一切都被字符串化了,稍后在我的应用程序中,当我需要使用这个字段时,我 JSON.parse 将结果取回所需的对象/数组/布尔值/等。

        【讨论】:

        • 聪明的做法,不错!
        • 聪明。感谢分享。
        • 这很棒而且简单,喜欢它。
        • 谢谢 :) 这不是一个完美的解决方案,但肯定是一个务实的解决方案
        【解决方案6】:

        是的。只需创建一个允许任何内容的新 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;
            }
        }
        

        【讨论】:

          猜你喜欢
          • 2020-07-05
          • 2020-03-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-05-12
          • 1970-01-01
          相关资源
          最近更新 更多