javascript - GraphQL Blackbox/"Any" type?
graphql-js (4)
La respuesta de @ mpen es excelente, pero opté por una solución más compacta:
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
}
}
})
Entonces mi resolvers se ve así:
{
Object: ObjectScalarType,
RootQuery: ...
RootMutation: ...
}
Y mi
.gql
ve así:
scalar Object
type Foo {
id: ID!
values: Object!
}
¿Es posible especificar que un campo en GraphQL debería ser una caja negra, similar a cómo Flow tiene un tipo "cualquiera"? Tengo un campo en mi esquema que debería ser capaz de aceptar cualquier valor arbitrario, que podría ser una cadena, booleano, objeto, matriz, etc.
Sí.
Simplemente cree un nuevo
GraphQLScalarType
que permita cualquier cosa.
Aquí hay uno que escribí que permite objetos. Puede extenderlo un poco para permitir más tipos de raíz.
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;
}
}
Se me ocurrió una solución intermedia.
En lugar de tratar de llevar esta complejidad a GraphQL, estoy optando por usar el tipo de
String
y
JSON.stringify
mis datos antes de configurarlos en el campo.
Entonces, todo se encadena, y más tarde en mi aplicación, cuando necesito consumir este campo,
JSON.parse
el resultado para recuperar el objeto / matriz / booleano / etc.
Para la mayoría de los casos de uso, puede usar un tipo escalar JSON para lograr este tipo de funcionalidad. Hay una serie de bibliotecas existentes que puede importar en lugar de escribir su propio escalar, por ejemplo, graphql-type-json .
Si necesita un enfoque más ajustado, entonces querrá escribir su propio tipo escalar. Aquí hay un ejemplo simple con el que puede comenzar:
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}`)
}
}
Tenga en cuenta que los escalares se usan como
salidas
(cuando se devuelve en su respuesta) y como
entradas
(cuando se usan como valores para argumentos de campo).
El método de
serialize
le dice a GraphQL cómo
serializar
un valor devuelto en un resolutor en los
data
que se devuelven en la respuesta.
El método
parseLiteral
le dice a GraphQL qué hacer con un valor literal que se pasa a un argumento (como
"foo"
, o
4.2
o
[12, 20]
).
El método
parseValue
le dice a GraphQL qué hacer con el valor de una
variable
que se pasa a un argumento.
Para
parseValue
y
serialize
solo podemos devolver el valor que se nos da.
Debido a que a
parseLiteral
se le asigna un objeto de nodo AST que representa el valor literal, tenemos que hacer un poco de trabajo para convertirlo al formato apropiado.
Puede tomar el escalar anterior y personalizarlo según sus necesidades agregando lógica de validación según sea necesario. En cualquiera de los tres métodos, puede lanzar un error para indicar un valor no válido. Por ejemplo, si queremos permitir la mayoría de los valores pero no queremos serializar funciones, podemos hacer algo como:
if (typeof value == ''function'') {
throw new TypeError(''Cannot serialize a function!'')
}
return value
Usar el escalar anterior en su esquema es simple.
Si está usando Vanilla GraphQL.js, entonces úselo como lo haría con cualquiera de los otros tipos escalares (
GraphQLString
,
GraphQLInt
, etc.) Si está usando Apollo, deberá incluir el escalar en su mapa de resolución así como en tu 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
`