├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json └── src ├── createMutations.js ├── createQueries.js ├── createTypes.js ├── formatErrors.js ├── generateSchema.js ├── helpers.js ├── index.js └── resolvers.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "plugins": [ 5 | "babel" 6 | ], 7 | 8 | "env": { 9 | "es6": true, 10 | "node": true 11 | }, 12 | 13 | "ecmaFeatures": { 14 | "arrowFunctions": true, 15 | "binaryLiterals": true, 16 | "blockBindings": true, 17 | "classes": true, 18 | "defaultParams": true, 19 | "destructuring": true, 20 | "experimentalObjectRestSpread": true, 21 | "forOf": true, 22 | "generators": true, 23 | "globalReturn": true, 24 | "jsx": true, 25 | "modules": true, 26 | "objectLiteralComputedProperties": true, 27 | "objectLiteralDuplicateProperties": true, 28 | "objectLiteralShorthandMethods": true, 29 | "objectLiteralShorthandProperties": true, 30 | "octalLiterals": true, 31 | "regexUFlag": true, 32 | "regexYFlag": true, 33 | "restParams": true, 34 | "spread": true, 35 | "superInFunctions": true, 36 | "templateStrings": true, 37 | "unicodeCodePointEscapes": true 38 | }, 39 | 40 | "rules": { 41 | "babel/arrow-parens": [2, "always"], 42 | "babel/array-bracket-spacing": [2, "always"], 43 | "babel/generator-star-spacing": [2, {"before": true, "after": false}], 44 | "array-bracket-spacing": 0, 45 | "generator-star-spacing": 0, 46 | 47 | "arrow-spacing": 2, 48 | "block-scoped-var": 0, 49 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 50 | "callback-return": 2, 51 | "camelcase": [2, {"properties": "always"}], 52 | "comma-dangle": 0, 53 | "comma-spacing": 0, 54 | "comma-style": [2, "last"], 55 | "complexity": 0, 56 | "computed-property-spacing": [2, "never"], 57 | "consistent-return": 0, 58 | "consistent-this": 0, 59 | "curly": [2, "all"], 60 | "default-case": 0, 61 | "dot-location": [2, "property"], 62 | "dot-notation": 0, 63 | "eol-last": 2, 64 | "eqeqeq": 2, 65 | "func-names": 0, 66 | "func-style": 0, 67 | "guard-for-in": 2, 68 | "handle-callback-err": [2, "error"], 69 | "id-length": 0, 70 | "id-match": [2, "^(?:_?[a-zA-Z0-9]*)|[_A-Z0-9]+$"], 71 | "indent": [2, "tab"], 72 | "init-declarations": 0, 73 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 74 | "keyword-spacing": 2, 75 | "linebreak-style": 2, 76 | "lines-around-comment": 0, 77 | "max-depth": 0, 78 | "max-len": [2, 80, 4], 79 | "max-nested-callbacks": 0, 80 | "max-params": 0, 81 | "max-statements": 0, 82 | "new-cap": 0, 83 | "new-parens": 2, 84 | "newline-after-var": 0, 85 | "no-alert": 2, 86 | "no-array-constructor": 2, 87 | "no-bitwise": 0, 88 | "no-caller": 2, 89 | "no-catch-shadow": 0, 90 | "no-class-assign": 2, 91 | "no-cond-assign": 2, 92 | "no-console": 1, 93 | "no-const-assign": 2, 94 | "no-constant-condition": 2, 95 | "no-continue": 0, 96 | "no-control-regex": 0, 97 | "no-debugger": 1, 98 | "no-delete-var": 2, 99 | "no-div-regex": 2, 100 | "no-dupe-args": 2, 101 | "no-dupe-keys": 2, 102 | "no-duplicate-case": 2, 103 | "no-else-return": 2, 104 | "no-empty": 2, 105 | "no-empty-character-class": 2, 106 | "no-eq-null": 0, 107 | "no-eval": 2, 108 | "no-ex-assign": 2, 109 | "no-extend-native": 2, 110 | "no-extra-bind": 2, 111 | "no-extra-boolean-cast": 2, 112 | "no-extra-parens": 0, 113 | "no-extra-semi": 2, 114 | "no-fallthrough": 2, 115 | "no-floating-decimal": 2, 116 | "no-func-assign": 2, 117 | "no-implicit-coercion": 2, 118 | "no-implied-eval": 2, 119 | "no-inline-comments": 0, 120 | "no-inner-declarations": [2, "functions"], 121 | "no-invalid-regexp": 2, 122 | "no-invalid-this": 0, 123 | "no-irregular-whitespace": 2, 124 | "no-iterator": 2, 125 | "no-label-var": 2, 126 | "no-labels": [2, {"allowLoop": true}], 127 | "no-lone-blocks": 2, 128 | "no-lonely-if": 2, 129 | "no-loop-func": 0, 130 | "no-mixed-requires": [2, true], 131 | "no-mixed-spaces-and-tabs": 2, 132 | "no-multi-spaces": 2, 133 | "no-multi-str": 2, 134 | "no-multiple-empty-lines": 0, 135 | "no-native-reassign": 0, 136 | "no-negated-in-lhs": 2, 137 | "no-nested-ternary": 0, 138 | "no-new": 2, 139 | "no-new-func": 0, 140 | "no-new-object": 2, 141 | "no-new-require": 2, 142 | "no-new-wrappers": 2, 143 | "no-obj-calls": 2, 144 | "no-octal": 2, 145 | "no-octal-escape": 2, 146 | "no-param-reassign": 2, 147 | "no-path-concat": 2, 148 | "no-plusplus": 0, 149 | "no-process-env": 0, 150 | "no-process-exit": 0, 151 | "no-proto": 2, 152 | "no-redeclare": 2, 153 | "no-regex-spaces": 2, 154 | "no-restricted-modules": 0, 155 | "no-return-assign": 2, 156 | "no-script-url": 2, 157 | "no-self-compare": 0, 158 | "no-sequences": 2, 159 | "no-shadow": 2, 160 | "no-shadow-restricted-names": 2, 161 | "no-spaced-func": 2, 162 | "no-sparse-arrays": 2, 163 | "no-sync": 2, 164 | "no-ternary": 0, 165 | "no-this-before-super": 2, 166 | "no-throw-literal": 2, 167 | "no-trailing-spaces": 2, 168 | "no-undef": 2, 169 | "no-undef-init": 2, 170 | "no-undefined": 0, 171 | "no-underscore-dangle": 0, 172 | "no-unexpected-multiline": 2, 173 | "no-unneeded-ternary": 2, 174 | "no-unreachable": 2, 175 | "no-unused-expressions": 2, 176 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 177 | "no-use-before-define": 0, 178 | "no-useless-call": 2, 179 | "no-var": 2, 180 | "no-void": 2, 181 | "no-warning-comments": 0, 182 | "no-with": 2, 183 | "object-curly-spacing": [0, "always"], 184 | "object-shorthand": [2, "always"], 185 | "one-var": [2, "never"], 186 | "operator-assignment": [2, "always"], 187 | "operator-linebreak": [2, "after"], 188 | "padded-blocks": 0, 189 | "prefer-const": 2, 190 | "prefer-reflect": 0, 191 | "prefer-spread": 0, 192 | "quote-props": [2, "as-needed"], 193 | "quotes": [2, "single"], 194 | "radix": 2, 195 | "require-yield": 2, 196 | "semi": [2, "always"], 197 | "semi-spacing": [2, {"before": false, "after": true}], 198 | "sort-vars": 0, 199 | "space-before-blocks": [2, "always"], 200 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 201 | "space-in-parens": 0, 202 | "space-infix-ops": [2, {"int32Hint": false}], 203 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 204 | "spaced-comment": [2, "always"], 205 | "strict": 0, 206 | "use-isnan": 2, 207 | "valid-jsdoc": 0, 208 | "valid-typeof": 2, 209 | "vars-on-top": 0, 210 | "wrap-iife": 2, 211 | "wrap-regex": 0, 212 | "yoda": [2, "never", {"exceptRange": true}] 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | lib 3 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | src/ 3 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 zhukmj 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 | # Sails models -> GraphQL schema 2 | This module will help you to create GraphQL schema of each model in your Sails application. 3 | It uses Waterline configuration files for creating GraphQL objects and `sails.request()` method for performing internal requests and resolving queries/mutations. 4 | 5 | ``` 6 | npm install sails-graphql --save 7 | ``` 8 | 9 | ## Quick example 10 | Assume you have two models: Author and Article 11 | ```javascript 12 | /** 13 | * api/models/Author.js 14 | */ 15 | module.exports = { 16 | attributes: { 17 | name: { 18 | type: 'string' 19 | }, 20 | articles: { 21 | collection: 'article', 22 | via: 'author' 23 | } 24 | } 25 | }; 26 | 27 | /** 28 | * api/models/Article.js 29 | */ 30 | module.exports = { 31 | attributes: { 32 | name: { 33 | type: 'string' 34 | }, 35 | slug: { 36 | type: 'string', 37 | unique: true 38 | }, 39 | author: { 40 | model: 'author' 41 | } 42 | } 43 | }; 44 | ``` 45 | 46 | Then `sails-graphql` will generate you the following schema: 47 | 48 | ```javascript 49 | import { generateSchema } from 'sails-graphql'; 50 | import { printSchema } from 'graphql'; 51 | 52 | // expected that sails is defined globally 53 | const schema = generateSchema(sails.models); 54 | console.log(printSchema(schema)); 55 | 56 | /** 57 | * Prints out: 58 | * 59 | * schema { 60 | * query: RootQueryType 61 | * mutation: RootMutationType 62 | * } 63 | * 64 | * type ArticleConnectionType { 65 | * page: Int 66 | * pages: Int 67 | * perPage: Int 68 | * total: Int 69 | * edges: [ArticleType] 70 | * } 71 | * 72 | * input ArticleInputType { 73 | * name: String 74 | * slug: String 75 | * author: AuthorInputType 76 | * id: String 77 | * createdAt: String 78 | * updatedAt: String 79 | * } 80 | * 81 | * type ArticleType { 82 | * name: String 83 | * slug: String 84 | * author: AuthorType 85 | * id: String 86 | * createdAt: String 87 | * updatedAt: String 88 | * } 89 | * 90 | * type AuthorArticlesConnectionType { 91 | * page: Int 92 | * pages: Int 93 | * perPage: Int 94 | * total: Int 95 | * edges: [ArticleType] 96 | * } 97 | * 98 | * type AuthorConnectionType { 99 | * page: Int 100 | * pages: Int 101 | * perPage: Int 102 | * total: Int 103 | * edges: [AuthorType] 104 | * } 105 | * 106 | * input AuthorInputType { 107 | * name: String 108 | * articles: [ArticleInputType] 109 | * id: String 110 | * createdAt: String 111 | * updatedAt: String 112 | * } 113 | * 114 | * type AuthorType { 115 | * name: String 116 | * articles(where: String, limit: Int, skip: Int, sort: String): AuthorArticlesConnectionType 117 | * id: String 118 | * createdAt: String 119 | * updatedAt: String 120 | * } 121 | * 122 | * type RootMutationType { 123 | * createArticle(article: ArticleInputType!): ArticleType 124 | * deleteArticle(id: String!): ArticleType 125 | * updateArticle(id: String!, article: ArticleInputType!): ArticleType 126 | * createAuthor(author: AuthorInputType!): AuthorType 127 | * deleteAuthor(id: String!): AuthorType 128 | * updateAuthor(id: String!, author: AuthorInputType!): AuthorType 129 | * } 130 | * 131 | * type RootQueryType { 132 | * article(slug: String, id: String): ArticleType 133 | * articles(where: String, limit: Int, skip: Int, sort: String): ArticleConnectionType 134 | * author(id: String): AuthorType 135 | * authors(where: String, limit: Int, skip: Int, sort: String): AuthorConnectionType 136 | * } 137 | * 138 | */ 139 | 140 | ``` 141 | 142 | ## Usage 143 | 1. Make sure you have `sails` and `graphql` installed 144 | 2. Run `npm install sails-graphql --save` 145 | 3. Create a GraphQLController 146 | ```javascript 147 | /** 148 | * api/controllers/GraphQLController.js 149 | */ 150 | import { graphql } from 'graphql'; 151 | import { generateSchema } from 'sails-graphql'; 152 | 153 | let schema = null; 154 | 155 | module.exports = { 156 | index(req, res) { // default index action 157 | 158 | if (!schema) { 159 | schema = generateSchema(sails.models); 160 | } 161 | 162 | graphql( 163 | schema, // generated schema 164 | req.body, // graphql query string 165 | null, // default rootValue 166 | { // context 167 | request: sails.request, // default request method - required 168 | reqData: { // object of any data you want to forward to server's internal request 169 | headers: {/*your headers to forward */} 170 | } 171 | } 172 | ).then((result) => { 173 | // errors handling 174 | res.json(result.data); 175 | }); 176 | } 177 | }; 178 | 179 | ``` 180 | Try to `POST` some query to `/graphql`. That's it! 181 | 182 | 183 | ## License 184 | The MIT License (MIT) 185 | 186 | Copyright (c) 2016 zhukmj 187 | 188 | Permission is hereby granted, free of charge, to any person obtaining a copy 189 | of this software and associated documentation files (the "Software"), to deal 190 | in the Software without restriction, including without limitation the rights 191 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 192 | copies of the Software, and to permit persons to whom the Software is 193 | furnished to do so, subject to the following conditions: 194 | 195 | The above copyright notice and this permission notice shall be included in all 196 | copies or substantial portions of the Software. 197 | 198 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 199 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 200 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 201 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 202 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 203 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 204 | SOFTWARE. 205 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-graphql", 3 | "version": "0.0.3", 4 | "description": "A tool for generating GraphQL Schema based on Sails models' configuration", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "compile": "./node_modules/.bin/babel --presets es2015,stage-0 -d lib/ src/", 8 | "prepublish": "npm run compile" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/zhukmj/sails-graphql.git" 13 | }, 14 | "keywords": [ 15 | "sails", 16 | "graphql" 17 | ], 18 | "author": "Mike Zhuk", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/zhukmj/sails-graphql/issues" 22 | }, 23 | "homepage": "https://github.com/zhukmj/sails-graphql#readme", 24 | "dependencies": { 25 | "pluralize": "^1.2.1" 26 | }, 27 | "peerDependencies": { 28 | "sails": "^0.12.3", 29 | "graphql": "^0.5.0" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "^6.8.0", 33 | "babel-eslint": "^6.0.4", 34 | "babel-preset-es2015": "^6.6.0", 35 | "babel-preset-stage-0": "^6.5.0", 36 | "eslint-plugin-babel": "^3.2.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/createMutations.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLNonNull, 3 | } from 'graphql'; 4 | 5 | import { 6 | getName, 7 | dataTypes, 8 | } from './helpers'; 9 | 10 | import { 11 | resolveCreate, 12 | resolveDelete, 13 | resolveUpdate, 14 | } from './resolvers'; 15 | 16 | const mutations = {}; 17 | let objectTypes; 18 | 19 | /** 20 | * Create 'create' mutation type 21 | * @param {object} model sails model 22 | */ 23 | function createCreateMutation(model) { 24 | 25 | const { 26 | mutationCreateName, 27 | mutationCreateTypeName, 28 | typeName, 29 | inputTypeName, 30 | queryName 31 | } = getName(model); 32 | 33 | mutations[mutationCreateName] = { 34 | name: mutationCreateTypeName, 35 | type: objectTypes[typeName], 36 | args: { 37 | [queryName]: { 38 | type: new GraphQLNonNull(objectTypes[inputTypeName]) 39 | } 40 | }, 41 | resolve: resolveCreate(model) 42 | }; 43 | 44 | } 45 | 46 | /** 47 | * Create 'delete' mutation type 48 | * @param {object} model sails model 49 | */ 50 | function createDeleteMutation(model) { 51 | 52 | const { 53 | mutationDeleteName, 54 | mutationDeleteTypeName, 55 | typeName 56 | } = getName(model); 57 | 58 | mutations[mutationDeleteName] = { 59 | name: mutationDeleteTypeName, 60 | type: objectTypes[typeName], 61 | args: { 62 | id: { 63 | type: new GraphQLNonNull(dataTypes[model._attributes.id.type]) 64 | } 65 | }, 66 | resolve: resolveDelete(model) 67 | }; 68 | 69 | } 70 | 71 | /** 72 | * Create 'update' mutation type 73 | * @param {object} model sails model 74 | */ 75 | function createUpdateMutation(model) { 76 | 77 | const { 78 | mutationUpdateName, 79 | mutationUpdateTypeName, 80 | typeName, 81 | inputTypeName, 82 | queryName, 83 | } = getName(model); 84 | 85 | mutations[mutationUpdateName] = { 86 | name: mutationUpdateTypeName, 87 | type: objectTypes[typeName], 88 | args: { 89 | id: { 90 | type: new GraphQLNonNull(dataTypes[model._attributes.id.type]) 91 | }, 92 | [queryName]: { 93 | type: new GraphQLNonNull(objectTypes[inputTypeName]) 94 | } 95 | }, 96 | resolve: resolveUpdate(model) 97 | }; 98 | 99 | } 100 | 101 | /** 102 | * Create fields for root mutation type 103 | * @param {object} models sails.models 104 | * @param {object} types predefined GraphQL object types 105 | * @returns {object} fields for root mutation type 106 | */ 107 | export default function createMutation(models, types) { 108 | 109 | objectTypes = types; 110 | 111 | Object.keys(models).forEach((modelName) => { 112 | createCreateMutation(models[modelName]); 113 | createDeleteMutation(models[modelName]); 114 | createUpdateMutation(models[modelName]); 115 | }); 116 | 117 | return mutations; 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/createQueries.js: -------------------------------------------------------------------------------- 1 | import { 2 | getName, 3 | getSingleQueryArgs, 4 | getConnectionType, 5 | connectionArgs, 6 | } from './helpers'; 7 | 8 | import { 9 | resolveGetSingle, 10 | resolveGetRange, 11 | } from './resolvers'; 12 | 13 | const queries = {}; 14 | let objectTypes; 15 | 16 | /** 17 | * Create a query object type 18 | * @param model 19 | */ 20 | function createSingleQuery(model) { 21 | 22 | const { queryName, queryTypeName, typeName } = getName(model); 23 | const args = getSingleQueryArgs(model._attributes); 24 | 25 | queries[queryName] = { 26 | name: queryTypeName, 27 | args, 28 | type: objectTypes[typeName], 29 | resolve: resolveGetSingle(model) 30 | }; 31 | 32 | } 33 | 34 | /** 35 | * Create a query object type for multiple instances 36 | * @param model 37 | */ 38 | function createRangeQuery(model) { 39 | 40 | const { 41 | queryPluralName, 42 | queryPluralTypeName, 43 | typeName, 44 | connectionTypeName 45 | } = getName(model); 46 | 47 | queries[queryPluralName] = { 48 | name: queryPluralTypeName, 49 | type: getConnectionType(connectionTypeName, objectTypes[typeName]), 50 | args: connectionArgs, 51 | resolve: resolveGetRange(model) 52 | }; 53 | 54 | } 55 | 56 | /** 57 | * Populate 'queries' object and return it 58 | * @param models 59 | * @param types 60 | * @returns {object} 61 | */ 62 | export default function createQueries(models, types) { 63 | objectTypes = types; 64 | 65 | Object.keys(models).forEach((modelName) => { 66 | createSingleQuery(models[modelName]); 67 | createRangeQuery(models[modelName]); 68 | }); 69 | 70 | return queries; 71 | } 72 | -------------------------------------------------------------------------------- /src/createTypes.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLInputObjectType, 4 | GraphQLList, 5 | } from 'graphql'; 6 | 7 | import { 8 | getName, 9 | getConnectionType, 10 | supportedTypes, 11 | dataTypes, 12 | connectionArgs, 13 | } from './helpers'; 14 | 15 | import { 16 | resolveGetRange, 17 | } from './resolvers'; 18 | 19 | // Cache of generated Object Types 20 | const objectTypes = {}; 21 | let models; 22 | 23 | /** 24 | * Generate fields for GraphQLObjectType or GraphQLInputObjectType 25 | * for provided sails model's attributes 26 | * @param {object} modelObject Sails model 27 | * @param {boolean} isInputType 28 | * for GraphQLInputType or not 29 | */ 30 | function getObjectFields(modelObject, isInputType = false) { 31 | 32 | const { _attributes: attributes } = modelObject; 33 | const { queryName } = getName(modelObject); 34 | 35 | // Go through all fields and return object of 36 | // converted sails types to GraphQL types 37 | return () => Object.keys(attributes).reduce((fields, fieldName) => { 38 | 39 | const { type, collection, model } = attributes[fieldName]; 40 | 41 | // Field type must be either `type`, `collection` or `model` 42 | const attrType = type || collection || model; 43 | let fieldTypeName; 44 | let childModel; 45 | 46 | // Check whether fieldType was provided 47 | if (!attrType) { 48 | console.error(` 49 | Each field must have either 'type', 'collection' or 'model' 50 | property defined. Field '${fieldName}' was omitted. 51 | `); 52 | return fields; 53 | } 54 | 55 | if (model || collection) { 56 | childModel = models[attrType.toLowerCase()]; 57 | fieldTypeName = isInputType ? 58 | getName(childModel).inputTypeName : 59 | getName(childModel).typeName; 60 | } 61 | 62 | // Check whether field has supported type 63 | if ( 64 | supportedTypes.indexOf(attrType) === -1 && 65 | !objectTypes[fieldTypeName] 66 | ) { 67 | console.error(` 68 | Field '${fieldName}' has unsupported type 69 | '${attrType}' and was omitted. 70 | `); 71 | return fields; 72 | } 73 | 74 | const fieldType = dataTypes[attrType] || objectTypes[fieldTypeName]; 75 | 76 | fields[fieldName] = {}; 77 | 78 | if (type || model) { 79 | fields[fieldName].type = fieldType; 80 | return fields; 81 | } 82 | 83 | if (isInputType && collection) { 84 | fields[fieldName].type = new GraphQLList(fieldType); 85 | return fields; 86 | } 87 | 88 | const { fieldConnectionTypeName } = getName(modelObject, fieldName); 89 | 90 | fields[fieldName] = { 91 | type: getConnectionType(fieldConnectionTypeName, fieldType), 92 | args: connectionArgs, 93 | // Getting the field's model 94 | resolve: resolveGetRange(childModel, queryName) 95 | }; 96 | 97 | return fields; 98 | 99 | }, {}); 100 | } 101 | 102 | /** 103 | * Create a GraphQLObjectType for provided sails model 104 | * @param {object} model sails model 105 | */ 106 | function createOutputType(model) { 107 | 108 | // Define output type name 109 | const { typeName } = getName(model); 110 | 111 | objectTypes[typeName] = new GraphQLObjectType({ 112 | name: typeName, 113 | fields: getObjectFields(model) 114 | }); 115 | 116 | } 117 | 118 | /** 119 | * Create a GraphQLInputObjectType for sails model 120 | * @param {object} model sails model 121 | */ 122 | function createInputType(model) { 123 | 124 | // Define output type name 125 | const { inputTypeName } = getName(model); 126 | 127 | objectTypes[inputTypeName] = new GraphQLInputObjectType({ 128 | name: inputTypeName, 129 | fields: getObjectFields(model, true) 130 | }); 131 | 132 | } 133 | 134 | /** 135 | * Return an object of GraphQLObjectType and GraphQLInputObjectType 136 | * for each given sails model 137 | * @param {object} sailsModels sails.models 138 | * @returns {object} Output and Input object types 139 | */ 140 | export default function createTypes(sailsModels) { 141 | models = sailsModels; 142 | 143 | Object.keys(models).forEach((modelName) => { 144 | createOutputType(models[modelName]); 145 | createInputType(models[modelName]); 146 | }); 147 | 148 | return objectTypes; 149 | } 150 | -------------------------------------------------------------------------------- /src/formatErrors.js: -------------------------------------------------------------------------------- 1 | export default function formatError(defaultErrors) { 2 | 3 | return defaultErrors.reduce((formattedErrors, { originalError }) => { 4 | 5 | const { body } = originalError; 6 | const { 7 | code, 8 | status, 9 | reason: message, 10 | model, 11 | invalidAttributes: errors 12 | } = body; 13 | 14 | const processedError = { 15 | code, 16 | status: (code === 'E_VALIDATION' ? 422 : status), 17 | message, 18 | model, 19 | validationErrors: {} 20 | }; 21 | 22 | if (errors) { 23 | processedError.validationErrors = Object.keys(errors).reduce(( 24 | validationErrors, 25 | fieldName 26 | ) => { 27 | 28 | if (!validationErrors[fieldName]) { 29 | validationErrors[fieldName] = []; 30 | } 31 | 32 | errors[fieldName].forEach((field) => { 33 | const { rule } = field; 34 | validationErrors[fieldName].push(`${model}.${rule}`); 35 | }); 36 | 37 | return validationErrors; 38 | 39 | }, {}); 40 | 41 | } 42 | 43 | formattedErrors.push(processedError); 44 | 45 | return formattedErrors; 46 | 47 | }, []); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/generateSchema.js: -------------------------------------------------------------------------------- 1 | import createTypes from './createTypes'; 2 | import createQueries from './createQueries'; 3 | import createMutations from './createMutations'; 4 | 5 | import { 6 | GraphQLObjectType, 7 | GraphQLSchema, 8 | } from 'graphql'; 9 | 10 | /** 11 | * Generate GraphQLSchema for provided sails.models 12 | * @param {object} sailsModels sails.models object 13 | */ 14 | export function generateSchema(sailsModels) { 15 | const types = createTypes(sailsModels); 16 | const queries = createQueries(sailsModels, types); 17 | const mutations = createMutations(sailsModels, types); 18 | 19 | return new GraphQLSchema({ 20 | query: new GraphQLObjectType({ 21 | name: 'RootQueryType', 22 | fields: () => queries 23 | }), 24 | mutation: new GraphQLObjectType({ 25 | name: 'RootMutationType', 26 | fields: () => mutations 27 | }) 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | import pluralize from 'pluralize'; 2 | 3 | import { 4 | GraphQLInt, 5 | GraphQLList, 6 | GraphQLObjectType, 7 | GraphQLBoolean, 8 | GraphQLFloat, 9 | GraphQLID, 10 | GraphQLString, 11 | } from 'graphql'; 12 | 13 | // Sails to GraphQL types conversion 14 | export const dataTypes = { 15 | string: GraphQLString, 16 | text: GraphQLString, 17 | integer: GraphQLInt, 18 | float: GraphQLFloat, 19 | date: GraphQLString, 20 | datetime: GraphQLString, 21 | boolean: GraphQLBoolean, 22 | objectid: GraphQLID 23 | }; 24 | 25 | export const supportedTypes = Object.keys(dataTypes); 26 | 27 | // Defining query args 28 | // Disabling max-len rule due to long docs link 29 | /* eslint-disable max-len */ 30 | export const connectionArgs = { 31 | // `where` is JSON-like string 32 | // used for filtering data 33 | // where: "{ 34 | // \"someProp\": \"someValue\", 35 | // \"otherProp\": { 36 | // \"contains\": \"a\" 37 | // } 38 | // }" 39 | // For full reference see 40 | // https://github.com/balderdashy/waterline-docs/blob/master/queries/query-language.md 41 | where: { type: GraphQLString }, 42 | limit: { type: GraphQLInt }, 43 | skip: { type: GraphQLInt }, 44 | sort: { type: GraphQLString } 45 | }; 46 | 47 | /** 48 | * Make the first letter of the `string` upper cased 49 | * @param {string} string 50 | * @returns {string} 51 | */ 52 | export function firstLetterToUpperCase(string) { 53 | return string.charAt(0).toUpperCase() + string.slice(1); 54 | } 55 | 56 | /** 57 | * Make the first letter of the `string` lower cased 58 | * @param {string} string 59 | * @returns {string} 60 | */ 61 | export function firstLetterToLowerCase(string) { 62 | return string.charAt(0).toLowerCase() + string.slice(1); 63 | } 64 | 65 | /** 66 | * Generate Type and Query names from a model name 67 | * @param {object} model Sails model object 68 | * @param {string} [fieldKey] 69 | * @returns {object} 70 | */ 71 | export function getName(model, fieldKey = '') { 72 | 73 | // Get model globalId to have original modal name (with correct case) 74 | // Basically it should start with upper cased letter 75 | const modelTypeName = model.globalId; 76 | 77 | // Make sure the first letter of field name is upper cased 78 | const fieldName = firstLetterToUpperCase(fieldKey); 79 | 80 | // Will be used as GraphQL query field identifier 81 | // Basically it should start with lower cased letter 82 | const modelName = firstLetterToLowerCase(modelTypeName); 83 | 84 | // TODO make an ability to customize this names 85 | // (for example from model definition file) 86 | return { 87 | typeName: `${modelTypeName}Type`, 88 | inputTypeName: `${modelTypeName}InputType`, 89 | connectionTypeName: `${modelTypeName}ConnectionType`, 90 | fieldConnectionTypeName: `${modelTypeName}${fieldName}ConnectionType`, 91 | fieldUnionTypeName: `${modelTypeName}${fieldName}UnionType`, 92 | queryName: `${modelName}`, 93 | queryPluralName: `${pluralize(modelName)}`, 94 | queryTypeName: `${modelTypeName}Query`, 95 | queryPluralTypeName: `${modelTypeName}RangeQuery`, 96 | mutationCreateName: `create${modelTypeName}`, 97 | mutationCreateTypeName: `Create${modelTypeName}Mutation`, 98 | mutationDeleteName: `delete${modelTypeName}`, 99 | mutationDeleteTypeName: `Delete${modelTypeName}Mutation`, 100 | mutationUpdateName: `update${modelTypeName}`, 101 | mutationUpdateTypeName: `Update${modelTypeName}Mutation` 102 | }; 103 | } 104 | 105 | /** 106 | * Create connection type with predefined field 107 | * @param {string} name 108 | * @param {GraphQLObjectType} edgesType 109 | */ 110 | export function getConnectionType(name, edgesType) { 111 | return new GraphQLObjectType({ 112 | name, 113 | fields: () => ({ 114 | page: { type: GraphQLInt }, 115 | pages: { type: GraphQLInt }, 116 | perPage: { type: GraphQLInt }, 117 | total: { type: GraphQLInt }, 118 | edges: { type: new GraphQLList(edgesType) } 119 | }) 120 | }); 121 | } 122 | 123 | /** 124 | * Return an array of model unique fields' names 125 | * @param attributes 126 | * @returns {Array.} 127 | */ 128 | export function getUniqueFields(attributes) { 129 | return Object.keys(attributes).filter((fieldName) => { 130 | return attributes[fieldName].unique === true; 131 | }); 132 | } 133 | 134 | /** 135 | * Create an args object for sinqle query 136 | * @param attributes 137 | */ 138 | export function getSingleQueryArgs(attributes) { 139 | 140 | const uniqueFields = getUniqueFields(attributes); 141 | 142 | return uniqueFields.reduce((args, fieldName) => { 143 | args[fieldName] = { 144 | type: dataTypes[attributes[fieldName].type] 145 | }; 146 | return args; 147 | }, {}); 148 | } 149 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | generateSchema, 3 | } from './generateSchema'; 4 | 5 | export formatErrors from './formatErrors'; 6 | -------------------------------------------------------------------------------- /src/resolvers.js: -------------------------------------------------------------------------------- 1 | import { 2 | getName, 3 | } from './helpers'; 4 | 5 | export function resolveGetRange(model, parentFieldName = '') { 6 | 7 | const modelIdentity = model.identity; 8 | 9 | return ( 10 | rootValue, 11 | { where = '{}', limit = 100, skip = 0, sort = '' }, 12 | { request, reqData = {} } 13 | ) => new Promise((resolve, reject) => { 14 | 15 | let whereWithParentId; 16 | 17 | if (rootValue && rootValue.id) { 18 | 19 | whereWithParentId = Object.assign(JSON.parse(where), { 20 | [parentFieldName]: rootValue.id 21 | }); 22 | 23 | whereWithParentId = JSON.stringify(whereWithParentId); 24 | 25 | } 26 | 27 | request({ 28 | method: 'GET', 29 | url: `/${modelIdentity}/find`, 30 | ...reqData 31 | }, { 32 | where: whereWithParentId || where, 33 | limit, 34 | skip, 35 | sort 36 | }, (err1, res) => { 37 | if (err1) { 38 | return reject(err1); 39 | } 40 | 41 | model.count(JSON.parse(where), (err2, count) => { 42 | if (err2) { 43 | return reject(err2); 44 | } 45 | 46 | resolve({ 47 | page: skip / limit + 1, 48 | pages: Math.ceil(count / limit), 49 | perPage: limit, 50 | total: count, 51 | edges: res.body 52 | }); 53 | }); 54 | }); 55 | 56 | }); 57 | 58 | } 59 | 60 | export function resolveGetSingle(model) { 61 | 62 | const modelIdentity = model.identity; 63 | 64 | return ( 65 | rootValue, 66 | args, 67 | { request, reqData = {} } 68 | ) => new Promise((resolve, reject) => { 69 | request({ 70 | method: 'GET', 71 | url: `/${modelIdentity}/find`, 72 | ...reqData 73 | }, args, (err, res) => { 74 | if (err) { 75 | return reject(err); 76 | } 77 | if (res.body instanceof Array) { 78 | res.body = res[0]; 79 | } 80 | resolve(res.body); 81 | }); 82 | }); 83 | 84 | } 85 | 86 | export function resolveCreate(model) { 87 | 88 | const modelIdentity = model.identity; 89 | const { queryName } = getName(model); 90 | 91 | return ( 92 | rootValue, 93 | args, 94 | { request, reqData = {} } 95 | ) => new Promise((resolve, reject) => { 96 | request({ 97 | method: 'POST', 98 | url: `/${modelIdentity}/create`, 99 | ...reqData 100 | }, args[queryName], (err, res) => { 101 | if (err) { 102 | return reject(err); 103 | } 104 | resolve(res.body); 105 | }); 106 | }); 107 | 108 | } 109 | 110 | export function resolveDelete(model) { 111 | 112 | const modelIdentity = model.identity; 113 | 114 | return ( 115 | rootValue, 116 | { id }, 117 | { request, reqData = {} } 118 | ) => new Promise((resolve, reject) => { 119 | request({ 120 | method: 'DELETE', 121 | url: `/${modelIdentity}/${id}`, 122 | ...reqData 123 | }, (err, res) => { 124 | if (err) { 125 | return reject(err); 126 | } 127 | resolve(res.body); 128 | }); 129 | }); 130 | 131 | } 132 | 133 | export function resolveUpdate(model) { 134 | 135 | const modelIdentity = model.identity; 136 | const { queryName } = getName(model); 137 | 138 | return ( 139 | rootValue, 140 | args, 141 | { request, reqData = {} } 142 | ) => new Promise((resolve, reject) => { 143 | request({ 144 | method: 'PUT', 145 | url: `/${modelIdentity}/${args.id}`, 146 | ...reqData 147 | }, args[queryName] , (err, res) => { 148 | if (err) { 149 | return reject(err); 150 | } 151 | resolve(res.body); 152 | }); 153 | }); 154 | 155 | } 156 | --------------------------------------------------------------------------------