javascript graphql graphql-js

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 `