├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── spec ├── support └── jasmine.json └── tests-spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | package-lock.json 4 | .vscode 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Cardinal90 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Union Input Type 2 | Union Input Type for GraphQL-js 3 | 4 | ## Why 5 | I wanted to represent a group of an arbitrary number of related, but different items and be able to work with it through graphql. When I started to write mutations, I realised, that Interfaces and Unions are not allowed. I'm not the only one - discussion goes [here](https://github.com/facebook/graphql/issues/114) and [here](https://github.com/graphql/graphql-js/issues/207) for example. There is a chance, that something like this will be added to the core, but it is not certain. 6 | 7 | It would be nice to have some syntax support to specify the type to be validated against. Otherwise the only way to have clean queries without some workarounds is to let developers manually traverse AST, which seems like a too low level detail to expose. 8 | 9 | ## Installation 10 | ``` 11 | npm install graphql-union-input-type 12 | ``` 13 | ## Usage 14 | ``` 15 | var UnionInputType = require('graphql-union-input-type'); 16 | 17 | UnionInputType(options); 18 | ``` 19 | ### Parameters 20 | #### Options(object) 21 | - `name`(`string`) 22 | Name for the UnionType itself. It has to be unique in your schema and it can be used to mutate a union nested inside of another union. 23 | - `inputTypes`(`array|object`): 24 | - either array of `GraphQLInputObjectType` objects. Their `name` property will be referenced in mutations. 25 | - or object with `name:GraphQLInputObjectType` pairs. This `name` will be used instead. 26 | 27 | Objects returned by `UnionInputType` may also be used. This argument will be ignored if `resolveType` function is provided. 28 | - `typeKey`(`string)`: a key in a mutation argument object containing the `name` of a type to validate against. If omitted, another [strategy](#now-you-can-call-mutations-on-it) will be used instead. 29 | - `resolveType`(`function(name)` -> `GraphQLInputObjectType|null`): takes a name found in mutation argument and returns corresponding 30 | `GraphQLInputObjectType` or an object returned by `UnionInputType`. This strategy is not restricted by a predefined set of input types. It behaves as an interface in that `UnionInputType` does not know what input types implement it. If omitted, `inputTypes` is used. 31 | - `resolveTypeFromAst`(`function(ast)` -> `GraphQLInputObjectType|null`): provide this, if you absolutely do not want to explicitly specify the type in you mutation. The function will be called with full AST, which you can traverse manually to identify the input type and return it. 32 | - `resolveTypeFromValue`(`function(value)` -> `GraphQLInputObjectType|null`): same as `resolveTypeFromAst`, but for the case, where you use variables for your input types. The function is called with a variable value, and you need to return the input type 33 | 34 | ### Examples 35 | #### Create normal input types 36 | ```js 37 | var JediInputType = new GraphQLInputObjectType({ 38 | name: 'jedi', 39 | fields: function() { 40 | return { 41 | side: { 42 | type: GraphQLString 43 | }, 44 | name: { 45 | type: GraphQLString 46 | }, 47 | } 48 | } 49 | }); 50 | 51 | var SithInputType = new GraphQLInputObjectType({ 52 | name: 'sith', 53 | fields: function() { 54 | return { 55 | side: { 56 | type: GraphQLString 57 | }, 58 | name: { 59 | type: GraphQLString 60 | }, 61 | doubleBlade: { 62 | type: GraphQLBoolean 63 | } 64 | }; 65 | } 66 | }); 67 | ``` 68 | #### Combine them together 69 | ```js 70 | var HeroInputType = UnionInputType({ 71 | name: 'hero', 72 | inputTypes: [JediInputType, SithInputType], //an object can be used instead to query by names other than defined in these types 73 | typeKey: 'side' //optional 74 | }); 75 | ``` 76 | OR 77 | ```js 78 | var HeroInputType = UnionInputType({ 79 | name: 'hero', 80 | resolveType: function resolveType(name) { 81 | if (name === 'jedi') { 82 | return JediInputType; 83 | } else { 84 | return SithInputType; 85 | } 86 | }, 87 | typeKey: 'side' //optional 88 | }); 89 | ``` 90 | OR 91 | ```js 92 | var HeroInputType = UnionInputType({ 93 | name: 'hero', 94 | resolveTypeFromAst: resolveTypeFromAst(ast) { 95 | if (ast.fields[2] && ast.fields[2].name.value === 'doubleBlade') { 96 | return SithInputType; 97 | } else { 98 | return JediInputType; 99 | } 100 | } 101 | }); 102 | ``` 103 | Note, that in the last case as it is written `doubleBlade` field on `SithInputType` needs to be wrapped with `GraphQLNonNull` for the result to be accurate. Also you could loop through `ast.fields` instead of checking just position 2. For more information just dump AST into console and see what it contains. 104 | #### Create schema 105 | ```js 106 | var MutationType = new GraphQLObjectType({ 107 | name: 'mutation', 108 | fields: function() { 109 | return { 110 | hero: { 111 | type: GraphQLBoolean, //this is output type, normally it will correspond to some HeroType of type GraphQLUnionType or GraphQLInterfaceType 112 | args: { 113 | input: { 114 | type: HeroInputType //here is our Union 115 | } 116 | }, 117 | resolve: function(root, args) { 118 | return true; 119 | } 120 | } 121 | }; 122 | } 123 | }); 124 | 125 | var schema = new GraphQLSchema({ 126 | query: someQueryType, 127 | mutation: MutationType 128 | }); 129 | ``` 130 | #### Now you can call mutations on it 131 | ```js 132 | var query = `mutation { 133 | hero(input: {{kind: "sith", name: "Maul", saberColor: "red", doubleBlade: true}) 134 | }`; 135 | 136 | graphql(schema, query).then(function(res) { 137 | expect(res.data).toBeDefined(); 138 | done(); 139 | }); 140 | 141 | query = `mutation { 142 | hero(input: {{kind: "jedi", name: "Maul", saberColor: "red", doubleBlade: true}) 143 | }`; 144 | 145 | graphql(schema, query).then(function(res) { 146 | expect(res.errors).toBeDefined(); 147 | done(); 148 | }); 149 | ``` 150 | The second query will fail to validate, as there is no `doubleBlade` field on `jedi` type. Of course you can also set the type of your mutation field to something other than `GraphQLBoolean` and specify the desired return schema. 151 | 152 | You can also omit `typeKey` property and write the same mutation this way: 153 | ```js 154 | var query = `mutation { 155 | hero(input: {_type_: "sith", _value_: {name: "Maul", saberColor: "red", doubleBlade: true}}) 156 | }`; 157 | ``` 158 | Your `resolve` function for mutation arguments will get this `input` argument as is. 159 | 160 | Finally if you provided `resolveTypeFromAst` function, you may query with an input argument as it is: 161 | ```js 162 | var query = `mutation { 163 | hero(input: {name: "Maul", saberColor: "red", doubleBlade: true}) 164 | }`; 165 | ``` 166 | #### Using variables (since 0.3.0) 167 | If you want to use variables, you may do it as you normally would. Please note, that the name of an input variable is a `name` property passed to the `UnionInputType` function. In terms of this readme it will be `hero`: 168 | ```js 169 | var query = `mutation($hero: hero!) { 170 | hero(input: $hero) 171 | }`; 172 | ``` 173 | There is a function `resolveTypeFromValue`, which is similar to `resolveTypeFromAst`, but used for a variable value. 174 | 175 | ## Capabilities 176 | You can use these unions as mutation arguments, nest them inside any input types and even create unions of unions. The only small problem is that objects returned by `UnionInputType` are really `GraphQLScalarType`, so I had to allow scalars to be passed to the function. 177 | 178 | You may use variables since 0.3.0. [This issue](https://github.com/Cardinal90/graphql-union-input-type/issues/2) might have some relevant information. 179 | 180 | ## Tests 181 | Test are written for `jasmine`. I use `nodemon` to run them. You can find more examples in the spec file. The last test is not written formally, I just used it to play around with nested structures. 182 | 183 | Since 0.3.0 just use `npm run test` 184 | 185 | `graphql-js` 0.10.1 breaks tests. Seems to be a bug. [Reported here](https://github.com/graphql/graphql-js/issues/910) 186 | 187 | ## Contributing 188 | Feel free to make suggestions or pull requests. 189 | 190 | ## License 191 | (The MIT License) 192 | 193 | Copyright (c) 2016 Sergei Petrov 194 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var GraphQLScalarType = require('graphql').GraphQLScalarType; 2 | var GraphQLInputObjectType = require('graphql').GraphQLInputObjectType; 3 | var GraphQLString = require('graphql').GraphQLString; 4 | 5 | var isValidLiteralValue = require('graphql').isValidLiteralValue; 6 | var coerceValue = require('graphql').coerceValue; 7 | var valueFromAST = require('graphql').valueFromAST; 8 | 9 | var GraphQLError = require('graphql').GraphQLError; 10 | 11 | function helper(name, type) { 12 | "use strict"; 13 | return (new GraphQLInputObjectType({ 14 | name: name, 15 | fields: function () { 16 | return { 17 | _type_: { 18 | type: GraphQLString 19 | }, 20 | _value_: { 21 | type: type 22 | } 23 | }; 24 | } 25 | })); 26 | } 27 | 28 | 29 | /** 30 | * UnionInputType - Union Input Type for GraphQL 31 | * 32 | * @param {object} options see below 33 | * @return {any} returns validated and parsed value 34 | */ 35 | module.exports = function UnionInputType(options) { 36 | "use strict"; 37 | 38 | /** 39 | * @param {array} options.name Name for the union type. Must be unique in your schema. Has to be used in queries to nested unions. 40 | */ 41 | var name = options.name; 42 | 43 | /** 44 | * @param {array|object} options.inputTypes Optional. Either array of GraphQLInputObjectType objects or UnionInputTypes (which are Scalars really) 45 | * or object with {name:GraphQLInputObjectType} pairs. 46 | * Will be ignored if resolveType is provided. 47 | */ 48 | var referenceTypes = options.inputTypes; 49 | 50 | /** 51 | * @param {string} options.typeKey Optional. If provided, is used as a key containing the type name. If not, the query argument must 52 | * contain _type_ and _value_ parameteres in this particular order 53 | */ 54 | var typeKey = options.typeKey; 55 | 56 | /** 57 | * @param {function} options.resolveType Optional. If provided, is called with a key name and must return corresponding GraphQLInputObjectType or null 58 | */ 59 | var resolveType = options.resolveType; 60 | 61 | /** 62 | * @param {function} options.resolveTypeFromAst Optional. If provided, is called with full AST for the input argument and must return 63 | * corresponding GraphQLInputObjectType or null 64 | */ 65 | var resolveTypeFromAst = options.resolveTypeFromAst; 66 | 67 | /** 68 | * @param {function} options.resolveTypeFromValue Optional. If provided, is called with a variable value and must return 69 | * corresponding GraphQLInputObjectType or null 70 | */ 71 | var resolveTypeFromValue = options.resolveTypeFromValue; 72 | 73 | if (!resolveType && !resolveTypeFromAst) { 74 | if (Array.isArray(referenceTypes)) { 75 | referenceTypes = referenceTypes.reduce(function (acc, refType) { 76 | if (!(refType instanceof GraphQLInputObjectType || refType instanceof GraphQLScalarType)) { 77 | throw (new GraphQLError(name + '(UnionInputType): all inputTypes must be of GraphQLInputObjectType or GraphQLScalarType(created by UnionInputType function)')); 78 | } 79 | acc[refType.name] = (typeKey ? refType : helper(refType.name, refType)); 80 | return acc; 81 | }, {}); 82 | } else if (referenceTypes !== null && typeof referenceTypes === 'object') { 83 | Object.keys(referenceTypes).forEach(function (key) { 84 | if (!(referenceTypes[key] instanceof GraphQLInputObjectType || referenceTypes[key] instanceof GraphQLScalarType)) { 85 | throw (new GraphQLError(name + '(UnionInputType): all inputTypes must be of GraphQLInputObjectType or GraphQLScalarType(created by UnionInputType function')); 86 | } 87 | referenceTypes[key] = typeKey ? referenceTypes[key] : helper(key, referenceTypes[key]); 88 | }); 89 | } 90 | } 91 | 92 | var union = (new GraphQLScalarType({ 93 | name: name, 94 | serialize: function (value) { 95 | return value; 96 | }, 97 | parseValue: function (value) { 98 | var type, inputType, ast; 99 | if (typeof resolveTypeFromValue === 'function') { 100 | inputType = resolveTypeFromValue(value); 101 | } else { 102 | if (typeKey) { 103 | if (value[typeKey]) { 104 | type = value[typeKey]; 105 | } else { 106 | throw new GraphQLError(name + '(UnionInputType): Expected an object with "' + typeKey + '" property'); 107 | } 108 | } else if (value._type_ && value._value_) { 109 | type = value._type_; 110 | } 111 | else { 112 | throw new GraphQLError(name + '(UnionInputType): Expected an object with _type_ and _value_ properties in this order'); 113 | } 114 | if (typeof resolveType === 'function') { 115 | inputType = resolveType(type); 116 | if (!typeKey) { 117 | inputType = helper(type, inputType); 118 | } 119 | } else { 120 | inputType = referenceTypes[type]; 121 | } 122 | } 123 | const errors = coerceValue(value, inputType).errors; 124 | 125 | if (!errors) { 126 | return value; 127 | } else { 128 | const errorString = errors.map((error) => { 129 | return "\n" + error.message; 130 | }).join(''); 131 | throw new GraphQLError(errorString); 132 | } 133 | }, 134 | parseLiteral: function (ast) { 135 | var type, inputType; 136 | if (typeof resolveTypeFromAst === 'function') { 137 | inputType = resolveTypeFromAst(ast); 138 | } else { 139 | if (typeKey) { 140 | try { 141 | for (var i = 0; i < ast.fields.length; i++) { 142 | if (ast.fields[i].name.value === typeKey) { 143 | type = ast.fields[i].value.value; 144 | break; 145 | } 146 | } 147 | if (!type) { 148 | throw (new Error); 149 | } 150 | } catch (err) { 151 | throw new GraphQLError(name + '(UnionInputType): Expected an object with "' + typeKey + '" property'); 152 | } 153 | } else { 154 | try { 155 | if (ast.fields[0].name.value === '_type_' && ast.fields[1].name.value === '_value_') { 156 | type = ast.fields[0].value.value; 157 | } else { 158 | throw (new Error); 159 | } 160 | } catch (err) { 161 | throw new GraphQLError(name + '(UnionInputType): Expected an object with _type_ and _value_ properties in this order'); 162 | } 163 | } 164 | if (typeof resolveType === 'function') { 165 | inputType = resolveType(type); 166 | if (!typeKey) { 167 | inputType = helper(type, inputType); 168 | } 169 | } else { 170 | inputType = referenceTypes[type]; 171 | } 172 | } 173 | if (isValidLiteralValue(inputType, ast).length == 0) { 174 | return valueFromAST(ast, inputType); 175 | } else { 176 | throw new GraphQLError('expected type ' + type + ', found ' + ast.loc.source.body.substring(ast.loc.start, ast.loc.end)); 177 | } 178 | } 179 | })); 180 | 181 | return union; 182 | }; 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-union-input-type", 3 | "version": "0.4.0", 4 | "description": "Union Input Type for graphql-js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jasmine", 8 | "test:watch": "nodemon --exec jasmine" 9 | }, 10 | "author": "Sergei Petrov", 11 | "license": "MIT", 12 | "peerDependencies": { 13 | "graphql": "<0.13" 14 | }, 15 | "keywords": [ 16 | "graphql", 17 | "union", 18 | "interface", 19 | "input", 20 | "type" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/Cardinal90/graphql-union-input-type.git" 25 | }, 26 | "homepage": "https://github.com/Cardinal90/graphql-union-input-type", 27 | "devDependencies": { 28 | "jasmine": "^2.6.0", 29 | "nodemon": "^1.11.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /spec/tests-spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var graphql = require('graphql').graphql; 4 | var GraphQLSchema = require('graphql').GraphQLSchema; 5 | var GraphQLObjectType = require('graphql').GraphQLObjectType; 6 | var GraphQLInputObjectType = require('graphql').GraphQLInputObjectType; 7 | var GraphQLList = require('graphql').GraphQLList; 8 | var GraphQLString = require('graphql').GraphQLString; 9 | var GraphQLInt = require('graphql').GraphQLInt; 10 | var GraphQLNonNull = require('graphql').GraphQLNonNull; 11 | var GraphQLBoolean = require('graphql').GraphQLBoolean; 12 | 13 | var UnionInputType = require('../index.js'); 14 | 15 | var JediInputType = new GraphQLInputObjectType({ 16 | name: 'jedi', 17 | fields: function () { 18 | return { 19 | side: { 20 | type: GraphQLString 21 | }, 22 | name: { 23 | type: GraphQLString 24 | }, 25 | saberColor: { 26 | type: GraphQLString 27 | } 28 | }; 29 | } 30 | }); 31 | 32 | var SithInputType = new GraphQLInputObjectType({ 33 | name: 'sith', 34 | fields: function () { 35 | return { 36 | side: { 37 | type: GraphQLString 38 | }, 39 | name: { 40 | type: GraphQLString 41 | }, 42 | saberColor: { 43 | type: GraphQLString 44 | }, 45 | doubleBlade: { 46 | type: GraphQLBoolean 47 | } 48 | }; 49 | } 50 | }); 51 | 52 | 53 | function generateSchema(options) { 54 | var HeroInputType = UnionInputType(options); 55 | 56 | var MutationType = new GraphQLObjectType({ 57 | name: 'mutation', 58 | fields: function () { 59 | return { 60 | hero: { 61 | type: GraphQLBoolean, 62 | args: { 63 | input: { 64 | type: HeroInputType 65 | } 66 | }, 67 | resolve: function (root, args) { 68 | return true; 69 | } 70 | } 71 | }; 72 | } 73 | }); 74 | 75 | var schema = new GraphQLSchema({ 76 | query: (new GraphQLObjectType({ 77 | name: 'query', 78 | fields: { 79 | id: { 80 | type: GraphQLInt 81 | } 82 | } 83 | })), 84 | mutation: MutationType 85 | }); 86 | 87 | return schema; 88 | } 89 | 90 | 91 | describe('_type_/_value_ query format:', function () { 92 | 93 | var schema = generateSchema({ 94 | name: 'heroUnion', 95 | inputTypes: [JediInputType, SithInputType] 96 | }); 97 | 98 | describe('Throws on incorrect query format', function () { 99 | it('Expects both _type_ and _value_ parameters', function (done) { 100 | var query = ` 101 | mutation { 102 | hero(input: {_value_: {name: "Maul", saberColor: "red", doubleBlade: true}}) 103 | } 104 | `; 105 | 106 | graphql(schema, query).then(function (res) { 107 | // console.log(res); 108 | expect(res.errors).toBeDefined(); 109 | done(); 110 | }); 111 | }); 112 | 113 | it('Does the same with a variable', function (done) { 114 | var query = ` 115 | mutation($hero: heroUnion!) { 116 | hero(input: $hero) 117 | } 118 | `; 119 | 120 | graphql(schema, query, {}, {}, { 121 | hero: { _type_: "sith", _value_: { name: "Maul", saberColor: "red", doubleBlade: true } } 122 | }).then(function (res) { 123 | // console.log(res); 124 | expect(res.data).toBeDefined(); 125 | done(); 126 | }); 127 | }); 128 | 129 | it('Expects parameters in correct order', function (done) { 130 | var query = ` 131 | mutation { 132 | hero(input: {_value_: {name: "Maul", saberColor: "red", doubleBlade: true}, _type_: "sith"}) 133 | } 134 | `; 135 | 136 | graphql(schema, query).then(function (res) { 137 | // console.log(res); 138 | expect(res.errors).toBeDefined(); 139 | done(); 140 | }); 141 | }); 142 | }) 143 | 144 | describe('Predefined input types:', function () { 145 | var schema = generateSchema({ 146 | name: 'heroUnion', 147 | inputTypes: [JediInputType, SithInputType] 148 | }); 149 | 150 | describe('Correctly validates against the type requested in the mutation:', function () { 151 | 152 | it('Validates Maul as a sith', function (done) { 153 | var query = ` 154 | mutation { 155 | hero(input: {_type_: "sith", _value_: {name: "Maul", saberColor: "red", doubleBlade: true}}) 156 | } 157 | `; 158 | 159 | graphql(schema, query).then(function (res) { 160 | // console.log(res); 161 | expect(res.data).toBeDefined(); 162 | done(); 163 | }); 164 | }); 165 | 166 | it('Throws, when trying to assign that doubleBlade to a jedi', function (done) { 167 | var query = ` 168 | mutation { 169 | hero(input: {_type_: "jedi", _value_: {name: "Maul", saberColor: "red", doubleBlade: true}}) 170 | } 171 | `; 172 | 173 | graphql(schema, query).then(function (res) { 174 | // console.log(res); 175 | expect(res.errors).toBeDefined(); 176 | done(); 177 | }); 178 | }); 179 | 180 | }); 181 | 182 | }); 183 | 184 | describe('Function to resolve types at runtime:', function () { 185 | 186 | function resolveType(type) { 187 | if (type === 'jedi') { 188 | return JediInputType; 189 | } else { 190 | return SithInputType; 191 | } 192 | } 193 | var schema = generateSchema({ 194 | name: 'heroUnion', 195 | resolveType: resolveType 196 | }); 197 | 198 | describe('Correctly validates against the type requested in the mutation:', function () { 199 | 200 | it('Validates Maul as a sith', function (done) { 201 | var query = ` 202 | mutation { 203 | hero(input: {_type_: "sith", _value_: {name: "Maul", saberColor: "red", doubleBlade: true}}) 204 | } 205 | `; 206 | 207 | graphql(schema, query).then(function (res) { 208 | // console.log(res); 209 | expect(res.data).toBeDefined(); 210 | done(); 211 | }); 212 | }); 213 | 214 | it('Throws, when trying to assign that doubleBlade to a jedi', function (done) { 215 | var query = ` 216 | mutation { 217 | hero(input: {_type_: "jedi", _value_: {name: "Maul", saberColor: "red", doubleBlade: true}}) 218 | } 219 | `; 220 | 221 | graphql(schema, query).then(function (res) { 222 | // console.log(res); 223 | expect(res.errors).toBeDefined(); 224 | done(); 225 | }); 226 | }); 227 | }); 228 | }); 229 | }); 230 | 231 | 232 | describe('Query format with provided typeKey:', function () { 233 | 234 | 235 | var schema = generateSchema({ 236 | name: 'heroUnion', 237 | inputTypes: [JediInputType, SithInputType], 238 | typeKey: 'side' 239 | }); 240 | 241 | describe('Throws on incorrect query format', function () { 242 | it('Expects to find the provided typeKey in object', function (done) { 243 | var query = ` 244 | mutation { 245 | hero(input: {name: "Maul", saberColor: "red", doubleBlade: true}) 246 | } 247 | `; 248 | 249 | graphql(schema, query).then(function (res) { 250 | // console.log(res); 251 | expect(res.errors).toBeDefined(); 252 | done(); 253 | }); 254 | }); 255 | 256 | it('Does not accept previous format', function (done) { 257 | var query = ` 258 | mutation { 259 | hero(input: {_type_: "jedi", _value_: {name: "Maul", saberColor: "red", doubleBlade: true}}) 260 | } 261 | `; 262 | 263 | graphql(schema, query).then(function (res) { 264 | // console.log(res); 265 | expect(res.errors).toBeDefined(); 266 | done(); 267 | }); 268 | }); 269 | }); 270 | 271 | describe('Predefined input types (with object format for inputTypes this time)', function () { 272 | 273 | var schema = generateSchema({ 274 | name: 'heroUnion', 275 | inputTypes: { 276 | JEDI: JediInputType, 277 | SITH: SithInputType 278 | }, 279 | typeKey: 'side' 280 | }); 281 | 282 | describe('Correctly validates against the type requested in the mutation:', function () { 283 | 284 | it('Validates Maul as a sith', function (done) { 285 | var query = ` 286 | mutation { 287 | hero(input: {name: "Maul", saberColor: "red", doubleBlade: true, side: "SITH"}) 288 | } 289 | `; 290 | 291 | graphql(schema, query).then(function (res) { 292 | // console.log(res); 293 | expect(res.data).toBeDefined(); 294 | done(); 295 | }); 296 | }); 297 | 298 | it('Does the same with a variable', function (done) { 299 | var query = ` 300 | mutation($hero: heroUnion!) { 301 | hero(input: $hero) 302 | } 303 | `; 304 | 305 | graphql(schema, query, {}, {}, { 306 | hero: { name: "Maul", saberColor: "red", doubleBlade: true, side: "SITH" } 307 | }).then(function (res) { 308 | // console.log(res); 309 | expect(res.data).toBeDefined(); 310 | done(); 311 | }); 312 | }); 313 | 314 | it('Throws, when trying to assign that doubleBlade to a jedi', function (done) { 315 | var query = ` 316 | mutation { 317 | hero(input: {name: "Maul", saberColor: "red", doubleBlade: true, side: "JEDI"}) 318 | } 319 | `; 320 | 321 | graphql(schema, query).then(function (res) { 322 | // console.log(res); 323 | expect(res.errors).toBeDefined(); 324 | done(); 325 | }); 326 | }); 327 | 328 | }); 329 | 330 | }); 331 | 332 | describe('Function to resolve types from names at runtime:', function () { 333 | 334 | function resolveType(type) { 335 | if (type === 'jedi') { 336 | return JediInputType; 337 | } else { 338 | return SithInputType; 339 | } 340 | } 341 | var schema = generateSchema({ 342 | name: 'heroUnion', 343 | resolveType: resolveType, 344 | typeKey: 'side' 345 | }); 346 | 347 | describe('Correctly validates against the type requested in the mutation:', function () { 348 | 349 | it('Validates Maul as a sith', function (done) { 350 | var query = ` 351 | mutation { 352 | hero(input: {side: "sith", name: "Maul", saberColor: "red", doubleBlade: true}) 353 | } 354 | `; 355 | 356 | graphql(schema, query).then(function (res) { 357 | // console.log(res); 358 | expect(res.data).toBeDefined(); 359 | done(); 360 | }); 361 | }); 362 | 363 | it('Throws, when trying to assign that doubleBlade to a jedi', function (done) { 364 | var query = ` 365 | mutation { 366 | hero(input: {side: "jedi", name: "Maul", saberColor: "red", doubleBlade: true}) 367 | } 368 | `; 369 | 370 | graphql(schema, query).then(function (res) { 371 | // console.log(res); 372 | expect(res.errors).toBeDefined(); 373 | done(); 374 | }); 375 | }); 376 | 377 | it('Does the same with a variable', function (done) { 378 | var query = ` 379 | mutation($hero: heroUnion!) { 380 | hero(input: $hero) 381 | } 382 | `; 383 | 384 | graphql(schema, query, {}, {}, { 385 | hero: { side: "jedi", name: "Maul", saberColor: "red", doubleBlade: true } 386 | }).then(function (res) { 387 | // console.log(res); 388 | expect(res.errors).toBeDefined(); 389 | done(); 390 | }); 391 | }); 392 | }); 393 | }); 394 | }); 395 | 396 | 397 | describe('Function to resolve types from AST:', function () { 398 | 399 | function resolveTypeFromAst(ast) { 400 | if (ast.fields[2] && ast.fields[2].name.value === 'doubleBlade') { 401 | return SithInputType; 402 | } else { 403 | return JediInputType; 404 | } 405 | } 406 | var schema = generateSchema({ 407 | name: 'heroUnion', 408 | resolveTypeFromAst: resolveTypeFromAst 409 | }); 410 | 411 | describe('Correctly validates against the type returned from function:', function () { 412 | 413 | it('Validates Maul as a sith', function (done) { 414 | var query = ` 415 | mutation { 416 | hero(input: {name: "Maul", saberColor: "red", doubleBlade: true}) 417 | } 418 | `; 419 | 420 | graphql(schema, query).then(function (res) { 421 | // console.log(res); 422 | expect(res.data).toBeDefined(); 423 | done(); 424 | }); 425 | }); 426 | }); 427 | }); 428 | 429 | 430 | describe('Function to resolve types from value (for variables):', function () { 431 | 432 | function resolveTypeFromValue(value) { 433 | if (value.type === 'jedi') { 434 | return JediInputType; 435 | } else { 436 | return SithInputType; 437 | } 438 | } 439 | var schema = generateSchema({ 440 | name: 'heroUnion', 441 | resolveTypeFromValue: resolveTypeFromValue 442 | }); 443 | 444 | describe('Correctly validates against the type returned from function:', function () { 445 | 446 | it('Does not validate a jedi with a doubleblade', function (done) { 447 | var query = ` 448 | mutation($hero: heroUnion!) { 449 | hero(input: $hero) 450 | } 451 | `; 452 | 453 | graphql(schema, query, {}, {}, { 454 | hero: { type: 'jedi', name: "Maul", saberColor: "red", doubleBlade: true } 455 | }).then(function (res) { 456 | // console.log(res); 457 | expect(res.errors).toBeDefined(); 458 | done(); 459 | }); 460 | }); 461 | }); 462 | }); 463 | 464 | 465 | describe('A more complex test to play around with to show that nested arguments and unions of unions also work', function () { 466 | 467 | it('', function (done) { 468 | 469 | JediInputType = new GraphQLInputObjectType({ 470 | name: 'jedi', 471 | fields: function () { 472 | return { 473 | side: { 474 | type: GraphQLString 475 | }, 476 | name: { 477 | type: GraphQLString 478 | }, 479 | saberColor: { 480 | type: GraphQLString 481 | }, 482 | friends: { 483 | type: new GraphQLList(UnionInputType({ 484 | name: 'jediUnion', 485 | inputTypes: [JediInputType, SithInputType] 486 | })) 487 | } 488 | } 489 | } 490 | }); 491 | 492 | SithInputType = new GraphQLInputObjectType({ 493 | name: 'sith', 494 | fields: function () { 495 | return { 496 | side: { 497 | type: GraphQLString 498 | }, 499 | name: { 500 | type: GraphQLString 501 | }, 502 | saberColor: { 503 | type: GraphQLString 504 | }, 505 | doubleBlade: { 506 | type: GraphQLBoolean 507 | }, 508 | friends: { 509 | type: new GraphQLNonNull(UnionInputType({ 510 | name: 'sithUnion', 511 | inputTypes: [UnionInputType({ 512 | name: 'qqq', 513 | inputTypes: [JediInputType] 514 | }), UnionInputType({ 515 | name: 'www', 516 | inputTypes: [JediInputType] 517 | })] 518 | })) 519 | } 520 | } 521 | } 522 | }); 523 | 524 | var schema = generateSchema({ 525 | name: 'heroUnion', 526 | inputTypes: [JediInputType, SithInputType] 527 | }); 528 | 529 | var query = ` 530 | mutation { 531 | hero(input: 532 | { 533 | _type_: "jedi", 534 | _value_: { 535 | name: "Qui-Gon", 536 | saberColor: "green", 537 | friends: [ 538 | { 539 | _type_: "jedi", 540 | _value_:{name: "anakin"} 541 | }, 542 | { 543 | _type_: "sith", 544 | _value_:{ 545 | name: "anakin", 546 | doubleBlade: true, 547 | friends :{ 548 | _type_ :"qqq", 549 | _value_: { 550 | _type_: "jedi", 551 | _value_: { 552 | name: "Yoda" 553 | } 554 | } 555 | } 556 | } 557 | } 558 | ] 559 | } 560 | }) 561 | } 562 | `; 563 | 564 | var query2 = ` 565 | mutation { 566 | hero(input: {_type_: "jedi", _value_: {name: "Maul", saberColor: "red", friends: [{_type_: "jedi", _value_:{name: "anakin"}}, {_type_: "jedi", _value_:{name: "anakin", doubleBlade: true}}]}}) 567 | } 568 | `; 569 | 570 | var query3 = ` 571 | mutation { 572 | hero(input: {_type_: "sith", _value_: {name: "Maul", saberColor: "red"}}) 573 | } 574 | `; 575 | 576 | graphql(schema, query).then(function (res) { 577 | // console.log(res); 578 | expect(res.data).toBeDefined(); 579 | }) 580 | .then(function () { 581 | graphql(schema, query2).then(function (res) { 582 | // console.log(res); 583 | expect(res.errors).toBeDefined(); 584 | }) 585 | .then(function () { 586 | graphql(schema, query3).then(function (res) { 587 | // console.log(res); 588 | expect(res.errors).toBeDefined(); 589 | done(); 590 | }); 591 | }); 592 | }); 593 | }); 594 | }); 595 | 596 | 597 | 598 | 599 | // var JediType = new GraphQLObjectType({ 600 | // name: 'jedi', 601 | // fields: function() { 602 | // return { 603 | // side: { 604 | // type: GraphQLString 605 | // }, 606 | // name: { 607 | // type: GraphQLString 608 | // }, 609 | // saberColor: { 610 | // type: GraphQLString 611 | // } 612 | // } 613 | // } 614 | // }); 615 | // 616 | // var SithType = new GraphQLObjectType({ 617 | // name: 'sith', 618 | // fields: function() { 619 | // return { 620 | // side: { 621 | // type: GraphQLString 622 | // }, 623 | // name: { 624 | // type: GraphQLString 625 | // }, 626 | // saberColor: { 627 | // type: GraphQLString 628 | // }, 629 | // doubleBlade: { 630 | // type: GraphQLBoolean 631 | // } 632 | // } 633 | // } 634 | // }); 635 | // 636 | // var HeroType = new GraphQLUnionType({ 637 | // name: 'hero', 638 | // types: [SithType, JediType], 639 | // resolveType: function(hero) { 640 | // if (hero.side == 'jedi') { 641 | // return JediType; 642 | // } else { 643 | // return SithType; 644 | // } 645 | // } 646 | // }); 647 | // 648 | // var QueryType = new GraphQLObjectType({ 649 | // name: 'query', 650 | // fields: function() { 651 | // return { 652 | // hero: { 653 | // type: HeroType, 654 | // args: { 655 | // id: { 656 | // type: GraphQLInt 657 | // } 658 | // }, 659 | // resolve: function(root, args) { 660 | // 661 | // return heroes[args.id] 662 | // } 663 | // } 664 | // } 665 | // } 666 | // }); 667 | --------------------------------------------------------------------------------