├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── README.md ├── package.json ├── scripts ├── build ├── lint └── prepublish └── src ├── baseObject.js ├── enumeration.js ├── index.js ├── interface.js ├── object.js └── types.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "block-scoped-var": 0, 10 | "padded-blocks": 0 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | graphql-schema 2 | ============== 3 | 4 | Create GraphQL schemas with a fluent/chainable interface. 5 | 6 | **Notice to <=0.3.0 users:** 7 | 8 | The API has been changed significantly. Rather than hacking ES7 classes, `graphql-schema` now implements a fluent/chainable API. As a bonus, we can define entire schemas. 9 | 10 | ## Installation 11 | 12 | npm install graphql-schema 13 | 14 | ## Basic Usage 15 | 16 | ```js 17 | const rootQueryType = objectType('RootQueryType', 'TODO: Description') 18 | .field('hello', GraphQLString, 'Say hello to someone') 19 | .arg('name', GraphQLString, 'The name of the person to say hello to') 20 | .resolve(root, {name} => `Hello, ${name}`) 21 | .end(); 22 | ``` 23 | 24 | becomes 25 | 26 | ```js 27 | var rootQueryType = new GraphQLObjectType({ 28 | name: 'RootQueryType', 29 | description: 'TODO: Description' 30 | fields: { 31 | hello: { 32 | type: GraphQLString, 33 | description: 'Say Hello to someone', 34 | args: { 35 | name: { 36 | name: 'name', 37 | type: GraphQLString, 38 | description: 'The name of the person to say Hello to' 39 | } 40 | } 41 | resolve: (root, {name}) => `Hello, ${name}`; 42 | } 43 | } 44 | }); 45 | ``` 46 | 47 | ## Full Example 48 | 49 | ```js 50 | import { interfaceType, objectType, enumType, schemaFrom, listOf, notNull } from 'graphql-schema'; 51 | 52 | const episodeEnum = enumType('Episode', 53 | 'One of the films in the Star Wars Trilogy') 54 | .value('NEWHOPE', 4, 'Released in 1977.') 55 | .value('EMPIRE', 5, 'Released in 1980.') 56 | .value('JEDI', 6, 'Released in 1983.') 57 | .end(); 58 | 59 | const characterInterface = interfaceType('Character', 60 | 'A character in the Star Wars Trilogy') 61 | .field('id', notNull(GraphQLString), 'The id of the character.') 62 | .field('name', GraphQLString, 'The name of the character.') 63 | .field('friends', listOf(characterInterface), 64 | 'The friends of the character, or an empty list if they have none') 65 | .field('appearsIn', listOf(episodeEnum), 'Which movies they appear in.') 66 | .resolve((obj) => { 67 | if (starWarsData.Humans[obj.id] !== undefined) { 68 | return humanType; 69 | } 70 | if (starWarsData.Droids[obj.id] !== undefined) { 71 | return droidType; 72 | } 73 | return null; 74 | }) 75 | .end(); 76 | 77 | const humanType = objectType('Human', [characterInterface], 78 | 'A humanoid creature in the Star Wars universe.') 79 | .field('id', notNull(GraphQLString), 'The id of the human.') 80 | .field('name', GraphQLString, 'The name of the human.') 81 | .field('friends', listOf(characterInterface), 82 | 'The friends of the human, or an empty list if they have none', (human) => { 83 | return getFriends(human); 84 | }) 85 | .field('appearsIn', listOf(episodeEnum), 'Which movies they appear in.') 86 | .field('homePlanet', GraphQLString, 87 | 'The home planet of the human, or null if unknown.') 88 | .end(); 89 | 90 | const droidType = objectType('Droid', [characterInterface], 91 | 'A mechanical creature in the Star Wars universe.') 92 | .field('id', notNull(GraphQLString), 'The id of the droid.') 93 | .field('name', GraphQLString, 'The name of the droid.') 94 | .field('friends', listOf(characterInterface), 95 | 'The friends of the droid, or an empty list if they have none', (droid) => { 96 | return getFriends(droid); 97 | }) 98 | .field('appearsIn', listOf(episodeEnum), 'Which movies they appear in.') 99 | .field('primaryFunction', GraphQLString, 'The primary function of the droid.') 100 | .end(); 101 | 102 | const queryType = objectType('Query') 103 | .field('hero', characterInterface, () => artoo) 104 | .field('human', humanType) 105 | .arg('id', notNull(GraphQLString)) 106 | .resolve((root, {id}) => starWarsData.Humans[id]) 107 | .field('droid', droidType) 108 | .arg('id', notNull(GraphQLString)) 109 | .resolve((root, {id}) => starWarsData.Droids[id]) 110 | .end(); 111 | 112 | const mutationType = objectType('Mutation') 113 | .field('updateCharacterName', characterInterface) 114 | .arg('id', notNull(GraphQLString)) 115 | .arg('newName', notNull(GraphQLString)) 116 | .resolve((root, {id, newName}) => updateCharacterName(id, newName)) 117 | .end(); 118 | 119 | const starWarsSchema = schemaFrom(queryType, mutationType); 120 | ``` 121 | 122 | # Cyclic Types 123 | 124 | `graphql-schema` supports cyclic types. Instead of passing in a reference, just pass in a function instead: 125 | 126 | ```js 127 | const userType = objectType('User') 128 | .field('friends', () => listOf(userType)) 129 | .end(); 130 | ``` 131 | 132 | ## API 133 | 134 | ### enumType(name, description) 135 | 136 | Define a new `GraphQLEnumType` 137 | 138 | ##### .value(name, value, description) 139 | ##### .deprecated(deprecationReason) 140 | 141 | ### interfaceType(name, description) 142 | 143 | Define a new `GraphQLInterfaceType`. 144 | 145 | ##### .field(name, type, description) 146 | ##### .deprecated(deprecationReason) 147 | ##### .arg(name, type, defaultValue, description) 148 | ##### .resolve(fn) 149 | 150 | ### objectType(name, [interfaces], description) 151 | 152 | Define a new `GraphQLObjectType`. 153 | 154 | ##### .field(name, type, description) 155 | ##### .deprecated(deprecationReason) 156 | ##### .arg(name, type, defaultValue, description) 157 | ##### .resolve(fn) 158 | 159 | ## schemaFrom(queryRootType, mutationRootType) 160 | 161 | Define a new `GraphQLSchema` from the given root types. 162 | 163 | ## listOf(type) 164 | 165 | Define a new `GraphQLList(type)`. 166 | 167 | ## notNull(type) 168 | 169 | Define a new `GraphQLNonNull(type)`. 170 | 171 | # Thanks 172 | 173 | Thanks to [Florent Cailhol](https://github.com/ooflorent) for the chainable interface idea! -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-schema", 3 | "version": "0.5.1", 4 | "description": "Create GraphQL schemas with a fluent/chainable interface", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "scripts/build", 8 | "lint": "scripts/lint", 9 | "prepublish": "scripts/prepublish" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/devknoll/graphql-schema.git" 14 | }, 15 | "author": "Gerald Monaco (http://github.com/devknoll)", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/devknoll/graphql-schema/issues" 19 | }, 20 | "homepage": "https://github.com/devknoll/graphql-schema", 21 | "dependencies": { 22 | "graphql": "^0.1.3", 23 | "invariant": "^2.1.0" 24 | }, 25 | "devDependencies": { 26 | "babel": "^5.6.14", 27 | "babel-core": "^5.6.15", 28 | "babel-eslint": "^3.1.20", 29 | "eslint": "^0.24.0", 30 | "eslint-config-airbnb": "0.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | rm -rf lib 4 | `npm bin`/babel src --out-dir lib -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | `npm bin`/eslint src -------------------------------------------------------------------------------- /scripts/prepublish: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | sh scripts/lint 4 | sh scripts/build -------------------------------------------------------------------------------- /src/baseObject.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | 3 | function resolveFields(fields) { 4 | const fieldNames = Object.keys(fields); 5 | const fieldDefs = {}; 6 | 7 | for (const fieldName of fieldNames) { 8 | const field = fields[fieldName]; 9 | const { type } = field; 10 | 11 | fieldDefs[fieldName] = { 12 | ...field, 13 | args: resolveFields(field.args || {}), 14 | type: typeof type === 'function' ? type() : type 15 | }; 16 | } 17 | 18 | return fieldDefs; 19 | } 20 | 21 | export default class BaseObject { 22 | constructor(name, interfaces, description) { 23 | if (typeof description === 'undefined') { 24 | /* eslint-disable no-param-reassign */ 25 | description = interfaces; 26 | interfaces = undefined; 27 | /* eslint-enable no-param-reassign */ 28 | } 29 | 30 | this.name = name; 31 | this.description = description; 32 | this.interfaces = interfaces; 33 | 34 | this.__field = null; 35 | this.fields = {}; 36 | } 37 | 38 | __saveField() { 39 | if (this.__field) { 40 | this.fields[this.__field.name] = this.__field; 41 | this.__field = null; 42 | } 43 | } 44 | 45 | field(name, type, description, resolve) { 46 | if (typeof description === 'function') { 47 | /* eslint-disable no-param-reassign */ 48 | resolve = description; 49 | description = null; 50 | /* eslint-enable no-param-reassign */ 51 | } 52 | 53 | invariant( 54 | !this.fields[name], 55 | `field(...): '${name}' is already defined` 56 | ); 57 | 58 | invariant( 59 | type, 60 | `field(...): '${name}' has an undefined or null type. If you ` + 61 | `are trying to refer to '${this.name}' then you should use a function` 62 | ); 63 | 64 | this.__saveField(); 65 | 66 | this.__field = { 67 | name, 68 | type, 69 | description, 70 | resolve, 71 | args: {} 72 | }; 73 | 74 | return this; 75 | } 76 | 77 | arg(name, type, defaultValue, description) { 78 | if (!description) { 79 | /* eslint-disable no-param-reassign */ 80 | description = defaultValue; 81 | defaultValue = undefined; 82 | /* eslint-enable no-param-reassign */ 83 | } 84 | 85 | invariant( 86 | this.__field, 87 | `arg(...): '${name}' must appear under a field` 88 | ); 89 | 90 | invariant( 91 | !this.__field.args[name], 92 | `arg(...): '${name}' is already defined by ${this.__field.name}` 93 | ); 94 | 95 | this.__field.args[name] = { 96 | name, 97 | type, 98 | description, 99 | defaultValue 100 | }; 101 | 102 | return this; 103 | } 104 | 105 | deprecated(reason) { 106 | invariant( 107 | this.__field, 108 | `deprecated(...): Deprecations must appear under a field` 109 | ); 110 | 111 | this.__field.deprecationReason = reason; 112 | return this; 113 | } 114 | 115 | end() { 116 | this.__saveField(); 117 | 118 | const { name, description } = this; 119 | 120 | return { 121 | name, 122 | description, 123 | fields: () => resolveFields(this.fields) 124 | }; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/enumeration.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import { GraphQLEnumType } from 'graphql'; 3 | 4 | class Enumeration { 5 | constructor(name, description) { 6 | this.name = name; 7 | this.description = description; 8 | this.values = {}; 9 | this.lastValue = null; 10 | } 11 | 12 | value(name, value, description) { 13 | invariant( 14 | !this.values[name], 15 | `value(...): '${name}' is already defined` 16 | ); 17 | 18 | this.lastValue = this.values[name] = { 19 | value, 20 | description 21 | }; 22 | 23 | return this; 24 | } 25 | 26 | deprecated(deprecationReason) { 27 | invariant( 28 | this.lastValue, 29 | `deprecated(...): Deprecated must appear after a value` 30 | ); 31 | 32 | this.lastValue.deprecationReason = deprecationReason; 33 | return this; 34 | } 35 | 36 | end() { 37 | const { name, description, values } = this; 38 | 39 | return new GraphQLEnumType({ 40 | name, 41 | description, 42 | values 43 | }); 44 | } 45 | } 46 | 47 | export default function enumType(...args) { 48 | return new Enumeration(...args); 49 | } 50 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export interfaceType from './interface'; 2 | export objectType from './object'; 3 | export enumType from './enumeration'; 4 | export { schemaFrom, listOf, notNull } from './types'; 5 | -------------------------------------------------------------------------------- /src/interface.js: -------------------------------------------------------------------------------- 1 | import BaseObject from './baseObject'; 2 | import { GraphQLInterfaceType } from 'graphql'; 3 | 4 | class Interface extends BaseObject { 5 | resolve(resolve) { 6 | this.resolveType = resolve; 7 | return this; 8 | } 9 | 10 | end() { 11 | const { resolveType } = this; 12 | 13 | return new GraphQLInterfaceType({ 14 | ...super(), 15 | resolveType 16 | }); 17 | } 18 | } 19 | 20 | export default function interfaceType(...args) { 21 | return new Interface(...args); 22 | } 23 | -------------------------------------------------------------------------------- /src/object.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import BaseObject from './baseObject'; 3 | import { GraphQLObjectType } from 'graphql'; 4 | 5 | class Obj extends BaseObject { 6 | resolve(resolve) { 7 | invariant( 8 | this.__field, 9 | `resolve(...): Resolve must appear under a field` 10 | ); 11 | 12 | this.__field.resolve = resolve; 13 | return this; 14 | } 15 | 16 | isTypeOf(isTypeOf) { 17 | this.__field.isTypeOf = isTypeOf; 18 | return this; 19 | } 20 | 21 | end() { 22 | return new GraphQLObjectType(super()); 23 | } 24 | } 25 | 26 | export default function objectType(...args) { 27 | return new Obj(...args); 28 | } 29 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLList, 3 | GraphQLNonNull, 4 | GraphQLSchema 5 | } from 'graphql'; 6 | 7 | export function listOf(type) { 8 | return new GraphQLList(type); 9 | } 10 | 11 | export function notNull(type) { 12 | return new GraphQLNonNull(type); 13 | } 14 | 15 | export function schemaFrom(queryRootType, mutationRootType) { 16 | return new GraphQLSchema({ 17 | query: queryRootType, 18 | mutation: mutationRootType 19 | }); 20 | } 21 | --------------------------------------------------------------------------------