├── .editorconfig ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── example └── .gitkeep ├── index.js ├── package.json ├── src ├── orm-loader.js └── utils.js └── test ├── index.js ├── support ├── db.js ├── mock-models.js ├── mock-translator.js ├── test-schema-relay.js └── test-schema.js └── unit └── orm-loader.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "es6": true, 5 | "node": true 6 | }, 7 | "ecmaFeatures": { 8 | "destructuring": true 9 | }, 10 | "rules": { 11 | "strict": 0, 12 | "no-console": 0, 13 | "no-unused-vars": 1, 14 | "no-var": 1, 15 | "prefer-const": 1, 16 | "no-trailing-spaces": 1, 17 | "quotes": [2, "single"], 18 | "comma-dangle": [2, "never"], 19 | "indent": [2, 2, { "SwitchCase": 2 }], 20 | "semi": [2, "always"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Lorenzo Ruiz 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraysQL ORM Loader # 2 | 3 | GraysQL ORM Loader is an extension for [GraysQL](https://github.com/larsbs/graysql) that transforms your existent ORM models/schema into a GraphQL Schema. In order to do the transformation, this extension needs an ORM translator. The 4 | translator will take care of the implementation details of the ORM and will get the data that ORM Loader needs. 5 | 6 | If the ORM that you use doesn't have a translator, you can build it yourself using the [Translators API](). 7 | 8 | ## Installation ## 9 | 10 | Install this package from npm using the following command: 11 | 12 | ```bash 13 | $ npm install graysql-ext-orm-loader 14 | ``` 15 | 16 | Note that this is an extension for GraysQL, so you must have it installed in order for this to work. 17 | 18 | ## Translators ## 19 | 20 | * [graysql-orm-loader-waterline](https://github.com/larsbs/graysql-orm-loader-waterline): A translator to get the schema from Waterline models. 21 | 22 | ## Example ## 23 | 24 | Here is a simple example using [Sails.js](http://sailsjs.org/) with [Waterline](https://github.com/balderdashy/waterline) as the ORM. 25 | 26 | ```javascript 27 | const GraysQL = require('graysql'); 28 | const ORMLoader = require('graysql-orm-loader'); 29 | const WaterlineTranslator = require('graysql-orm-loader-waterline'); // We'll use the waterline translator 30 | 31 | const GQL = new GraysQL(); 32 | 33 | // Add the extension to GQL 34 | GQL.use(ORMLoader); 35 | 36 | // Instantiate the translator and pass the sails models. 37 | GQL.loadFromORM(new WaterlineTranslator(sails.models)); 38 | 39 | // Generate the schema 40 | const Schema = GQL.generateSchema(); 41 | console.log(Schema); 42 | ``` 43 | 44 | ## Overview ## 45 | 46 | In order to load a Schema from the ORM you will need a translator. Then, this 47 | library will use that translator to retrieve the necessary information from 48 | the ORM and load it into [GraysQL](https://github.com/larsbs/graysql). This 49 | loader will create the following objects for each model. 50 | 51 | * **Types**: A valid [GraysQL Type](https://github.com/larsbs/graysql#Type) for every model in the ORM. 52 | * **Queries**: For each type created, these queries will be created too: 53 | * `type(id: Int): Type`: Gets a single type by its id. 54 | * `types(): [Type]`: Gets all the types. 55 | * **Mutations**: For each type created, these mutations will be created too: 56 | * `createType(args)`: Creates a type. 57 | * `updateType(args)`: Updates a type. 58 | * `deleteType(args)`: Deletes a type. 59 | 60 | If you don't want to create all the mutations by default, you can customize what mutations the loader will create automatically. 61 | 62 | ### Methods ### 63 | 64 | This extension adds the following method to GraysQL. 65 | 66 | #### `GQL.loadFromORM(translator, [options])` #### 67 | > Receives an instance of an ORM Translator and will use this translator to 68 | > load all the models into GraysQL. 69 | 70 | * **Parameters**: 71 | * `translator` *Object*: An instance of a valid translator. 72 | * `options` *Object*: A configuration object with the following keys and default values: 73 | 74 | ```javascript 75 | { 76 | relay: false, // Create valid relay types instead of default ones. 77 | mutations: { 78 | create: true, // Indicates if a create mutation shoud be created. 79 | update: true, // Indicates if a update mutation should be creted. 80 | delete: true // Indicates if a delete mutation should be created. 81 | } 82 | } 83 | ``` 84 | 85 | ## Translators API ## 86 | 87 | A translator is simply an object that implements certain methods. As long as 88 | the result is this object, it can be a class, a raw object, a function, etc. 89 | There is a [mock translator](test/support/mock-translator.js) in the tests folder that you can take as example. Usually, but not necessarily, a translator 90 | implements a method to receive the models that it should translate from. 91 | 92 | The methods that translators must implement are: 93 | 94 | #### `getModelsNames()` #### 95 | > Returns an array with the name of all the models in the translators. 96 | 97 | * **Returns** 98 | * *Array: String*: An array containing the names of the models inside the translator. 99 | 100 | ```javascript 101 | const expected = ['Group', 'User']; 102 | const result = translator.getModelsNames(); 103 | expect(result).to.deep.equal(expected); 104 | ``` 105 | 106 | #### `parseModelProperties(modelName)` #### 107 | > Returns the parsed properties of the model indicated with `modelName` 108 | > argument. Only the properties are parsed here, ignore the relationships. 109 | > The returned properties object should be an object with keys as the names of [GraysQL Types](http://github.com/larsbs/graysql#Type) fields, and the value as the fields. 110 | 111 | * **Parameters** 112 | * `modelName` *String*: The name of the model to parse. 113 | * **Returns** 114 | * *Object*: An object containing the parsed properties. 115 | 116 | ```javascript 117 | const expected = { 118 | id: { 119 | type: 'Int' 120 | }, 121 | nick: { 122 | type: 'String' 123 | } 124 | }; 125 | const result = translator.parseModelProperties('User'); 126 | expect(result).to.deep.equal(expected); 127 | ``` 128 | 129 | #### `parseModelAssociations(modelName)` #### 130 | > Returns the parsed associations of the model indicated with `modelName` 131 | > argument. ONly the associations are parsed here, ignore the properties. 132 | > The returned associations object should be an object with keys as the names of [GraysQL Types](http://github.com/larsbs/graysql#Type) fields, and the value as the fields. 133 | 134 | * **Parameters** 135 | * `modelName` *String*: The name of the model to parse. 136 | * **Returns** 137 | * *Object*: An object containing the parsed associations. 138 | 139 | ```javascript 140 | const expected = { 141 | members: { 142 | tye: '[User]' 143 | } 144 | }; 145 | const result = translator.parseModelAssociations('Group'); 146 | expect(result).to.deep.equal(expected); 147 | ``` 148 | 149 | #### `getArgsForCreate(modelName)` #### 150 | > Should return an object containing the necessary arguments to create a new entity of the model indicated with `modelName`. The keys of the object are the arguments names and the values are the arguments types. 151 | 152 | * **Parameters** 153 | * `modelName` *String*: The name of the model from which get the arguments. 154 | * **Returns** 155 | * *Object*: The necessary arguments to create a new entity. 156 | 157 | ```javascript 158 | const expected = { 159 | nick: { 160 | type: 'String!' // The nick of the user that it's about to be created 161 | } 162 | }; 163 | const result = translator.getArgsForCreate('User'); 164 | expect(result).to.deep.equal(expected); 165 | ``` 166 | 167 | #### `getArgsForUpdate(modelName)` #### 168 | > Should return an object containing the necessary arguments to update an entity of the model indicated with modelName. The keys of the object are the arguments names and the values are the arguments types. 169 | 170 | * **Parameters** 171 | * `modelName` *String*: The name of the model from which get the arguments. 172 | * **Returns** 173 | * *Object*: The necessary arguments to update an entity. 174 | 175 | ```javascript 176 | const expected = { 177 | id: { 178 | type: 'Int!', 179 | }, 180 | nick: { 181 | type: 'String!' // The new nick of the user 182 | } 183 | }; 184 | const result = translator.getArgsForUpdate('User'); 185 | expect(result).to.deep.equal(expected); 186 | ``` 187 | 188 | #### `getArgsForDelete(modelName)` #### 189 | > Should return an object containing the necessary arguments to delete an entity of the model indicated with modelName. The keys of the object are the arguments names and the values are the arguments types. 190 | 191 | * **Parameters** 192 | * `modelName` *String*: The name of the model from which get the arguments. 193 | * **Returns** 194 | * *Object*: The necessary arguments to delete an entity. 195 | 196 | ```javascript 197 | const expected = { 198 | id: { 199 | type: 'Int!', 200 | } 201 | }; 202 | const result = translator.getArgsForDelete('User'); 203 | expect(result).to.deep.equal(expected); 204 | ``` 205 | 206 | #### `resolveById(modelName)` #### 207 | > Should return a function that takes the same parameters as a `resolve` function from [GraphQL](http://graphql.org/) and returns the resolved entity by id. The id of the entity to returns is in the `args` parameter. 208 | 209 | * **Parameters** 210 | * `modelName` *String*: The name of the model from which resolve the entity. 211 | * **Returns** 212 | * *Function*: The resolve function. 213 | 214 | ```javascript 215 | const expected = (root, args) => DB.getUser(args.id); 216 | const result = translator.resolveById('User'); 217 | expect(result).to.equal(expected); 218 | ``` 219 | 220 | #### `resolveAll(modelName)` #### 221 | > Should return a function that takes the same parameters as a `resolve` function from [GraphQL](http://graphql.org/) and returns all the entities of the model. 222 | 223 | * **Parameters** 224 | * `modelName` *String*: The name of the model from which resolve the entities. 225 | * **Returns** 226 | * *Function*: The resolve function. 227 | 228 | ```javascript 229 | const expected = (root, args) => DB.getUsers(); 230 | const result = translator.resolveAll('User'); 231 | expect(result).to.equal(expected); 232 | ``` 233 | 234 | #### `resolveCreate(modelName)` #### 235 | > Should return a function that takes the same parameters as a `resolve` function from [GraphQL](http://graphql.org/) and creates a new entity of the model. 236 | 237 | * **Parameters** 238 | * `modelName` *String*: The name of the model from which create the entity. 239 | * **Returns** 240 | * *Function*: The resolve function. 241 | 242 | ```javascript 243 | const expected = (root, args) => DB.createUser(args.nick); 244 | const result = translator.resolveCreate('User'); 245 | expect(result).to.equal(expected); 246 | ``` 247 | 248 | #### `resolveUpdate(modelName)` #### 249 | > Should return a function that takes the same parameters as a `resolve` function from [GraphQL](http://graphql.org/) and updates an entity of the model. 250 | 251 | * **Parameters** 252 | * `modelName` *String*: The name of the model from which update the entity. 253 | * **Returns** 254 | * *Function*: The resolve function. 255 | 256 | ```javascript 257 | const expected = (root, args) => DB.updateUser(args.id, args.nick); 258 | const result = translator.resolveUpdate('User'); 259 | expect(result).to.equal(expected); 260 | ``` 261 | 262 | #### `resolveDelete(modelName)` #### 263 | > Should return a function that takes the same parameters as a `resolve` function from [GraphQL](http://graphql.org/) and deletes an entity of the model. 264 | 265 | * **Parameters** 266 | * `modelName` *String*: The name of the model from which delete the entity. 267 | * **Returns** 268 | * *Function*: The resolve function. 269 | 270 | ```javascript 271 | const expected = (root, args) => DB.deleteUser(args.id); 272 | const result = translator.resolveDelete('User'); 273 | expect(result).to.equal(expected); 274 | ``` 275 | 276 | #### `resolveNodeId(modelName)` #### 277 | > Should return a function that takes an id as parameter and returns an entity with the same id of the model specified with `modelName`. Only used if the option `option.relay` is set to `true`. 278 | 279 | * **Parameters** 280 | * `modelName` *String*: The name of the model from which get the entity. 281 | * **Returns** 282 | * *Function*: The nodeId function. 283 | 284 | ```javascript 285 | const expected = (id) => DB.getUser(id); 286 | const result = translator.resolveNodeId('User'); 287 | expect(result).to.equal(expected); 288 | ``` 289 | 290 | ### `resolveIsTypeOf(modelName)` #### 291 | > Should return a function that takes an object as parameter and returns `true` or `false` depending of the type of the object. Is the same as the function `isTypeOf` of GraphQL. 292 | 293 | * **Parameters** 294 | * `modelName` *String*: The name of the model from which resolve the type. 295 | * **Returns** 296 | * *Function*: The isTypeOf function. 297 | 298 | ```javascript 299 | const expected = (obj) => obj instanceof DB.User; 300 | const result = translator.resolveIsTypeOf('User'); 301 | expect(result).to.equal(expected); 302 | ``` 303 | 304 | ## Examples ## 305 | 306 | An usage example can be found in [example](example) directory. 307 | 308 | ## Tests ## 309 | 310 | The tests are written with [mocha](https://mochajs.org) and can be run with the following command: 311 | 312 | ```bash 313 | $ npm test 314 | ``` 315 | 316 | To get a code coverage report, run the following command: 317 | 318 | ```bash 319 | $ npm run cover 320 | ``` 321 | 322 | ## TODO ## 323 | 324 | - [ ] Use GraphQLInputType for mutations instead of passing arguments one by one. 325 | 326 | ## License ## 327 | 328 | [MIT](LICENSE) 329 | -------------------------------------------------------------------------------- /example/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larsbs/graysql-orm-loader/064dd1a596e8d1255f81291df2768d03a3f9118a/example/.gitkeep -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/orm-loader'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graysql-orm-loader", 3 | "version": "0.1.3", 4 | "description": "A GraysQL extension to load a GraphQL schema from a ORM", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run lint && mocha --check-leaks -t 5000 test/index.js", 8 | "cover": "npm run lint && istanbul cover _mocha -- --check-leaks -t 5000 -b -R spec test/index.js", 9 | "lint": "eslint index.js src/" 10 | }, 11 | "keywords": [ 12 | "graphql", 13 | "graysql", 14 | "graysql-extension" 15 | ], 16 | "contributors": [ 17 | "Lorenzo Ruiz ", 18 | "José A. Jarana " 19 | ], 20 | "license": "MIT", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/larsbs/graysql-orm-loader.git" 24 | }, 25 | "peerDependencies": { 26 | "graysql": "^0.4.1" 27 | }, 28 | "devDependencies": { 29 | "chai": "^3.4.1", 30 | "eslint": "^1.10.3", 31 | "graphql": "^0.4.14", 32 | "graphql-relay": "^0.3.6", 33 | "graysql": "^0.4.1", 34 | "istanbul": "^0.4.2", 35 | "mocha": "^2.3.4" 36 | }, 37 | "dependencies": { 38 | "pluralize": "^1.2.1", 39 | "deep-freeze": "0.0.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/orm-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const pluralize = require('pluralize'); 4 | const Utils = require('./utils'); 5 | 6 | 7 | function _setDefaults(options) { 8 | options = Object.assign({ 9 | relay: false, 10 | mutations: {} 11 | }, options); 12 | options.mutations = Object.assign({ 13 | create: true, 14 | update: true, 15 | delete: true 16 | }, options.mutations); 17 | return options; 18 | } 19 | 20 | 21 | module.exports = (/* GraysQL */) => { 22 | 23 | let _translator; 24 | 25 | function _getMutationsForModel(modelName, mutationsOptions) { 26 | const mutations = {}; 27 | 28 | if (mutationsOptions.create) { 29 | mutations[`create${Utils.capitalize(modelName)}`] = { 30 | type: modelName, 31 | args: _translator.getArgsForCreate(modelName), 32 | resolve: _translator.resolveCreate(modelName) 33 | }; 34 | } 35 | if (mutationsOptions.update) { 36 | mutations[`update${Utils.capitalize(modelName)}`] = { 37 | type: modelName, 38 | args: _translator.getArgsForUpdate(modelName), 39 | resolve: _translator.resolveUpdate(modelName) 40 | }; 41 | } 42 | if (mutationsOptions.delete) { 43 | mutations[`delete${Utils.capitalize(modelName)}`] = { 44 | type: modelName, 45 | args: _translator.getArgsForDelete(modelName), 46 | resolve: _translator.resolveDelete(modelName) 47 | }; 48 | } 49 | 50 | return mutations; 51 | } 52 | 53 | function _getQueriesForModel(modelName) { 54 | const findOne = { 55 | type: modelName, 56 | args: { 57 | id: { type: 'Int!' } 58 | }, 59 | resolve: _translator.resolveById(modelName) 60 | }; 61 | const findAll = { 62 | type: `[${modelName}]`, 63 | resolve: _translator.resolveAll(modelName) 64 | }; 65 | return { 66 | [modelName.toLowerCase()]: findOne, 67 | [pluralize(modelName.toLowerCase())]: findAll 68 | }; 69 | } 70 | 71 | function _getTypeFromModel(modelName, options) { 72 | const modelProperties = _translator.parseModelProperties(modelName); 73 | const modelAssociations = _translator.parseModelAssociations(modelName, options.relay); 74 | 75 | let type = { 76 | name: modelName, 77 | fields: Object.assign({}, modelProperties, modelAssociations), 78 | queries: _getQueriesForModel(modelName), 79 | mutations: _getMutationsForModel(modelName, options.mutations) 80 | }; 81 | 82 | if (options.relay) { 83 | type = Object.assign({}, type, { 84 | interfaces: ['Node'], 85 | nodeId: _translator.resolveNodeId(modelName), 86 | isTypeOf: _translator.resolveIsTypeOf(modelName) 87 | }); 88 | } 89 | 90 | return (/* GQL */) => type; 91 | } 92 | 93 | return { 94 | loadFromORM(translator, opts) { 95 | if ( ! translator || typeof translator !== 'object' || Array.isArray(translator)) { 96 | throw new TypeError(`Expected translator to be an object, got ${typeof translator} instead`); 97 | } 98 | 99 | if ( ! Utils.isValidTranslator(translator)) { 100 | throw new TypeError(`Invalid translator received. A translator must implement the Translators API.`); 101 | } 102 | 103 | _translator = translator; 104 | 105 | const modelsNames = translator.getModelsNames(); // [ ModelName, ModelName2, ... ] 106 | const options = _setDefaults(opts); 107 | 108 | if (options.relay) { 109 | // TODO: Add a method to check if Graylay is enabled, and if not, throw an Error 110 | } 111 | 112 | for (const modelName of modelsNames) { 113 | const type = _getTypeFromModel(modelName, options); 114 | this.registerType(type); 115 | } 116 | } 117 | 118 | }; 119 | 120 | }; 121 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | isValidTranslator(translator) { 3 | const mustKeys = [ 4 | 'getModelsNames', 5 | 'parseModelProperties', 6 | 'parseModelAssociations' 7 | ]; 8 | for (const key of mustKeys) { 9 | if ( ! translator[key]) { 10 | return false; 11 | } 12 | } 13 | return true; 14 | }, 15 | capitalize(str) { 16 | return str.replace(/(?:^|\s)\S/g, c => c.toUpperCase()); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const ORMLoader = require('../src/orm-loader'); 2 | 3 | 4 | describe('UNIT TESTS', function () { 5 | describe('ORMLoader', function () { 6 | 7 | require('./unit/orm-loader')(ORMLoader); 8 | 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/support/db.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Group(id, name) { 4 | this.id = id; 5 | this.name = name; 6 | this.members = []; 7 | } 8 | 9 | 10 | function User(id, nick, group) { 11 | this.id = id; 12 | this.nick = nick; 13 | this.group = group; 14 | group.members.push(this); 15 | } 16 | 17 | 18 | const groups = [ 19 | new Group(1, 'Group 1'), 20 | new Group(2, 'Group 2') 21 | ]; 22 | 23 | 24 | const users = [ 25 | new User(1, 'Lars', groups[0]), 26 | new User(2, 'Deathvoid', groups[0]), 27 | new User(3, 'Grishan', groups[1]) 28 | ]; 29 | 30 | 31 | module.exports = { 32 | getUser: (id) => users.find(u => u.id === id), 33 | getGroup: (id) => groups.find(g => g.id === id), 34 | User, 35 | Group 36 | }; 37 | -------------------------------------------------------------------------------- /test/support/mock-models.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const DB = require('./db'); 4 | 5 | 6 | module.exports = { 7 | Group: { 8 | attributes: { 9 | id: { type: 'Integer' }, 10 | name: { type: 'String' } 11 | }, 12 | relationships: { 13 | members: { 14 | hasMany: 'User' 15 | } 16 | }, 17 | model: DB.Group, 18 | findById(id) { 19 | return DB.getGroup(id); 20 | }, 21 | findAll() { 22 | return DB.getGroups(); 23 | } 24 | }, 25 | User: { 26 | attributes: { 27 | id: { type: 'Integer' }, 28 | nick: { type: 'String' } 29 | }, 30 | relationships: { 31 | group: { 32 | belongsTo: 'Group' 33 | } 34 | }, 35 | model: DB.User, 36 | findById(id) { 37 | return DB.getUser(id); 38 | }, 39 | findAll() { 40 | return DB.getUsers(); 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /test/support/mock-translator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const deepFreeze = require('deep-freeze'); 4 | const DB = require('./db'); 5 | 6 | 7 | function parseTypeToGraysQL(type) { 8 | switch(type) { 9 | case 'Integer': 10 | return 'Int'; 11 | default: 12 | return type; 13 | } 14 | } 15 | 16 | function parseRelationshipUsingRelay(key, relationship) { 17 | return { 18 | type: `@${relationship.hasMany}`, 19 | resolve: (root, args) => root[key] 20 | } 21 | } 22 | 23 | function parseRelationshipToGraysQL(key, relationship, useRelay) { 24 | if (relationship.hasOwnProperty('belongsTo')) { 25 | return { type: relationship.belongsTo }; 26 | } 27 | else if (relationship.hasOwnProperty('hasMany')){ 28 | if (useRelay) { 29 | return parseRelationshipUsingRelay(key, relationship); 30 | } 31 | else { 32 | return { type: `[${relationship.hasMany}]`}; 33 | } 34 | } 35 | } 36 | 37 | 38 | class MockTranslator { 39 | 40 | constructor(models) { 41 | this._models = deepFreeze(models); 42 | } 43 | 44 | getModelsNames() { 45 | return Object.keys(this._models); 46 | } 47 | 48 | parseModelProperties(modelName) { 49 | const model = this._models[modelName]; 50 | const properties = {}; 51 | for (const key in model.attributes) { 52 | properties[key] = Object.assign({}, model.attributes[key], { 53 | type: parseTypeToGraysQL(model.attributes[key].type) 54 | }); 55 | } 56 | return properties; 57 | } 58 | 59 | parseModelAssociations(modelName, useRelay) { 60 | const model = this._models[modelName]; 61 | const properties = {}; 62 | for (const key in model.relationships) { 63 | properties[key] = parseRelationshipToGraysQL(key, model.relationships[key], useRelay); 64 | } 65 | return properties; 66 | } 67 | 68 | getArgsForCreate(modelName) { 69 | const args = this.parseModelProperties(modelName); 70 | delete args.id; 71 | for (const key in args) { 72 | args[key].type = args[key].type + '!'; 73 | } 74 | return args; 75 | } 76 | 77 | getArgsForUpdate(modelName) { 78 | const args = this.parseModelProperties(modelName); 79 | for (const key in args) { 80 | args[key].type = args[key].type + '!'; 81 | } 82 | return args; 83 | } 84 | 85 | getArgsForDelete(modelName) { 86 | const args = this.parseModelProperties(modelName); 87 | return { id: { type: args.id.type + '!' }}; 88 | } 89 | 90 | resolveById(modelName) { 91 | return (root, args) => this._models[modelName].findById(args.id); 92 | } 93 | 94 | resolveAll(modelName) { 95 | return (root, args) => this._models[modelName].findAll(); 96 | } 97 | 98 | resolveCreate(modelName) { 99 | } 100 | 101 | resolveUpdate(modelName) { 102 | } 103 | 104 | resolveDelete(modelName) { 105 | } 106 | 107 | resolveNodeId(modelName) { 108 | return id => this._models[modelName].findById(id); 109 | } 110 | 111 | resolveIsTypeOf(modelName) { 112 | return obj => obj instanceof this._models[modelName].model; 113 | } 114 | 115 | } 116 | 117 | module.exports = MockTranslator; 118 | -------------------------------------------------------------------------------- /test/support/test-schema-relay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const graphql = require('graphql'); 4 | const GraphQLRelay = require('graphql-relay'); 5 | const DB = require('./db'); 6 | 7 | 8 | const NodeInterface = GraphQLRelay.nodeDefinitions(globalId => { 9 | const node = GraphQLRelay.fromGlobalId(globalId); 10 | switch (node.type) { 11 | case 'User': 12 | return DB.getUser(node.id); 13 | case 'Group': 14 | return DB.getGroup(node.id); 15 | default: 16 | return null; 17 | } 18 | }).nodeInterface; 19 | 20 | 21 | const User = new graphql.GraphQLObjectType({ 22 | name: 'User', 23 | interfaces: [NodeInterface], 24 | isTypeOf: obj => obj instanceof DB.User, 25 | fields: () => ({ 26 | id: GraphQLRelay.globalIdField('User'), 27 | nick: { type: graphql.GraphQLString }, 28 | group: { type: Group } 29 | }) 30 | }); 31 | 32 | 33 | const Group = new graphql.GraphQLObjectType({ 34 | name: 'Group', 35 | interfaces: [NodeInterface], 36 | isTypeOf: obj => obj instanceof DB.Group, 37 | fields: () => ({ 38 | id: GraphQLRelay.globalIdField('Group'), 39 | name: { type: graphql.GraphQLString }, 40 | members: { 41 | type: GraphQLRelay.connectionDefinitions({ 42 | name: 'User', 43 | nodeType: User 44 | }).connectionType, 45 | resolve: (group, args) => GraphQLRelay.connectionFromArray(group.members, args) 46 | } 47 | }) 48 | }); 49 | 50 | 51 | const Query = new graphql.GraphQLObjectType({ 52 | name: 'Query', 53 | fields: () => ({ 54 | group: { 55 | type: Group, 56 | args: { 57 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) } 58 | }, 59 | resolve: (_, args) => DB.getGroup(args.id) 60 | }, 61 | groups: { 62 | type: new graphql.GraphQLList(Group), 63 | resolve: (_, args) => DB.getGroups() 64 | }, 65 | user: { 66 | type: User, 67 | args: { 68 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) } 69 | }, 70 | resolve: (_, args) => DB.getUser(args.id) 71 | }, 72 | users: { 73 | type: new graphql.GraphQLList(User), 74 | resolve: (_, args) => DB.getUsers() 75 | } 76 | }) 77 | }); 78 | 79 | 80 | const Mutation = new graphql.GraphQLObjectType({ 81 | name: 'Mutation', 82 | fields: () => ({ 83 | createGroup: { 84 | type: Group, 85 | args: { 86 | name: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) } 87 | }, 88 | resolve: (_, args) => ({ id: 5, nick: args.name }) 89 | }, 90 | updateGroup: { 91 | type: Group, 92 | args: { 93 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) }, 94 | name: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) } 95 | }, 96 | resolve: (_, args) => ({ id: args.id, name: args.name }) 97 | }, 98 | deleteGroup: { 99 | type: Group, 100 | args: { 101 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) }, 102 | }, 103 | resolve: (_, args) => DB.getGroup(args.id) 104 | }, 105 | createUser: { 106 | type: User, 107 | args: { 108 | nick: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) } 109 | }, 110 | resolve: (_, args) => ({ id: 5, nick: args.nick }) 111 | }, 112 | updateUser: { 113 | type: User, 114 | args: { 115 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) }, 116 | nick: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) } 117 | }, 118 | resolve: (_, args) => ({ id: args.id, nick: args.nick }) 119 | }, 120 | deleteUser: { 121 | type: User, 122 | args: { 123 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) }, 124 | }, 125 | resolve: (_, args) => DB.getUser(args.id) 126 | } 127 | }) 128 | }); 129 | 130 | 131 | const Schema = new graphql.GraphQLSchema({ 132 | query: Query, 133 | mutation: Mutation 134 | }); 135 | 136 | 137 | module.exports = { 138 | User, 139 | Group, 140 | Query, 141 | Schema 142 | }; 143 | -------------------------------------------------------------------------------- /test/support/test-schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const graphql = require('graphql'); 4 | const DB = require('./db'); 5 | 6 | 7 | const User = new graphql.GraphQLObjectType({ 8 | name: 'User', 9 | isTypeOf: obj => obj instanceof DB.User, 10 | fields: () => ({ 11 | id: { type: graphql.GraphQLInt }, 12 | nick: { type: graphql.GraphQLString }, 13 | group: { type: Group } 14 | }) 15 | }); 16 | 17 | 18 | const Group = new graphql.GraphQLObjectType({ 19 | name: 'Group', 20 | fields: () => ({ 21 | id: { type: graphql.GraphQLInt }, 22 | name: { type: graphql.GraphQLString }, 23 | members: { type: new graphql.GraphQLList(User) } 24 | }) 25 | }); 26 | 27 | 28 | const Query = new graphql.GraphQLObjectType({ 29 | name: 'Query', 30 | fields: () => ({ 31 | group: { 32 | type: Group, 33 | args: { 34 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) } 35 | }, 36 | resolve: (_, args) => DB.getGroup(args.id) 37 | }, 38 | groups: { 39 | type: new graphql.GraphQLList(Group), 40 | resolve: (_, args) => DB.getGroups() 41 | }, 42 | user: { 43 | type: User, 44 | args: { 45 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) } 46 | }, 47 | resolve: (_, args) => DB.getUser(args.id) 48 | }, 49 | users: { 50 | type: new graphql.GraphQLList(User), 51 | resolve: (_, args) => DB.getUsers() 52 | } 53 | }) 54 | }); 55 | 56 | 57 | const Mutation = new graphql.GraphQLObjectType({ 58 | name: 'Mutation', 59 | fields: () => ({ 60 | createGroup: { 61 | type: Group, 62 | args: { 63 | name: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) } 64 | }, 65 | resolve: (_, args) => ({ id: 5, nick: args.name }) 66 | }, 67 | updateGroup: { 68 | type: Group, 69 | args: { 70 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) }, 71 | name: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) } 72 | }, 73 | resolve: (_, args) => ({ id: args.id, name: args.name }) 74 | }, 75 | deleteGroup: { 76 | type: Group, 77 | args: { 78 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) }, 79 | }, 80 | resolve: (_, args) => DB.getGroup(args.id) 81 | }, 82 | createUser: { 83 | type: User, 84 | args: { 85 | nick: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) } 86 | }, 87 | resolve: (_, args) => ({ id: 5, nick: args.nick }) 88 | }, 89 | updateUser: { 90 | type: User, 91 | args: { 92 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) }, 93 | nick: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) } 94 | }, 95 | resolve: (_, args) => ({ id: args.id, nick: args.nick }) 96 | }, 97 | deleteUser: { 98 | type: User, 99 | args: { 100 | id: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) }, 101 | }, 102 | resolve: (_, args) => DB.getUser(args.id) 103 | } 104 | }) 105 | }); 106 | 107 | 108 | const Schema = new graphql.GraphQLSchema({ 109 | query: Query, 110 | mutation: Mutation 111 | }); 112 | 113 | 114 | module.exports = { 115 | User, 116 | Group, 117 | Query, 118 | Schema 119 | }; 120 | -------------------------------------------------------------------------------- /test/unit/orm-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const GraysQL = require('graysql'); 5 | const Graylay = require('graysql/extensions/graylay'); 6 | const graphql = require('graphql'); 7 | const GraphQLUtils = require('graphql/utilities'); 8 | 9 | const MockTranslator = require('../support/mock-translator'); 10 | const MockModels = require('../support/mock-models'); 11 | const TestSchema = require('../support/test-schema'); 12 | const TestSchemaRelay = require('../support/test-schema-relay'); 13 | 14 | 15 | module.exports = function (ORMLoader) { 16 | 17 | describe('#loadFromORM(translator, [options])', function () { 18 | 19 | let GQL; 20 | beforeEach(function () { 21 | GQL = new GraysQL(); 22 | GQL.use(ORMLoader); 23 | }); 24 | 25 | it('should only accept an object as a translator', function () { 26 | expect(() => GQL.loadFromORM('asdfa')).to.throw(TypeError, /Expected translator to be an object/); 27 | expect(() => GQL.loadFromORM(null)).to.throw(TypeError, /Expected translator to be an object/); 28 | expect(() => GQL.loadFromORM(x => x)).to.throw(TypeError, /Expected translator to be an object/); 29 | }); 30 | 31 | it('should accept a valid translator', function () { 32 | function InvalidTranslator() {}; 33 | expect(() => GQL.loadFromORM(new InvalidTranslator())).to.throw(TypeError, /Invalid translator/); 34 | expect(() => GQL.loadFromORM(new MockTranslator(MockModels))).to.not.throw(TypeError, /Invalid translator/); 35 | }); 36 | 37 | it('should generate a complete schema', function () { 38 | GQL.loadFromORM(new MockTranslator(MockModels)); 39 | const expected = GraphQLUtils.printSchema(TestSchema.Schema); 40 | const result = GraphQLUtils.printSchema(GQL.generateSchema()); 41 | expect(result).to.equal(expected); 42 | }); 43 | it('should generate a valid schema', function (done) { 44 | GQL.loadFromORM(new MockTranslator(MockModels)); 45 | const Schema = GQL.generateSchema(); 46 | const query = `query GetUser { 47 | user(id: 1) { 48 | id, 49 | nick, 50 | group { 51 | id, 52 | name, 53 | members { 54 | id 55 | } 56 | } 57 | } 58 | }`; 59 | const expected = { 60 | data: { 61 | user: { 62 | id: 1, 63 | nick: 'Lars', 64 | group: { 65 | id: 1, 66 | name: 'Group 1', 67 | members: [{ id: 1 }, { id: 2 }] 68 | } 69 | } 70 | } 71 | }; 72 | graphql.graphql(Schema, query) 73 | .then(result => { 74 | expect(result).to.deep.equal(expected); 75 | done(); 76 | }) 77 | .catch(err => done(err)); 78 | }); 79 | it('should generate a complete relay schema when options.relay is true', function () { 80 | GQL.use(Graylay); 81 | GQL.loadFromORM(new MockTranslator(MockModels), { relay: true }); 82 | const Schema = GQL.generateSchema(); 83 | const expected = GraphQLUtils.printSchema(TestSchemaRelay.Schema); 84 | const result = GraphQLUtils.printSchema(Schema); 85 | expect(result).to.equal(expected); 86 | }); 87 | it('should generate a valid schema when options.relay is true', function (done) { 88 | GQL.use(Graylay); 89 | GQL.loadFromORM(new MockTranslator(MockModels), { relay: true }); 90 | const Schema = GQL.generateSchema(); 91 | const query = `query GetGroup { 92 | group(id: 1) { 93 | id, 94 | name, 95 | members { 96 | edges { 97 | node { 98 | id, 99 | nick 100 | } 101 | } 102 | } 103 | } 104 | }`; 105 | const expected = { 106 | data: { 107 | group: { 108 | id: "R3JvdXA6MQ==", 109 | name: "Group 1", 110 | members: { 111 | edges: [{ 112 | node: { 113 | id: "VXNlcjox", 114 | nick: "Lars" 115 | } 116 | }, { 117 | node: { 118 | id: "VXNlcjoy", 119 | nick: "Deathvoid" 120 | } 121 | }] 122 | } 123 | } 124 | } 125 | }; 126 | graphql.graphql(Schema, query) 127 | .then(result => { 128 | expect(result).to.deep.equal(expected); 129 | done(); 130 | }) 131 | .catch(err => done(err)); 132 | }); 133 | it('should not generate create mutations when options.mutations.create is false', function () { 134 | GQL.loadFromORM(new MockTranslator(MockModels), { 135 | mutations: { 136 | create: false 137 | } 138 | }); 139 | const Schema = GQL.generateSchema(); 140 | const expected = GraphQLUtils.printSchema(TestSchema.Schema).replace(/\n^\s+create.*$/gm, ''); 141 | const result = GraphQLUtils.printSchema(Schema); 142 | expect(result).to.equal(expected); 143 | }); 144 | it('should not generate update mutations when options.mutations.update is false', function () { 145 | GQL.loadFromORM(new MockTranslator(MockModels), { 146 | mutations: { 147 | update: false 148 | } 149 | }); 150 | const Schema = GQL.generateSchema(); 151 | const expected = GraphQLUtils.printSchema(TestSchema.Schema).replace(/\n^\s+update.*$/gm, ''); 152 | const result = GraphQLUtils.printSchema(Schema); 153 | expect(result).to.equal(expected); 154 | }); 155 | it('should not generate delete mutations when options.mutations.delete is false', function () { 156 | GQL.loadFromORM(new MockTranslator(MockModels), { 157 | mutations: { 158 | delete: false 159 | } 160 | }); 161 | const Schema = GQL.generateSchema(); 162 | const expected = GraphQLUtils.printSchema(TestSchema.Schema).replace(/\n^\s+delete.*$/gm, ''); 163 | const result = GraphQLUtils.printSchema(Schema); 164 | expect(result).to.equal(expected); 165 | }); 166 | }); 167 | 168 | 169 | }; 170 | --------------------------------------------------------------------------------