【发布时间】:2018-01-17 19:45:49
【问题描述】:
是否可以指定 GraphQL 中的字段应该是黑盒,类似于 Flow 具有“任何”类型的方式?我的架构中有一个字段应该能够接受任意值,可以是字符串、布尔值、对象、数组等。
【问题讨论】:
标签: javascript graphql graphql-js
是否可以指定 GraphQL 中的字段应该是黑盒,类似于 Flow 具有“任何”类型的方式?我的架构中有一个字段应该能够接受任意值,可以是字符串、布尔值、对象、数组等。
【问题讨论】:
标签: javascript graphql graphql-js
对于类似的问题,我创建了这样的架构:
"""`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
}
}
}
【讨论】:
只需通过 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);
}
}
【讨论】:
对于大多数用例,您可以使用 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
在您的架构中使用上述标量很简单。如果您使用的是普通 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
`
【讨论】:
@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!
}
【讨论】:
我想出了一个折中的解决方案。我没有尝试将这种复杂性推到 GraphQL 上,而是选择在将数据设置到现场之前仅使用 String 类型和 JSON.stringifying 我的数据。所以一切都被字符串化了,稍后在我的应用程序中,当我需要使用这个字段时,我 JSON.parse 将结果取回所需的对象/数组/布尔值/等。
【讨论】:
是的。只需创建一个允许任何内容的新 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;
}
}
【讨论】: