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", 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/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..eba9259 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", @@ -7,24 +7,20 @@ }, "MyUnion": { "title": "MyUnion", - "type": "GRAPHQL_UNION", "oneOf": [ { "$ref": "#/definitions/Foo" }, { - "type": "string", - "required": false + "type": "string" }, { - "type": "number", - "required": false + "type": "number" } ] }, "MyEnum": { "title": "MyEnum", - "type": "GRAPHQL_ENUM", "enum": [ "FIRST_ITEM", "SECOND_ITEM", @@ -37,13 +33,11 @@ "properties": { "my_field": { "type": "integer", - "required": false, "title": "my_field", "arguments": [] }, "req_field": { "type": "string", - "required": true, "title": "req_field", "arguments": [] }, @@ -89,10 +83,7 @@ "first": { "type": "array", "items": { - "type": { - "type": "number", - "required": false - } + "type": "number" }, "title": "first", "arguments": [] @@ -100,20 +91,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 +108,6 @@ }, "bool": { "type": "boolean", - "required": true, "title": "bool", "arguments": [] }, @@ -138,14 +123,12 @@ }, "with_params": { "type": "integer", - "required": false, "title": "with_params", "arguments": [ { "title": "param1", "type": { - "type": "integer", - "required": false + "type": "integer" }, "defaultValue": null }, @@ -154,10 +137,7 @@ "type": { "type": "array", "items": { - "type": { - "type": "number", - "required": false - } + "type": "number" } }, "defaultValue": null @@ -167,6 +147,7 @@ }, "required": [ "identifier", + "reference", "bool" ] }, @@ -177,12 +158,10 @@ "properties": { "an_int": { "type": "integer", - "required": true, "title": "an_int" }, "a_string": { "type": "string", - "required": false, "title": "a_string" } }, @@ -191,4 +170,4 @@ ] } } -} +} \ No newline at end of file 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 3e7f9ce..a8ccd0b 100644 --- a/spec/transformSpec.js +++ b/spec/transformSpec.js @@ -1,8 +1,11 @@ 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 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', () => { @@ -21,4 +24,33 @@ 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) + } + }); + + 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 e030176..7ff0d94 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 { @@ -63,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 }] }; @@ -71,47 +68,52 @@ const toSchemaProperty = field => { return Object.assign( propertyType, { title: field.name.value }, - field.arguments ? { arguments: toFieldArguments(field.arguments) } : {} + field.arguments && !strictMode ? { arguments: toFieldArguments(field.arguments) } : {} ); } + +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 * * @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' - } + return Object.assign( + { + title: definition.name.value + }, + 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 fields = definition.fields.map(toSchemaProperty); + const required = getRequiredFields(definition.fields); + + const fields = definition.fields.map(toSchemaProperty(strictMode)); 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', @@ -119,7 +121,7 @@ const toSchemaObject = definition => { required, }; - if (definition.kind === 'InputObjectTypeDefinition') { + if (!strictMode && definition.kind === 'InputObjectTypeDefinition') { Object.assign(schemaObject, { input: true }); } @@ -130,13 +132,18 @@ const toSchemaObject = 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 */ -const transform = document => { - const definitions = document.definitions.map(toSchemaObject); + +const transform = (document, strictMode = false) => { + // ignore directives + const definitions = document.definitions + .filter(d => d.kind !== 'DirectiveDefinition') + .map(toSchemaObject(strictMode)); const schema = { - $schema: 'http://json-schema.org/draft-04/schema#', + $schema: 'http://json-schema.org/draft-07/schema#', definitions: {} };