From 4d932246cd4e9eefd2ab4a8f894af72123691038 Mon Sep 17 00:00:00 2001 From: Charly POLY Date: Mon, 16 Apr 2018 11:36:57 +0200 Subject: [PATCH 1/8] fix(transform): skip directives --- transform.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/transform.js b/transform.js index e030176..de7d149 100644 --- a/transform.js +++ b/transform.js @@ -133,7 +133,10 @@ const toSchemaObject = definition => { * @return {object} A plain JavaScript object which conforms to JSON Schema */ const transform = document => { - const definitions = document.definitions.map(toSchemaObject); + // ignore directives + const definitions = document.definitions. + filter(d => d.kind !== 'DirectiveDefinition'). + map(toSchemaObject); const schema = { $schema: 'http://json-schema.org/draft-04/schema#', From 332e1bfbad759c69f6b852e6143592cc22ee5415 Mon Sep 17 00:00:00 2001 From: Charly POLY Date: Mon, 16 Apr 2018 17:14:15 +0200 Subject: [PATCH 2/8] feat(transform): should return a valid JSON schema --- package.json | 1 + spec/data/mock_schema.json | 39 ++++++++++---------------------------- spec/transformSpec.js | 19 +++++++++++++++++++ transform.js | 22 ++++++++++----------- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 5fe7b4c..13a2f50 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "graphql": "^0.10.1" }, "devDependencies": { + "ajv": "^6.4.0", "jasmine": "^2.6.0" } } diff --git a/spec/data/mock_schema.json b/spec/data/mock_schema.json index ebbec8c..ba0d66f 100644 --- a/spec/data/mock_schema.json +++ b/spec/data/mock_schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "Foo": { "title": "Foo", @@ -13,12 +13,10 @@ "$ref": "#/definitions/Foo" }, { - "type": "string", - "required": false + "type": "string" }, { - "type": "number", - "required": false + "type": "number" } ] }, @@ -37,13 +35,11 @@ "properties": { "my_field": { "type": "integer", - "required": false, "title": "my_field", "arguments": [] }, "req_field": { "type": "string", - "required": true, "title": "req_field", "arguments": [] }, @@ -89,10 +85,7 @@ "first": { "type": "array", "items": { - "type": { - "type": "number", - "required": false - } + "type": "number" }, "title": "first", "arguments": [] @@ -100,20 +93,15 @@ "identifier": { "type": "array", "items": { - "type": { - "type": "string", - "required": false - } + "type": "string" }, - "required": true, "title": "identifier", "arguments": [] }, "reference": { "allOf": [ { - "$ref": "#/definitions/Stuff", - "required": true + "$ref": "#/definitions/Stuff" }, { "title": "reference" @@ -122,7 +110,6 @@ }, "bool": { "type": "boolean", - "required": true, "title": "bool", "arguments": [] }, @@ -138,14 +125,12 @@ }, "with_params": { "type": "integer", - "required": false, "title": "with_params", "arguments": [ { "title": "param1", "type": { - "type": "integer", - "required": false + "type": "integer" }, "defaultValue": null }, @@ -154,10 +139,7 @@ "type": { "type": "array", "items": { - "type": { - "type": "number", - "required": false - } + "type": "number" } }, "defaultValue": null @@ -167,6 +149,7 @@ }, "required": [ "identifier", + "reference", "bool" ] }, @@ -177,12 +160,10 @@ "properties": { "an_int": { "type": "integer", - "required": true, "title": "an_int" }, "a_string": { "type": "string", - "required": false, "title": "a_string" } }, @@ -191,4 +172,4 @@ ] } } -} +} \ No newline at end of file diff --git a/spec/transformSpec.js b/spec/transformSpec.js index 3e7f9ce..345a94c 100644 --- a/spec/transformSpec.js +++ b/spec/transformSpec.js @@ -1,7 +1,9 @@ const transform = require('../index.js'); const fs = require('fs'); const path = require('path'); +const Ajv = require('ajv'); +const ajv = new Ajv(); const mockJSONSchema = require(path.join(__dirname, 'data/mock_schema.json')); const mockGraphQL = fs.readFileSync(path.join(__dirname, 'data/mock_schema.graphql'), { encoding: 'utf-8' }); @@ -21,4 +23,21 @@ describe('GraphQL to JSON Schema transform', () => { it('parses a test GraphQL Schema properly', () => { expect(transform(mockGraphQL)).toEqual(mockJSONSchema); }); + + it('return a valid JSON Schema definition', () => { + const schema = transform(` + type Stuff { + my_field: Int + req_field: String! + recursion: MoreStuff + custom_scalar: Foo + enum: MyEnum + } + `); + const valid = ajv.validateSchema(schema); + expect(valid).toBe(true); + if (!valid) { + console.log(ajv.errors) + } + }); }) diff --git a/transform.js b/transform.js index e030176..7e169f5 100644 --- a/transform.js +++ b/transform.js @@ -20,19 +20,16 @@ const PRIMITIVES = { const getPropertyType = type => { switch (type.kind) { case 'NonNullType': - return Object.assign(getPropertyType(type.type), { required: true }); + return getPropertyType(type.type); case 'ListType': return { type: 'array', - items: { - type: getPropertyType(type.type) - } + items: getPropertyType(type.type) } default: if (type.name.value in PRIMITIVES) { return { - type: PRIMITIVES[type.name.value], - required: false + type: PRIMITIVES[type.name.value] }; } else { @@ -75,6 +72,11 @@ const toSchemaProperty = field => { ); } + +const getRequiredFields = fields => fields + .filter(f => f.type ? f.type.kind === 'NonNullType' : f.kind === 'NonNullType') + .map(f => f.name.value); + /** * Converts a single GQL definition into a plain JS schema object * @@ -103,15 +105,13 @@ const toSchemaObject = definition => { }; } + const required = getRequiredFields(definition.fields); + const fields = definition.fields.map(toSchemaProperty); const properties = {}; for (let f of fields) properties[f.title] = f.allOf ? { allOf: f.allOf } : f; - const required = fields - .filter(f => f.required) - .map(f => f.title); - let schemaObject = { title: definition.name.value, type: 'object', @@ -136,7 +136,7 @@ const transform = document => { const definitions = document.definitions.map(toSchemaObject); const schema = { - $schema: 'http://json-schema.org/draft-04/schema#', + $schema: 'http://json-schema.org/draft-07/schema#', definitions: {} }; From df584478b67edbf06789f9cced5763209edbac80 Mon Sep 17 00:00:00 2001 From: Charly POLY Date: Tue, 17 Apr 2018 10:32:41 +0200 Subject: [PATCH 3/8] style(transform): leading . --- transform.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/transform.js b/transform.js index de7d149..8eced07 100644 --- a/transform.js +++ b/transform.js @@ -134,9 +134,9 @@ const toSchemaObject = definition => { */ const transform = document => { // ignore directives - const definitions = document.definitions. - filter(d => d.kind !== 'DirectiveDefinition'). - map(toSchemaObject); + const definitions = document.definitions + .filter(d => d.kind !== 'DirectiveDefinition') + .map(toSchemaObject); const schema = { $schema: 'http://json-schema.org/draft-04/schema#', From f01c0ee447bb7f9f103742d00bd8ad092c9060ae Mon Sep 17 00:00:00 2001 From: Charly POLY Date: Tue, 17 Apr 2018 11:00:59 +0200 Subject: [PATCH 4/8] feat(transform): introduce strict mode for valid JSON schema generation --- index.js | 4 +- spec/data/mock_schema.json | 2 - spec/data/mock_schema_strict.json | 147 ++++++++++++++++++++++++++++++ spec/transformSpec.js | 13 +++ transform.js | 20 ++-- 5 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 spec/data/mock_schema_strict.json diff --git a/index.js b/index.js index 788aaa7..c7b5470 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ const parse = require('graphql/language').parse; const transform = require('./transform.js'); -module.exports = schema => { +module.exports = (schema, strictMode = false) => { if (typeof schema !== 'string') throw new TypeError('GraphQL Schema must be a string'); - return transform(parse(schema)); + return transform(parse(schema), strictMode); }; diff --git a/spec/data/mock_schema.json b/spec/data/mock_schema.json index ba0d66f..eba9259 100644 --- a/spec/data/mock_schema.json +++ b/spec/data/mock_schema.json @@ -7,7 +7,6 @@ }, "MyUnion": { "title": "MyUnion", - "type": "GRAPHQL_UNION", "oneOf": [ { "$ref": "#/definitions/Foo" @@ -22,7 +21,6 @@ }, "MyEnum": { "title": "MyEnum", - "type": "GRAPHQL_ENUM", "enum": [ "FIRST_ITEM", "SECOND_ITEM", diff --git a/spec/data/mock_schema_strict.json b/spec/data/mock_schema_strict.json new file mode 100644 index 0000000..2d780de --- /dev/null +++ b/spec/data/mock_schema_strict.json @@ -0,0 +1,147 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Foo": { + "title": "Foo" + }, + "MyUnion": { + "title": "MyUnion", + "oneOf": [ + { + "$ref": "#/definitions/Foo" + }, + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "MyEnum": { + "title": "MyEnum", + "enum": [ + "FIRST_ITEM", + "SECOND_ITEM", + "THIRD_ITEM" + ] + }, + "Stuff": { + "title": "Stuff", + "type": "object", + "properties": { + "my_field": { + "type": "integer", + "title": "my_field" + }, + "req_field": { + "type": "string", + "title": "req_field" + }, + "recursion": { + "allOf": [ + { + "$ref": "#/definitions/MoreStuff" + }, + { + "title": "recursion" + } + ] + }, + "custom_scalar": { + "allOf": [ + { + "$ref": "#/definitions/Foo" + }, + { + "title": "custom_scalar" + } + ] + }, + "enum": { + "allOf": [ + { + "$ref": "#/definitions/MyEnum" + }, + { + "title": "enum" + } + ] + } + }, + "required": [ + "req_field" + ] + }, + "MoreStuff": { + "title": "MoreStuff", + "type": "object", + "properties": { + "first": { + "type": "array", + "items": { + "type": "number" + }, + "title": "first" + }, + "identifier": { + "type": "array", + "items": { + "type": "string" + }, + "title": "identifier" + }, + "reference": { + "allOf": [ + { + "$ref": "#/definitions/Stuff" + }, + { + "title": "reference" + } + ] + }, + "bool": { + "type": "boolean", + "title": "bool" + }, + "union": { + "allOf": [ + { + "$ref": "#/definitions/MyUnion" + }, + { + "title": "union" + } + ] + }, + "with_params": { + "type": "integer", + "title": "with_params" + } + }, + "required": [ + "identifier", + "reference", + "bool" + ] + }, + "InputType": { + "title": "InputType", + "type": "object", + "properties": { + "an_int": { + "type": "integer", + "title": "an_int" + }, + "a_string": { + "type": "string", + "title": "a_string" + } + }, + "required": [ + "an_int" + ] + } + } +} \ No newline at end of file diff --git a/spec/transformSpec.js b/spec/transformSpec.js index 345a94c..a8ccd0b 100644 --- a/spec/transformSpec.js +++ b/spec/transformSpec.js @@ -5,6 +5,7 @@ const Ajv = require('ajv'); const ajv = new Ajv(); const mockJSONSchema = require(path.join(__dirname, 'data/mock_schema.json')); +const mockJSONSchemaStrict = require(path.join(__dirname, 'data/mock_schema_strict.json')); const mockGraphQL = fs.readFileSync(path.join(__dirname, 'data/mock_schema.graphql'), { encoding: 'utf-8' }); describe('GraphQL to JSON Schema transform', () => { @@ -40,4 +41,16 @@ describe('GraphQL to JSON Schema transform', () => { console.log(ajv.errors) } }); + + describe('with strict mode', () => { + it('parses a test GraphQL Schema properly', () => { + const schema = transform(mockGraphQL, true); + expect(schema).toEqual(mockJSONSchemaStrict); + const valid = ajv.validateSchema(schema); + expect(valid).toBe(true); + if (!valid) { + console.log(ajv.errors) + } + }); + }) }) diff --git a/transform.js b/transform.js index 7e169f5..1c10942 100644 --- a/transform.js +++ b/transform.js @@ -60,7 +60,7 @@ const toFieldArguments = _arguments => { * @param {object} field The GQL field object * @return {Object} a plain JS object containing the property schema or a reference to another definition */ -const toSchemaProperty = field => { +const toSchemaProperty = (strictMode = false) => field => { let propertyType = getPropertyType(field.type); if ('$ref' in propertyType) propertyType = { allOf: [propertyType, { title: field.name.value }] }; @@ -68,7 +68,7 @@ const toSchemaProperty = field => { return Object.assign( propertyType, { title: field.name.value }, - field.arguments ? { arguments: toFieldArguments(field.arguments) } : {} + field.arguments && !strictMode ? { arguments: toFieldArguments(field.arguments) } : {} ); } @@ -83,31 +83,31 @@ const getRequiredFields = fields => fields * @param {Object} definition The GQL definition object * @return {Object} A plain JS schema object */ -const toSchemaObject = definition => { +const toSchemaObject = (strictMode = false) => definition => { if (definition.kind === 'ScalarTypeDefinition') { return { title: definition.name.value, - type: 'GRAPHQL_SCALAR' + ...(strictMode ? {} : { type: 'GRAPHQL_SCALAR' }) } } else if (definition.kind === 'UnionTypeDefinition') { return { title: definition.name.value, - type: 'GRAPHQL_UNION', + // type: 'GRAPHQL_UNION', // type is optional here oneOf: definition.types.map(getPropertyType) } } else if (definition.kind === 'EnumTypeDefinition') { return { title: definition.name.value, - type: 'GRAPHQL_ENUM', + // type: 'GRAPHQL_ENUM', // type is optional here enum: definition.values.map(v => v.name.value) }; } const required = getRequiredFields(definition.fields); - const fields = definition.fields.map(toSchemaProperty); + const fields = definition.fields.map(toSchemaProperty(strictMode)); const properties = {}; for (let f of fields) properties[f.title] = f.allOf ? { allOf: f.allOf } : f; @@ -119,7 +119,7 @@ const toSchemaObject = definition => { required, }; - if (definition.kind === 'InputObjectTypeDefinition') { + if (!strictMode && definition.kind === 'InputObjectTypeDefinition') { Object.assign(schemaObject, { input: true }); } @@ -132,8 +132,8 @@ const toSchemaObject = definition => { * @param {Document} document The GraphQL document returned by the parse function of graphql/language * @return {object} A plain JavaScript object which conforms to JSON Schema */ -const transform = document => { - const definitions = document.definitions.map(toSchemaObject); +const transform = (document, strictMode = false) => { + const definitions = document.definitions.map(toSchemaObject(strictMode)); const schema = { $schema: 'http://json-schema.org/draft-07/schema#', From f12fb2c4db100b017c83e768e008f737bb88bedc Mon Sep 17 00:00:00 2001 From: Charly POLY Date: Tue, 17 Apr 2018 11:22:47 +0200 Subject: [PATCH 5/8] fix(transform): use Object.assign instead of spread --- transform.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/transform.js b/transform.js index 7cbe206..51a5e1f 100644 --- a/transform.js +++ b/transform.js @@ -85,10 +85,12 @@ const getRequiredFields = fields => fields */ const toSchemaObject = (strictMode = false) => definition => { if (definition.kind === 'ScalarTypeDefinition') { - return { - title: definition.name.value, - ...(strictMode ? {} : { type: 'GRAPHQL_SCALAR' }) - } + return Object.assign( + { + title: definition.name.value + }, + strictMode ? {} : { type: 'GRAPHQL_SCALAR' } + ); } else if (definition.kind === 'UnionTypeDefinition') { return { From f028005439fd61b71c38e07e6076d9d618828fe7 Mon Sep 17 00:00:00 2001 From: Charly POLY Date: Tue, 17 Apr 2018 11:38:21 +0200 Subject: [PATCH 6/8] doc(README): update doc --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d54fb58..a61b86c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ npm install graphql-json-schema ```js const transform = require('graphql-json-schema'); + // transform(schemaStr, strictMode=flase) + // - strict mode ensure a valid JSON schema generation + const schema = transform(` scalar Foo @@ -55,7 +58,7 @@ the code above prints the following JSON as a plain JS object: ```json { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "Foo": { "title": "Foo", From 720697cb18b96ded562665ac903ee882ef5964f2 Mon Sep 17 00:00:00 2001 From: Charly POLY Date: Tue, 17 Apr 2018 11:38:37 +0200 Subject: [PATCH 7/8] fix(transform): typo with last merge --- transform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transform.js b/transform.js index 51a5e1f..e9a4116 100644 --- a/transform.js +++ b/transform.js @@ -139,7 +139,7 @@ const transform = (document, strictMode = false) => { // ignore directives const definitions = document.definitions .filter(d => d.kind !== 'DirectiveDefinition') - .map(toSchemaObject); + .map(toSchemaObject(strictMode)); const schema = { $schema: 'http://json-schema.org/draft-07/schema#', From 708a2db85dc6d72140135577383df56dce484329 Mon Sep 17 00:00:00 2001 From: Charly POLY Date: Tue, 17 Apr 2018 16:08:50 +0200 Subject: [PATCH 8/8] doc --- transform.js | 1 + 1 file changed, 1 insertion(+) diff --git a/transform.js b/transform.js index e9a4116..7ff0d94 100644 --- a/transform.js +++ b/transform.js @@ -132,6 +132,7 @@ const toSchemaObject = (strictMode = false) => definition => { * GQL -> JSON Schema transform * * @param {Document} document The GraphQL document returned by the parse function of graphql/language + * @param {boolean} strictMode Should return a valid JSON Schema (default: false) * @return {object} A plain JavaScript object which conforms to JSON Schema */