From 1e19d6675aaf2660ad7e57525a617bc865b1dac1 Mon Sep 17 00:00:00 2001 From: Jimmy Jia Date: Fri, 29 Mar 2019 14:58:28 -0400 Subject: [PATCH] feat: Add GraphQLJSONObject (#57) --- README.md | 23 ++-- src/index.js | 39 ++++-- test/index.test.js | 304 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 297 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 6d6a951..59583cd 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,36 @@ # graphql-type-json [![Travis][build-badge]][build] [![npm][npm-badge]][npm] -JSON scalar type for [GraphQL.js](https://github.com/graphql/graphql-js). +JSON scalar types for [GraphQL.js](https://github.com/graphql/graphql-js). [![Codecov][codecov-badge]][codecov] ## Usage -This package exports a JSON scalar GraphQL.js type: +This package exports a JSON value scalar GraphQL.js type: ```js import GraphQLJSON from 'graphql-type-json'; ``` +It also exports a JSON object scalar type: + +```js +import { GraphQLJSONObject } from 'graphql-type-json'; +``` + ### Programmatically-constructed schemas You can use this in a programmatically-constructed schema as with any other scalar type: ```js -import { GraphQLObjectType } from 'graphql'; -import GraphQLJSON from 'graphql-type-json'; +import GraphQLJSON, { GraphQLJSONObject } from 'graphql-type-json'; export default new GraphQLObjectType({ name: 'MyType', fields: { - myField: { type: GraphQLJSON }, + myValue: { type: GraphQLJSON }, + myObject: { type: GraphQLJSONObject }, }, }); ``` @@ -35,13 +41,15 @@ When using the SDL with GraphQL-tools, define `GraphQLJSON` as the resolver for ```js import { makeExecutableSchema } from 'graphql-tools'; -import GraphQLJSON from 'graphql-type-json'; +import GraphQLJSON, { GraphQLJSONObject } from 'graphql-type-json'; const typeDefs = ` scalar JSON +scalar JSONObject type MyType { - myField: JSON + myValue: JSON + myObject: JSONObject } # ... @@ -49,6 +57,7 @@ type MyType { const resolvers = { JSON: GraphQLJSON, + JSONObject: GraphQLJSONObject, }; export default makeExecutableSchema({ typeDefs, resolvers }); diff --git a/src/index.js b/src/index.js index 4f16f26..50866c9 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,26 @@ function identity(value) { return value; } +function ensureObject(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new TypeError( + `JSONObject cannot represent non-object value: ${value}`, + ); + } + + return value; +} + +function parseObject(ast, variables) { + const value = Object.create(null); + ast.fields.forEach(field => { + // eslint-disable-next-line no-use-before-define + value[field.name.value] = parseLiteral(field.value, variables); + }); + + return value; +} + function parseLiteral(ast, variables) { switch (ast.kind) { case Kind.STRING: @@ -13,14 +33,8 @@ function parseLiteral(ast, variables) { case Kind.INT: case Kind.FLOAT: return parseFloat(ast.value); - case Kind.OBJECT: { - const value = Object.create(null); - ast.fields.forEach(field => { - value[field.name.value] = parseLiteral(field.value, variables); - }); - - return value; - } + case Kind.OBJECT: + return parseObject(ast, variables); case Kind.LIST: return ast.values.map(n => parseLiteral(n, variables)); case Kind.NULL: @@ -42,3 +56,12 @@ export default new GraphQLScalarType({ parseValue: identity, parseLiteral, }); + +export const GraphQLJSONObject = new GraphQLScalarType({ + name: 'JSONObject', + description: + 'The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).', + serialize: ensureObject, + parseValue: ensureObject, + parseLiteral: parseObject, +}); diff --git a/test/index.test.js b/test/index.test.js index 45fc649..3171011 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -5,61 +5,88 @@ import { GraphQLSchema, } from 'graphql'; -import GraphQLJSON from '../src'; +import GraphQLJSON, { GraphQLJSONObject } from '../src'; const FIXTURE = { string: 'string', int: 3, - float: Math.PI, + float: 3.14, true: true, - false: true, + false: false, null: null, object: { string: 'string', int: 3, - float: Math.PI, + float: 3.14, true: true, - false: true, + false: false, null: null, }, - array: ['string', 3, Math.PI, true, false, null], + array: ['string', 3, 3.14, true, false, null], }; +function createSchema(type) { + return new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + value: { + type, + args: { + arg: { type }, + }, + resolve: (obj, { arg }) => arg, + }, + rootValue: { + type, + resolve: obj => obj, + }, + }, + }), + types: [GraphQLInt], + }); +} + describe('GraphQLJSON', () => { let schema; beforeEach(() => { - schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - value: { - type: GraphQLJSON, - args: { - arg: { - type: GraphQLJSON, - }, - }, - resolve: (obj, { arg }) => arg, - }, - }, - }), - types: [GraphQLInt], - }); + schema = createSchema(GraphQLJSON); }); describe('serialize', () => { - it('should support serialization', () => { - expect(GraphQLJSON.serialize(FIXTURE)).toEqual(FIXTURE); - }); + it('should support serialization', () => + graphql( + schema, + /* GraphQL */ ` + query { + rootValue + } + `, + FIXTURE, + ).then(({ data, errors }) => { + expect(data.rootValue).toEqual(FIXTURE); + expect(errors).toBeUndefined(); + })); }); describe('parseValue', () => { it('should support parsing values', () => - graphql(schema, 'query ($arg: JSON!) { value(arg: $arg) }', null, null, { - arg: FIXTURE, - }).then(({ data }) => { + graphql( + schema, + /* GraphQL */ ` + query($arg: JSON!) { + value(arg: $arg) + } + `, + null, + null, + { + arg: FIXTURE, + }, + ).then(({ data, errors }) => { expect(data.value).toEqual(FIXTURE); + expect(errors).toBeUndefined(); })); }); @@ -67,7 +94,7 @@ describe('GraphQLJSON', () => { it('should support parsing literals', () => graphql( schema, - ` + /* GraphQL */ ` query($intValue: Int = 3) { value( arg: { @@ -90,50 +117,219 @@ describe('GraphQLJSON', () => { ) } `, - ).then(({ data }) => { - expect(data.value).toEqual({ - string: 'string', - int: 3, - float: 3.14, - true: true, - false: false, - null: null, - object: { - string: 'string', - int: 3, - float: 3.14, - true: true, - false: false, - null: null, - }, - array: ['string', 3, 3.14, true, false, null], - }); + ).then(({ data, errors }) => { + expect(data.value).toEqual(FIXTURE); + expect(errors).toBeUndefined(); })); - it('should handle null literals', () => + it('should handle null literal', () => graphql( schema, - ` + /* GraphQL */ ` { value(arg: null) } `, - ).then(({ data }) => { + ).then(({ data, errors }) => { expect(data).toEqual({ value: null, }); + expect(errors).toBeUndefined(); + })); + + it('should handle list literal', () => + graphql( + schema, + /* GraphQL */ ` + { + value(arg: []) + } + `, + ).then(({ data, errors }) => { + expect(data).toEqual({ + value: [], + }); + expect(errors).toBeUndefined(); })); - it('should reject invalid literals', () => + it('should reject invalid literal', () => graphql( schema, - ` + /* GraphQL */ ` { value(arg: INVALID) } `, - ).then(({ data }) => { + ).then(({ data, errors }) => { + expect(data).toBeUndefined(); + expect(errors).toBeDefined(); + })); + }); +}); + +describe('GraphQLJSONObject', () => { + let schema; + + beforeEach(() => { + schema = createSchema(GraphQLJSONObject); + }); + + describe('serialize', () => { + it('should support serialization', () => + graphql( + schema, + /* GraphQL */ ` + query { + rootValue + } + `, + FIXTURE, + ).then(({ data, errors }) => { + expect(data.rootValue).toEqual(FIXTURE); + expect(errors).toBeUndefined(); + })); + + it('should reject string value', () => + graphql( + schema, + /* GraphQL */ ` + query { + rootValue + } + `, + 'foo', + ).then(({ data, errors }) => { + expect(data.rootValue).toBeNull(); + expect(errors).toBeDefined(); + })); + + it('should reject array value', () => + graphql( + schema, + /* GraphQL */ ` + query { + rootValue + } + `, + [], + ).then(({ data, errors }) => { + expect(data.rootValue).toBeNull(); + expect(errors).toBeDefined(); + })); + }); + + describe('parseValue', () => { + it('should support parsing values', () => + graphql( + schema, + /* GraphQL */ ` + query($arg: JSONObject!) { + value(arg: $arg) + } + `, + null, + null, + { + arg: FIXTURE, + }, + ).then(({ data, errors }) => { + expect(data.value).toEqual(FIXTURE); + expect(errors).toBeUndefined(); + })); + + it('should reject string value', () => + graphql( + schema, + /* GraphQL */ ` + query($arg: JSONObject!) { + value(arg: $arg) + } + `, + null, + null, + { + arg: 'foo', + }, + ).then(({ data, errors }) => { + expect(data).toBeUndefined(); + expect(errors).toBeDefined(); + })); + + it('should reject array value', () => + graphql( + schema, + /* GraphQL */ ` + query($arg: JSONObject!) { + value(arg: $arg) + } + `, + null, + null, + { + arg: [], + }, + ).then(({ data, errors }) => { + expect(data).toBeUndefined(); + expect(errors).toBeDefined(); + })); + }); + + describe('parseLiteral', () => { + it('should support parsing literals', () => + graphql( + schema, + /* GraphQL */ ` + query($intValue: Int = 3) { + value( + arg: { + string: "string" + int: $intValue + float: 3.14 + true: true + false: false + null: null + object: { + string: "string" + int: $intValue + float: 3.14 + true: true + false: false + null: null + } + array: ["string", $intValue, 3.14, true, false, null] + } + ) + } + `, + ).then(({ data, errors }) => { + expect(data.value).toEqual(FIXTURE); + expect(errors).toBeUndefined(); + })); + + it('should reject string literal', () => + graphql( + schema, + /* GraphQL */ ` + { + value(arg: "foo") + } + `, + ).then(({ data, errors }) => { + expect(data).toBeUndefined(); + expect(errors).toBeDefined(); + })); + + it('should reject array literal', () => + graphql( + schema, + /* GraphQL */ ` + { + value(arg: []) + } + `, + ).then(({ data, errors }) => { expect(data).toBeUndefined(); + expect(errors).toBeDefined(); })); }); });