├── .babelrc ├── .eslintrc ├── .github └── stale.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── docs └── relay.md ├── examples └── graphql-yoga │ ├── .babelrc │ ├── .gitignore │ ├── .node-version │ ├── .nvmrc │ ├── README.md │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── src │ ├── config │ └── database.js │ ├── index.js │ ├── models │ ├── Pet.js │ ├── User.js │ └── index.js │ └── server.js ├── package-lock.json ├── package.json ├── pre-commit ├── scripts └── mocha-bootload ├── src ├── argsToFindOptions.js ├── attributeFields.js ├── base64.js ├── defaultArgs.js ├── defaultListArgs.js ├── index.js ├── relay.js ├── replaceWhereOperators.js ├── resolver.js ├── sequelizeOps.js ├── simplifyAST.js ├── typeMapper.js └── types │ ├── dateType.js │ └── jsonType.js └── test ├── benchmark.js ├── benchmark ├── hasManyWhere.json ├── models.js ├── nestedBelongsToMany.json ├── nestedBelongsToManyLimit.json ├── nestedBelongsToManyWhere.json ├── nestedHasMany.json ├── schema.js ├── seed.js ├── singleBelongsTo.json ├── singleBelongsToMany.json ├── singleBelongsToManyLimit.json ├── singleHasMany.json └── twoHasMany.json ├── integration ├── relay.test.js ├── relay │ └── connection.test.js └── resolver.test.js ├── support └── helper.js └── unit ├── argsToFindOptions.test.js ├── attributeFields.test.js ├── defaultArgs.test.js ├── defaultListArgs.test.js ├── relay ├── connection.test.js └── mutation.test.js ├── replaceWhereOperators.test.js ├── simplifyAST.test.js └── typeMapper.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-object-rest-spread"], 3 | "presets": [ 4 | "async-to-bluebird", 5 | "es2015-node4" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "arrowFunctions": true, 4 | "blockBindings": true, 5 | "classes": true, 6 | "defaultParams": true, 7 | "destructuring": true, 8 | "forOf": true, 9 | "generators": true, 10 | "modules": true, 11 | "objectLiteralComputedProperties": true, 12 | "objectLiteralShorthandMethods": true, 13 | "objectLiteralShorthandProperties": true, 14 | "spread": true, 15 | "templateStrings": true, 16 | "env": { 17 | "node": true, 18 | "es6": true 19 | }, 20 | "rules": { 21 | "comma-dangle": 0, 22 | "no-cond-assign": 2, 23 | "no-console": 0, 24 | "no-constant-condition": 2, 25 | "no-control-regex": 0, 26 | "no-debugger": 0, 27 | "no-dupe-args": 2, 28 | "no-dupe-keys": 2, 29 | "no-duplicate-case": 2, 30 | "no-empty": 2, 31 | "no-empty-character-class": 2, 32 | "no-ex-assign": 2, 33 | "no-extra-boolean-cast": 2, 34 | "no-extra-semi": 2, 35 | "no-func-assign": 2, 36 | "no-inner-declarations": [ 37 | 2, 38 | "functions" 39 | ], 40 | "no-invalid-regexp": 2, 41 | "no-irregular-whitespace": 2, 42 | "no-negated-in-lhs": 2, 43 | "no-obj-calls": 2, 44 | "no-regex-spaces": 2, 45 | "no-reserved-keys": 0, 46 | "no-sparse-arrays": 2, 47 | "no-unreachable": 2, 48 | "use-isnan": 2, 49 | "valid-jsdoc": 0, 50 | "valid-typeof": 2, 51 | "block-scoped-var": 0, 52 | "complexity": 0, 53 | "consistent-return": 0, 54 | "default-case": 0, 55 | "dot-notation": 0, 56 | "eqeqeq": 2, 57 | "guard-for-in": 2, 58 | "no-alert": 2, 59 | "no-caller": 2, 60 | "no-div-regex": 2, 61 | "no-empty-label": 2, 62 | "no-eq-null": 0, 63 | "no-eval": 2, 64 | "no-extend-native": 2, 65 | "no-extra-bind": 2, 66 | "no-fallthrough": 2, 67 | "no-floating-decimal": 2, 68 | "no-implied-eval": 2, 69 | "no-iterator": 2, 70 | "no-labels": 0, 71 | "no-lone-blocks": 0, 72 | "no-loop-func": 0, 73 | "no-multi-spaces": 2, 74 | "no-multi-str": 2, 75 | "no-native-reassign": 0, 76 | "no-new": 2, 77 | "no-new-func": 0, 78 | "no-new-wrappers": 2, 79 | "no-octal": 2, 80 | "no-octal-escape": 2, 81 | "no-param-reassign": 0, 82 | "no-process-env": 0, 83 | "no-proto": 2, 84 | "no-redeclare": 2, 85 | "no-return-assign": 2, 86 | "no-script-url": 2, 87 | "no-self-compare": 0, 88 | "no-sequences": 2, 89 | "no-throw-literal": 2, 90 | "no-unused-expressions": 0, 91 | "no-void": 2, 92 | "no-warning-comments": 0, 93 | "no-with": 2, 94 | "radix": 2, 95 | "vars-on-top": 0, 96 | "wrap-iife": 2, 97 | "yoda": [ 98 | 2, 99 | "never", 100 | { 101 | "exceptRange": true 102 | } 103 | ], 104 | "strict": 0, 105 | "no-catch-shadow": 2, 106 | "no-delete-var": 2, 107 | "no-label-var": 2, 108 | "no-shadow": 0, 109 | "no-shadow-restricted-names": 2, 110 | "no-undef": 2, 111 | "no-undef-init": 2, 112 | "no-undefined": 0, 113 | "no-unused-vars": [ 114 | 2, 115 | { 116 | "vars": "all", 117 | "args": "after-used" 118 | } 119 | ], 120 | "no-use-before-define": 0, 121 | "handle-callback-err": [ 122 | 2, 123 | "error" 124 | ], 125 | "no-mixed-requires": [ 126 | 2, 127 | true 128 | ], 129 | "no-new-require": 2, 130 | "no-path-concat": 2, 131 | "no-process-exit": 0, 132 | "no-restricted-modules": 0, 133 | "curly": 0, 134 | "no-sync": 2, 135 | "indent": [ 136 | 2, 137 | 2, 138 | { 139 | "SwitchCase": 2 140 | } 141 | ], 142 | "brace-style": [ 143 | 2, 144 | "1tbs", 145 | { 146 | "allowSingleLine": true 147 | } 148 | ], 149 | "camelcase": [ 150 | 2, 151 | { 152 | "properties": "always" 153 | } 154 | ], 155 | "comma-spacing": 0, 156 | "comma-style": 0, 157 | "consistent-this": 0, 158 | "eol-last": 2, 159 | "func-names": 0, 160 | "func-style": 0, 161 | "key-spacing": [ 162 | 2, 163 | { 164 | "beforeColon": false, 165 | "afterColon": true 166 | } 167 | ], 168 | "max-nested-callbacks": 0, 169 | "new-cap": 0, 170 | "new-parens": 2, 171 | "newline-after-var": 0, 172 | "no-array-constructor": 2, 173 | "no-inline-comments": 0, 174 | "no-lonely-if": 2, 175 | "no-mixed-spaces-and-tabs": 2, 176 | "no-multiple-empty-lines": 0, 177 | "no-nested-ternary": 0, 178 | "no-new-object": 2, 179 | "no-spaced-func": 2, 180 | "no-ternary": 0, 181 | "no-trailing-spaces": 2, 182 | "no-underscore-dangle": 0, 183 | "no-extra-parens": 2, 184 | "one-var": 0, 185 | "operator-assignment": [ 186 | 2, 187 | "always" 188 | ], 189 | "padded-blocks": 0, 190 | "quote-props": [ 191 | 2, 192 | "as-needed" 193 | ], 194 | "quotes": [ 195 | 2, 196 | "single" 197 | ], 198 | "semi": [ 199 | 2, 200 | "always" 201 | ], 202 | "semi-spacing": [ 203 | 2, 204 | { 205 | "before": false, 206 | "after": true 207 | } 208 | ], 209 | "sort-vars": 0, 210 | "space-after-keywords": [ 211 | 2, 212 | "always" 213 | ], 214 | "space-before-blocks": [ 215 | 2, 216 | "always" 217 | ], 218 | "space-before-function-paren": [ 219 | 2, 220 | { 221 | "anonymous": "always", 222 | "named": "never" 223 | } 224 | ], 225 | "space-in-brackets": 0, 226 | "space-in-parens": 0, 227 | "space-infix-ops": [ 228 | 2, 229 | { 230 | "int32Hint": false 231 | } 232 | ], 233 | "space-return-throw-case": 2, 234 | "space-unary-ops": [ 235 | 2, 236 | { 237 | "words": true, 238 | "nonwords": false 239 | } 240 | ], 241 | "spaced-comment": [ 242 | 2, 243 | "always" 244 | ], 245 | "wrap-regex": 0, 246 | "no-var": 0, 247 | "max-len": [2, 130, 4] 248 | }, 249 | "globals": { 250 | "it": false, 251 | "afterEach": false, 252 | "beforeEach": false, 253 | "before": false, 254 | "after": false, 255 | "describe": false, 256 | "expect": false, 257 | "assert": false 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | #staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | .idea 11 | .DS_Store 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | lib 32 | coverage/ 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | src 29 | test 30 | scripts 31 | 32 | # Docker/tests 33 | Dockerfile 34 | docker-compose.yml 35 | 36 | # Git 37 | pre-commit 38 | 39 | # Examples / Docs 40 | exampels 41 | README.md 42 | docs 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - "6" 6 | - "8" 7 | - "10" 8 | - "node" 9 | 10 | cache: 11 | directories: 12 | - node_modules 13 | 14 | env: 15 | - DIALECT=mysql 16 | - DIALECT=postgres 17 | 18 | services: 19 | - mysql 20 | addons: 21 | postgresql: "9.4" 22 | 23 | before_script: 24 | - if [[ "$DIALECT" == "postgres" ]]; then psql -c "drop database if exists test;" -U postgres; fi 25 | - if [[ "$DIALECT" == "postgres" ]]; then psql -c "create database test;" -U postgres; fi 26 | - if [[ "$DIALECT" == "mysql" ]]; then mysql -e "create database IF NOT EXISTS test;" -uroot; fi 27 | 28 | script: 29 | - "npm run lint && npm run cover" 30 | - "bash <(curl -s https://codecov.io/bash) -f coverage/lcov.info" 31 | 32 | notifications: 33 | email: false -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:4.5 2 | 3 | RUN mkdir -p /src/graphql-sequelize 4 | WORKDIR /src/graphql-sequelize 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mick Hansen 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-sequelize 2 | 3 | [![NPM](https://img.shields.io/npm/v/graphql-sequelize.svg)](https://www.npmjs.com/package/graphql-sequelize) 4 | [![Build Status](https://travis-ci.org/mickhansen/graphql-sequelize.svg?branch=master)](https://travis-ci.org/mickhansen/graphql-sequelize) 5 | [![Slack](http://sequelize-slack.herokuapp.com/badge.svg)](http://sequelize-slack.herokuapp.com) 6 | [![Coverage](https://codecov.io/gh/mickhansen/graphql-sequelize/branch/master/graph/badge.svg)](https://codecov.io/gh/mickhansen/graphql-sequelize) 7 | 8 | Should be used with [dataloader-sequelize](https://github.com/mickhansen/dataloader-sequelize) to avoid N+1 queries 9 | 10 | - [Installation](#installation) 11 | - [Resolve helpers](#resolve-helpers) 12 | - [field helpers](#field-helpers) 13 | - [args helpers](#args-helpers) 14 | 15 | ## Installation 16 | 17 | `$ npm install --save graphql-sequelize` 18 | 19 | graphql-sequelize assumes you have graphql and sequelize installed. 20 | 21 | ## Resolve helpers 22 | 23 | ```js 24 | import { resolver } from "graphql-sequelize"; 25 | 26 | resolver(SequelizeModel[, options]); 27 | ``` 28 | 29 | A helper for resolving graphql queries targeted at Sequelize models or associations. 30 | Please take a look at [the tests](https://github.com/mickhansen/graphql-sequelize/blob/master/test/integration/resolver.test.js) to best get an idea of implementation. 31 | 32 | ### Features 33 | 34 | - Automatically converts args to where if arg keys matches model attributes 35 | - Automatically converts an arg named 'limit' to a sequelize limit 36 | - Automatically converts an arg named 'order' to a sequelize order 37 | 38 | ### Relay & Connections 39 | 40 | [Relay documentation](docs/relay.md) 41 | 42 | ### Options 43 | 44 | The `resolver` function takes a model as its first (required) argument, but also 45 | has a second options object argument. The available options are: 46 | 47 | ```js 48 | resolver(SequelizeModel, { 49 | // Whether or not this should return a list. Defaults to whether or not the 50 | // field type is an instance of `GraphQLList`. 51 | list: false, 52 | 53 | // Whether or not relay connections should be handled. Defaults to `true`. 54 | handleConnection: true, 55 | 56 | /** 57 | * Manipulate the query before it's sent to Sequelize. 58 | * @param findOptions {object} - Options sent to Seqeulize model's find function 59 | * @param args {object} - The arguments from the incoming GraphQL query 60 | * @param context {object} - Resolver context, see more at GraphQL docs below. 61 | * @returns findOptions or promise that resolves with findOptions 62 | */ 63 | before: (findOptions, args, context) => { 64 | findOptions.where = { /* Custom where arguments */ }; 65 | return findOptions; 66 | }, 67 | /** 68 | * Manipulate the Sequelize find results before it's sent back to the requester. 69 | * @param result {object|array} - Result of the query, object or array depending on list or not. 70 | * @param args {object} - The arguments from the incoming GraphQL query 71 | * @param context {object} - Resolver context, see more at GraphQL docs below. 72 | * @returns result(s) or promise that resolves with result(s) 73 | */ 74 | after: (result, args, context) => { 75 | result.sort(/* Custom sort function */); 76 | return result; 77 | }, 78 | 79 | /* 80 | * Transfer fields from the graphql context to the options passed to model calls 81 | * Inherits from global resolver.contextToOptions 82 | */ 83 | contextToOptions: { 84 | a: 'a', 85 | b: 'c' 86 | } 87 | }); 88 | 89 | resolver.contextToOptions = {}; /* Set contextToOptions globally */ 90 | ``` 91 | 92 | _The `args` and `context` parameters are provided by GraphQL. More information 93 | about those is available in their [resolver docs](http://graphql.org/learn/execution/#root-fields-resolvers)._ 94 | 95 | ### Examples 96 | 97 | ```js 98 | import {resolver} from 'graphql-sequelize'; 99 | 100 | let User = sequelize.define('user', { 101 | name: Sequelize.STRING 102 | }); 103 | 104 | let Task = sequelize.define('task', { 105 | title: Sequelize.STRING 106 | }); 107 | 108 | User.Tasks = User.hasMany(Task, {as: 'tasks'}); 109 | 110 | let taskType = new GraphQLObjectType({ 111 | name: 'Task', 112 | description: 'A task', 113 | fields: { 114 | id: { 115 | type: new GraphQLNonNull(GraphQLInt), 116 | description: 'The id of the task.', 117 | }, 118 | title: { 119 | type: GraphQLString, 120 | description: 'The title of the task.', 121 | } 122 | } 123 | }); 124 | 125 | let userType = new GraphQLObjectType({ 126 | name: 'User', 127 | description: 'A user', 128 | fields: { 129 | id: { 130 | type: new GraphQLNonNull(GraphQLInt), 131 | description: 'The id of the user.', 132 | }, 133 | name: { 134 | type: GraphQLString, 135 | description: 'The name of the user.', 136 | }, 137 | tasks: { 138 | type: new GraphQLList(taskType), 139 | resolve: resolver(User.Tasks) 140 | } 141 | } 142 | }); 143 | 144 | let schema = new GraphQLSchema({ 145 | query: new GraphQLObjectType({ 146 | name: 'RootQueryType', 147 | fields: { 148 | // Field for retrieving a user by ID 149 | user: { 150 | type: userType, 151 | // args will automatically be mapped to `where` 152 | args: { 153 | id: { 154 | description: 'id of the user', 155 | type: new GraphQLNonNull(GraphQLInt) 156 | } 157 | }, 158 | resolve: resolver(User) 159 | }, 160 | 161 | // Field for searching for a user by name 162 | userSearch: { 163 | type: new GraphQLList(userType), 164 | args: { 165 | query: { 166 | description: "Fuzzy-matched name of user", 167 | type: new GraphQLNonNull(GraphQLString), 168 | } 169 | }, 170 | resolve: resolver(User, { 171 | // Custom `where` clause that fuzzy-matches user's name and 172 | // alphabetical sort by username 173 | before: (findOptions, args) => { 174 | findOptions.where = { 175 | name: { "$like": `%${args.query}%` }, 176 | }; 177 | findOptions.order = [['name', 'ASC']]; 178 | return findOptions; 179 | }, 180 | // Custom sort override for exact matches first 181 | after: (results, args) => { 182 | return results.sort((a, b) => { 183 | if (a.name === args.query) { 184 | return 1; 185 | } 186 | else if (b.name === args.query) { 187 | return -1; 188 | } 189 | 190 | return 0; 191 | }); 192 | } 193 | }) 194 | } 195 | } 196 | }) 197 | }); 198 | 199 | let schema = new GraphQLSchema({ 200 | query: new GraphQLObjectType({ 201 | name: 'RootQueryType', 202 | fields: { 203 | users: { 204 | // The resolver will use `findOne` or `findAll` depending on whether the field it's used in is a `GraphQLList` or not. 205 | type: new GraphQLList(userType), 206 | args: { 207 | // An arg with the key limit will automatically be converted to a limit on the target 208 | limit: { 209 | type: GraphQLInt 210 | }, 211 | // An arg with the key order will automatically be converted to a order on the target 212 | order: { 213 | type: GraphQLString 214 | } 215 | }, 216 | resolve: resolver(User) 217 | } 218 | } 219 | }) 220 | }); 221 | ``` 222 | 223 | ## field helpers 224 | 225 | field helpers help you automatically define a models attributes as fields for a GraphQL object type. 226 | 227 | ```js 228 | var Model = sequelize.define('User', { 229 | email: { 230 | type: Sequelize.STRING, 231 | allowNull: false 232 | }, 233 | firstName: { 234 | type: Sequelize.STRING 235 | }, 236 | lastName: { 237 | type: Sequelize.STRING 238 | } 239 | }); 240 | 241 | import {attributeFields} from 'graphql-sequelize'; 242 | 243 | attributeFields(Model, { 244 | // ... options 245 | exclude: Array, // array of model attributes to ignore - default: [] 246 | only: Array, // only generate definitions for these model attributes - default: null 247 | globalId: Boolean, // return an relay global id field - default: false 248 | map: Object, // rename fields - default: {} 249 | allowNull: Boolean, // disable wrapping mandatory fields in `GraphQLNonNull` - default: false 250 | commentToDescription: Boolean, // convert model comment to GraphQL description - default: false 251 | cache: Object, // Cache enum types to prevent duplicate type name error - default: {} 252 | }); 253 | 254 | /* 255 | { 256 | id: { 257 | type: new GraphQLNonNull(GraphQLInt) 258 | }, 259 | email: { 260 | type: new GraphQLNonNull(GraphQLString) 261 | }, 262 | firstName: { 263 | type: GraphQLString 264 | }, 265 | lastName: { 266 | type: GraphQLString 267 | } 268 | } 269 | */ 270 | 271 | userType = new GraphQLObjectType({ 272 | name: 'User', 273 | description: 'A user', 274 | fields: Object.assign(attributeFields(Model), { 275 | // ... extra fields 276 | }) 277 | }); 278 | ``` 279 | ### Providing custom types 280 | 281 | `attributeFields` uses the graphql-sequelize `typeMapper` to map Sequelize types to GraphQL types. You can supply your own 282 | mapping function to override this behavior using the `mapType` export. 283 | 284 | ```js 285 | var Model = sequelize.define('User', { 286 | email: { 287 | type: Sequelize.STRING, 288 | allowNull: false 289 | }, 290 | isValid: { 291 | type: Sequelize.BOOLEAN, 292 | allowNull: false 293 | } 294 | }); 295 | 296 | import {attributeFields,typeMapper} from 'graphql-sequelize'; 297 | typeMapper.mapType((type) => { 298 | //map bools as strings 299 | if (type instanceof Sequelize.BOOLEAN) { 300 | return GraphQLString 301 | } 302 | //use default for everything else 303 | return false 304 | }); 305 | 306 | //map fields 307 | attributeFields(Model); 308 | 309 | /* 310 | { 311 | id: { 312 | type: new GraphQLNonNull(GraphQLInt) 313 | }, 314 | email: { 315 | type: new GraphQLNonNull(GraphQLString) 316 | }, 317 | isValid: { 318 | type: new GraphQLNonNull(GraphQLString) 319 | }, 320 | } 321 | */ 322 | 323 | ``` 324 | 325 | ### Renaming generated fields 326 | 327 | attributeFields accepts a ```map``` option to customize the way the attribute fields are named. The ```map``` option accepts 328 | an object or a function that returns a string. 329 | 330 | ```js 331 | 332 | var Model = sequelize.define('User', { 333 | email: { 334 | type: Sequelize.STRING, 335 | allowNull: false 336 | }, 337 | firstName: { 338 | type: Sequelize.STRING 339 | }, 340 | lastName: { 341 | type: Sequelize.STRING 342 | } 343 | }); 344 | 345 | attributeFields(Model, { 346 | map:{ 347 | email:"Email", 348 | firstName:"FirstName", 349 | lastName:"LastName" 350 | } 351 | }); 352 | 353 | /* 354 | { 355 | id: { 356 | type: new GraphQLNonNull(GraphQLInt) 357 | }, 358 | Email: { 359 | type: new GraphQLNonNull(GraphQLString) 360 | }, 361 | FirstName: { 362 | type: GraphQLString 363 | }, 364 | LastName: { 365 | type: GraphQLString 366 | } 367 | } 368 | */ 369 | 370 | attributeFields(Model, { 371 | map:(k) => k.toLowerCase() 372 | }); 373 | 374 | /* 375 | { 376 | id: { 377 | type: new GraphQLNonNull(GraphQLInt) 378 | }, 379 | email: { 380 | type: new GraphQLNonNull(GraphQLString) 381 | }, 382 | firstname: { 383 | type: GraphQLString 384 | }, 385 | lastname: { 386 | type: GraphQLString 387 | } 388 | } 389 | */ 390 | 391 | ``` 392 | 393 | ### ENUM attributes with non-alphanumeric characters 394 | 395 | GraphQL enum types [only support ASCII alphanumeric characters, digits and underscores with leading non-digit](https://facebook.github.io/graphql/#Name). 396 | If you have other characters, like a dash (`-`) in your Sequelize enum types, 397 | they will be converted to camelCase. If your enum value starts from a digit, it 398 | will be prepended with an underscore. 399 | 400 | For example: 401 | 402 | - `foo-bar` becomes `fooBar` 403 | 404 | - `25.8` becomes `_258` 405 | 406 | ### VIRTUAL attributes and GraphQL fields 407 | 408 | If you have `Sequelize.VIRTUAL` attributes on your sequelize model, you need to explicitly set the return type and any field dependencies via `new Sequelize.VIRTUAL(returnType, [dependencies ... ])`. 409 | 410 | For example, `fullName` here will not always return valid data when queried via GraphQL: 411 | ```js 412 | firstName: { type: Sequelize.STRING }, 413 | lastName: { type: Sequelize.STRING }, 414 | fullName: { 415 | type: Sequelize.VIRTUAL, 416 | get: function() { return `${this.firstName} ${this.lastName}`; }, 417 | }, 418 | ``` 419 | 420 | To work properly `fullName` needs to be more fully specified: 421 | 422 | ```js 423 | firstName: { type: Sequelize.STRING }, 424 | lastName: { type: Sequelize.STRING }, 425 | fullName: { 426 | type: new Sequelize.VIRTUAL(Sequelize.STRING, ['firstName', 'lastName']), 427 | get: function() { return `${this.firstName} ${this.lastName}`; }, 428 | }, 429 | ``` 430 | 431 | ## args helpers 432 | 433 | ### defaultArgs 434 | 435 | `defaultArgs(Model)` will return an object containing an arg with a key and type matching your models primary key and 436 | the "where" argument for passing complex query operations described [here](http://docs.sequelizejs.com/en/latest/docs/querying/) 437 | 438 | ```js 439 | var Model = sequelize.define('User', { 440 | 441 | }); 442 | 443 | defaultArgs(Model); 444 | 445 | /* 446 | { 447 | id: { 448 | type: new GraphQLNonNull(GraphQLInt) 449 | } 450 | } 451 | */ 452 | 453 | var Model = sequelize.define('Project', { 454 | project_id: { 455 | type: Sequelize.UUID 456 | } 457 | }); 458 | 459 | defaultArgs(Model); 460 | 461 | /* 462 | { 463 | project_id: { 464 | type: GraphQLString 465 | }, 466 | where: { 467 | type: JSONType 468 | } 469 | } 470 | */ 471 | ``` 472 | 473 | If you would like to pass "where" as a query variable - you should pass it as a JSON string and declare its type as SequelizeJSON: 474 | 475 | ``` 476 | /* with GraphiQL */ 477 | // request 478 | query($where: SequelizeJSON) { 479 | user(where: $where) { 480 | name 481 | } 482 | } 483 | 484 | // query variables 485 | # JSON doesn't allow single quotes, so you need to use escaped double quotes in your JSON string 486 | { 487 | "where": "{\"name\": {\"like\": \"Henry%\"}}" 488 | } 489 | ``` 490 | 491 | ### defaultListArgs 492 | 493 | `defaultListArgs` will return an object like: 494 | 495 | ```js 496 | { 497 | limit: { 498 | type: GraphQLInt 499 | }, 500 | order: { 501 | type: GraphQLString 502 | }, 503 | where: { 504 | type: JSONType 505 | } 506 | } 507 | ``` 508 | 509 | Which when added to args will let the resolver automatically support limit and ordering in args for graphql queries. 510 | Should be used with fields of type `GraphQLList`. 511 | 512 | ```js 513 | import {defaultListArgs} from 'graphql-sequelize' 514 | 515 | args: Object.assign(defaultListArgs(), { 516 | // ... additional args 517 | }) 518 | ``` 519 | 520 | `order` expects a valid field name and will sort `ASC` by default. For `DESC` you would prepend `reverse:` to the field name. 521 | 522 | 523 | ``` 524 | /* with GraphiQL */ 525 | // users represents a GraphQLList of type user 526 | 527 | query($limit: Int, $order: String, $where: SequelizeJSON) { 528 | users(limit: $limit, order: $order, where: $where) { 529 | name 530 | } 531 | } 532 | 533 | // query variables 534 | { 535 | "order": "name" // OR "reverse:name" for DESC 536 | } 537 | ``` 538 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | dev: 2 | build: . 3 | links: 4 | - postgres 5 | - mysql 6 | volumes: 7 | - .:/src/graphql-sequelize 8 | environment: 9 | DIALECT: "${DIALECT}" 10 | 11 | postgres: 12 | image: postgres:9.4 13 | environment: 14 | POSTGRES_USER: graphql_sequelize_test 15 | POSTGRES_PASSWORD: graphql_sequelize_test 16 | 17 | mysql: 18 | image: mysql:5.6 19 | environment: 20 | MYSQL_USER: test 21 | MYSQL_PASSWORD: test 22 | MYSQL_DATABASE: test 23 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 24 | 25 | benchmark_server: 26 | build: . 27 | command: [node, test/benchmark.js] 28 | ports: 29 | - "4001:4001" 30 | links: 31 | - postgres 32 | volumes: 33 | - .:/src/graphql-sequelize 34 | environment: 35 | DIALECT: postgres 36 | DB_DATABASE: graphql_sequelize_test 37 | DB_USER: graphql_sequelize_test 38 | DB_PASSWORD: graphql_sequelize_test 39 | -------------------------------------------------------------------------------- /docs/relay.md: -------------------------------------------------------------------------------- 1 | # graphql-sequelize and Relay 2 | 3 | ## node lookups 4 | 5 | relay will perform certain queries on a root "node" type. 6 | graphql-sequelize will automatically map these node lookups to findById calls. 7 | 8 | If you wish to use non-sequelize entities, or if you want to override the default 9 | behaviour for sequelize models, you can specify a resolve function. 10 | 11 | ```js 12 | import {createNodeInterface} from 'graphql-sequelize'; 13 | import sequelize from './your-sequelize-instance'; 14 | 15 | const { 16 | User 17 | } = sequelize; 18 | 19 | const { 20 | nodeInterface, 21 | nodeField, 22 | nodeTypeMapper 23 | } = createNodeInterface(sequelize); 24 | 25 | const userType = new GraphQLObjectType({ 26 | name: User.name, 27 | fields: { 28 | id: globalIdField(User.name), 29 | name: { 30 | type: GraphQLString 31 | } 32 | }, 33 | interfaces: [nodeInterface] 34 | }); 35 | 36 | nodeTypeMapper.mapTypes({ 37 | [User.name]: userType, 38 | 39 | //Non-sequelize models can be added as well 40 | SomeOther: { 41 | type: SomeOtherType, //Specify graphql type to map to 42 | resolve(globalId, context) { //Specify function to get entity from id 43 | const { id } = fromGlobalId(globalId); 44 | return getSomeOther(id); 45 | } 46 | } 47 | }); 48 | 49 | const schema = new GraphQLSchema({ 50 | query: new GraphQLObjectType({ 51 | name: 'RootType', 52 | fields: { 53 | user: { 54 | type: userType, 55 | args: { 56 | id: { 57 | type: new GraphQLNonNull(GraphQLInt) 58 | } 59 | }, 60 | resolve: resolver(User) 61 | }, 62 | node: nodeField 63 | } 64 | }) 65 | }); 66 | ``` 67 | 68 | If you make sure to call `nodeTypeMapper.mapTypes` with all your graphql types matching your sequelize models all node with global id lookups will work. 69 | You can also add any non-model mapping you'd like to `mapTypes'. 70 | 71 | ## connections 72 | 73 | graphql-sequelize's createConnection will automatically handle pagination via cursors, first, last, before, after and orderBy. 74 | 75 | ```js 76 | import {createConnection} from 'graphql-sequelize'; 77 | import sequelize from './your-sequelize-instance'; 78 | 79 | const { 80 | User, 81 | Task 82 | } = sequelize; 83 | 84 | 85 | const taskType = new GraphQLObjectType({ 86 | name: Task.name, 87 | fields: { 88 | id: globalIdField(Task.name), 89 | title: { 90 | type: GraphQLString 91 | } 92 | } 93 | }); 94 | 95 | const userTaskConnection = createConnection({ 96 | name: 'userTask', 97 | nodeType: taskType, 98 | target: User.Tasks | Task, // Can be an association for parent related connections or a model for "anonymous" connections 99 | // if no orderBy is specified the model primary key will be used. 100 | orderBy: new GraphQLEnumType({ 101 | name: 'UserTaskOrderBy', 102 | values: { 103 | AGE: {value: ['createdAt', 'DESC']}, // The first ENUM value will be the default order. The direction will be used for `first`, will automatically be inversed for `last` lookups. 104 | TITLE: {value: ['title', 'ASC']}, 105 | CUSTOM: {value: [function (source, args, context, info) {}, 'ASC']} // build and return custom order for sequelize orderBy option 106 | } 107 | }), 108 | where: function (key, value, currentWhere) { 109 | // for custom args other than connectionArgs return a sequelize where parameter 110 | 111 | return {[key]: value}; 112 | }, 113 | connectionFields: { 114 | total: { 115 | type: GraphQLInt, 116 | resolve: ({source}) => { 117 | /* 118 | * We return a object containing the source, edges and more as the connection result 119 | * You there for need to extract source from the usual source argument 120 | */ 121 | return source.countTasks(); 122 | } 123 | } 124 | }, 125 | edgeFields: { 126 | wasCreatedByUser: { 127 | type: GraphQLBoolean, 128 | resolve: (edge) => { 129 | /* 130 | * We attach the connection source to edges 131 | */ 132 | return edge.node.createdBy === edge.source.id; 133 | } 134 | } 135 | } 136 | }); 137 | 138 | const userType = new GraphQLObjectType({ 139 | name: User.name, 140 | fields: { 141 | id: globalIdField(User.name), 142 | name: { 143 | type: GraphQLString 144 | }, 145 | tasks: { 146 | type: userTaskConnection.connectionType, 147 | args: userTaskConnection.connectionArgs, 148 | resolve: userTaskConnection.resolve 149 | } 150 | } 151 | }); 152 | ``` 153 | ``` 154 | { 155 | user(id: 123) { 156 | tasks(first: 10, orderBy: AGE) { 157 | ...totalCount 158 | edges { 159 | ...getCreated 160 | cursor 161 | node { 162 | id 163 | title 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | fragment totalCount on userTaskConnection { 171 | total 172 | } 173 | 174 | fragment getCreated on userTaskEdge { 175 | wasCreatedByUser 176 | } 177 | ``` 178 | 179 | You can pass custom args in your connection definition and they will 180 | automatically be turned into where arguments. These can be further modified 181 | using the `where` option in `createConnection`. 182 | 183 | ```js 184 | const userTaskConnection = createConnection({ 185 | name: 'userTask', 186 | nodeType: taskType, 187 | target: User.Tasks, 188 | where: function (key, value) { 189 | if (key === 'titleStartsWith') { 190 | return { title: { $like: `${value}%` } }; 191 | } else { 192 | return {[key]: value}; 193 | } 194 | }, 195 | }); 196 | const userType = new GraphQLObjectType({ 197 | name: User.name, 198 | fields: { 199 | id: globalIdField(User.name), 200 | name: { 201 | type: GraphQLString 202 | }, 203 | tasks: { 204 | type: userTaskConnection.connectionType, 205 | args: { 206 | ...userTaskConnection.connectionArgs, // <-- Load the defaults 207 | titleStartsWith: { // <-- Extend further yourself 208 | type: GraphQLString, 209 | } 210 | }, 211 | resolve: userTaskConnection.resolve 212 | } 213 | } 214 | }); 215 | ``` 216 | 217 | Alongside `createConnection` you can also use `createConnectionResolver` which can be useful for schemas defined via separated SDL and Resolver functions: 218 | 219 | ```ts 220 | import {makeExecutableSchema} from 'graphql-tools'; 221 | import {resolver, createNodeInterface, createConnectionResolver} from 'graphql-sequelize'; 222 | import gql from 'graphql-tag'; 223 | 224 | const { nodeField, nodeTypeMapper } = createNodeInterface(sequelize); 225 | 226 | nodeTypeMapper.mapTypes({ 227 | [User.name]: 'User' // Supports both new GraphQLObjectType({...}) and type name 228 | }); 229 | 230 | const typeDefs = gql` 231 | type Query { 232 | node(id: ID!): Node 233 | user(id: ID!): User 234 | users(after: String, before: String, first: Int, last: Int, order: UserOrderBy): UserConnection 235 | } 236 | 237 | interface Node { 238 | id: ID! 239 | } 240 | 241 | type User { 242 | id: ID! 243 | name: String 244 | } 245 | 246 | type UserConnection { 247 | pageInfo: PageInfo! 248 | edges: [UserEdge] 249 | total: Int 250 | } 251 | 252 | type UserEdge { 253 | node: User 254 | cursor: String! 255 | } 256 | 257 | enum UserOrderBy { 258 | ID 259 | } 260 | `; 261 | 262 | const resolvers = { 263 | User: { 264 | name: (user) => user.name, 265 | }, 266 | UserOrderBy: { 267 | ID: ['id', 'ASC'], 268 | }, 269 | Query: { 270 | node: nodeField.resolve, 271 | user: resolver(User), 272 | users: createConnectionResolver({ 273 | target: User, 274 | orderBy: 'UserOrderBy', // supports both new GraphQLEnumType({...}) and type name 275 | }).resolveConnection, 276 | }, 277 | }; 278 | 279 | const schema = makeExecutableSchema({ typeDefs , resolvers }); 280 | ``` -------------------------------------------------------------------------------- /examples/graphql-yoga/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "exclude": ["transform-regenerator"], 5 | "targets": { 6 | "node": 8 7 | } 8 | }] 9 | ], 10 | "plugins": [ 11 | "transform-class-properties" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/graphql-yoga/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | -------------------------------------------------------------------------------- /examples/graphql-yoga/.node-version: -------------------------------------------------------------------------------- 1 | 8.11.3 2 | -------------------------------------------------------------------------------- /examples/graphql-yoga/.nvmrc: -------------------------------------------------------------------------------- 1 | 8.11.3 2 | -------------------------------------------------------------------------------- /examples/graphql-yoga/README.md: -------------------------------------------------------------------------------- 1 | # `graphql-sequelize` + `graphql-yoga` 2 | 3 | An example of how to set up `graphql-sequelize` and `dataloader-sequelize` with `graphql-yoga`. 4 | 5 | ## Prerequisites 6 | 7 | - Node 8+ 8 | 9 | ```bash 10 | npm install 11 | npm start 12 | open http://localhost:4000 13 | ``` 14 | 15 | ## Running a query 16 | 17 | In GraphQL Playground, run the following query: 18 | 19 | ```graphql 20 | { 21 | pets { 22 | name 23 | 24 | owner { 25 | id 26 | name 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | Your response should look like this: 33 | 34 | ```json 35 | { 36 | "data": { 37 | "pets": [ 38 | { 39 | "name": "Bat", 40 | "owner": { 41 | "id": "1", 42 | "name": "Foo" 43 | } 44 | }, 45 | { 46 | "name": "Baz", 47 | "owner": { 48 | "id": "2", 49 | "name": "Bar" 50 | } 51 | } 52 | ] 53 | } 54 | } 55 | ``` 56 | 57 | To verify that DataLoader is working as expected, look at your server output. You should see two 58 | queries: 59 | 60 | ```bash 61 | Executing (default): SELECT `id`, `name`, `ownerId`, `createdAt`, `updatedAt` FROM `pets` AS `Pet` ORDER BY `Pet`.`id` ASC; 62 | Executing (default): SELECT `id`, `name`, `createdAt`, `updatedAt` FROM `users` AS `User` WHERE `User`.`id` IN (1, 2); 63 | ``` 64 | 65 | For comparison, open [src/server.js](./src/server.js) and comment out the entirety of the 66 | `context` option that gets passed into `GraphQLServer`. Restart the server, run the 67 | GraphQL query again, and check the server output. Now, you should see three queries: 68 | 69 | ```bash 70 | Executing (default): SELECT `id`, `name`, `ownerId`, `createdAt`, `updatedAt` FROM `pets` AS `Pet` ORDER BY `Pet`.`id` ASC; 71 | Executing (default): SELECT `id`, `name`, `createdAt`, `updatedAt` FROM `users` AS `User` WHERE `User`.`id` = 1; 72 | Executing (default): SELECT `id`, `name`, `createdAt`, `updatedAt` FROM `users` AS `User` WHERE `User`.`id` = 2; 73 | ``` 74 | -------------------------------------------------------------------------------- /examples/graphql-yoga/index.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./src'); 3 | -------------------------------------------------------------------------------- /examples/graphql-yoga/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-sequelize-example", 3 | "private": true, 4 | "version": "0.1.0", 5 | "description": "An example of using graphql-sequelize in a NodeJS application", 6 | "scripts": { 7 | "build": "NODE_ENV=production babel src -d build", 8 | "start": "node index.js" 9 | }, 10 | "dependencies": { 11 | "dataloader-sequelize": "^1.7.2", 12 | "express": "^4.16.3", 13 | "graphql-relay": "^0.5.5", 14 | "graphql-sequelize": "^8.3.1", 15 | "graphql-yoga": "^1.14.12", 16 | "sequelize": "^4.38.0", 17 | "sqlite3": "^4.0.1" 18 | }, 19 | "devDependencies": { 20 | "babel-cli": "^6.26.0", 21 | "babel-plugin-transform-class-properties": "^6.24.1", 22 | "babel-preset-env": "^1.7.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/graphql-yoga/src/config/database.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | 3 | const sequelize = new Sequelize('graphql_sequelize_test', 'root', '', { 4 | host: 'localhost', 5 | dialect: 'sqlite', 6 | }); 7 | 8 | export default sequelize; 9 | -------------------------------------------------------------------------------- /examples/graphql-yoga/src/index.js: -------------------------------------------------------------------------------- 1 | import server from './server'; 2 | import models from './models'; 3 | 4 | /** 5 | * DISCLAIMER: using sequelize#sync is not recommended for production use. Please, please 6 | * use migrations. This method of creating a database is used in this demo for simplicity's sake. 7 | */ 8 | async function start() { 9 | // Make sure the database tables are up to date 10 | await models.sequelize.sync({ force: true }); 11 | 12 | // Create sample data 13 | const foo = await models.User.create({ name: 'Foo' }); 14 | const bar = await models.User.create({ name: 'Bar' }); 15 | await foo.createPet({ name: 'Bat' }); 16 | await bar.createPet({ name: 'Baz' }); 17 | 18 | // Start the GraphQL server 19 | server.start(() => { 20 | console.log('Server is running on localhost:4000'); 21 | }); 22 | } 23 | 24 | start(); 25 | 26 | -------------------------------------------------------------------------------- /examples/graphql-yoga/src/models/Pet.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | 3 | class Pet extends Model { 4 | static tableName = 'pets'; 5 | 6 | static associate(models) { 7 | Pet.Owner = Pet.belongsTo(models.User, { 8 | as: 'owner', 9 | }); 10 | } 11 | } 12 | 13 | export default (sequelize) => { 14 | Pet.init({ 15 | name: Sequelize.STRING, 16 | ownerId: { 17 | type: Sequelize.INTEGER, 18 | }, 19 | }, { 20 | sequelize, 21 | tableName: Pet.tableName, 22 | }); 23 | 24 | return Pet; 25 | }; 26 | -------------------------------------------------------------------------------- /examples/graphql-yoga/src/models/User.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | 3 | class User extends Model { 4 | static tableName = 'users'; 5 | 6 | static associate(models) { 7 | User.Pets = User.hasMany(models.Pet, { 8 | foreignKey: 'ownerId', 9 | as: 'pets', 10 | }); 11 | } 12 | } 13 | 14 | const schema = { 15 | name: Sequelize.STRING, 16 | }; 17 | 18 | export default (sequelize) => { 19 | User.init(schema, { 20 | sequelize, 21 | tableName: User.tableName, 22 | }); 23 | 24 | return User; 25 | }; 26 | -------------------------------------------------------------------------------- /examples/graphql-yoga/src/models/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import Sequelize from 'sequelize'; 4 | 5 | import sequelize from '../config/database'; 6 | 7 | const db = { 8 | sequelize, 9 | Sequelize, 10 | }; 11 | 12 | fs 13 | .readdirSync(__dirname) 14 | .filter(file => 15 | path.extname(file) === '.js' && 16 | file !== 'index.js', 17 | ) 18 | .forEach((file) => { 19 | const model = sequelize.import(path.join(__dirname, file)); 20 | db[model.name] = model; 21 | }); 22 | 23 | Object.keys(db).forEach((modelName) => { 24 | if ('associate' in db[modelName]) { 25 | db[modelName].associate(db); 26 | } 27 | }); 28 | 29 | export default db; 30 | -------------------------------------------------------------------------------- /examples/graphql-yoga/src/server.js: -------------------------------------------------------------------------------- 1 | import { GraphQLServer } from 'graphql-yoga'; 2 | import { createContext, EXPECTED_OPTIONS_KEY } from 'dataloader-sequelize'; 3 | import { resolver } from 'graphql-sequelize'; 4 | import models from './models'; 5 | 6 | const typeDefs = ` 7 | type Query { 8 | pet(id: ID!): Pet 9 | pets: [Pet] 10 | user(id: ID!): User 11 | users: [User] 12 | } 13 | 14 | type User { 15 | id: ID! 16 | name: String 17 | pets: [Pet] 18 | } 19 | 20 | type Pet { 21 | id: ID! 22 | name: String 23 | owner: User 24 | } 25 | `; 26 | 27 | const resolvers = { 28 | Query: { 29 | pet: resolver(models.Pet), 30 | pets: resolver(models.Pet), 31 | user: resolver(models.User), 32 | users: resolver(models.User), 33 | }, 34 | User: { 35 | pets: resolver(models.User.Pets), 36 | }, 37 | Pet: { 38 | owner: resolver(models.Pet.Owner), 39 | }, 40 | }; 41 | 42 | // Tell `graphql-sequelize` where to find the DataLoader context in the 43 | // global request context 44 | resolver.contextToOptions = { [EXPECTED_OPTIONS_KEY]: EXPECTED_OPTIONS_KEY }; 45 | 46 | const server = new GraphQLServer({ 47 | typeDefs, 48 | resolvers, 49 | context(req) { 50 | // For each request, create a DataLoader context for Sequelize to use 51 | const dataloaderContext = createContext(models.sequelize); 52 | 53 | // Using the same EXPECTED_OPTIONS_KEY, store the DataLoader context 54 | // in the global request context 55 | return { 56 | [EXPECTED_OPTIONS_KEY]: dataloaderContext, 57 | }; 58 | }, 59 | }); 60 | 61 | export default server; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-sequelize", 3 | "version": "9.5.1", 4 | "description": "GraphQL & Relay for MySQL & Postgres via Sequelize", 5 | "main": "lib/index.js", 6 | "options": { 7 | "mocha": "--require scripts/mocha-bootload" 8 | }, 9 | "scripts": { 10 | "prepublish": "npm run check && npm run build", 11 | "check": "npm run lint && npm run test:unit", 12 | "lint": "eslint src", 13 | "build": "rm -rf lib/* && babel src --ignore test --out-dir lib", 14 | "test": "npm run test:unit && npm run test:docker", 15 | "test:watch": "npm run test:unit -- --watch", 16 | "test:unit": "mocha $npm_package_options_mocha test/unit/*.test.js test/unit/**/*.test.js", 17 | "build:docker": "docker-compose build", 18 | "test:docker": "DIALECT=${DIALECT:=postgres} docker-compose run dev /bin/sh -c \"npm run test:integration\"", 19 | "test:integration": "mocha $npm_package_options_mocha test/integration/*.test.js test/integration/**/*.test.js", 20 | "psql": "docker run -it --link graphqlsequelize_postgres_1:postgres --rm postgres:9.4 sh -c 'PGPASSWORD=graphql_sequelize_test exec psql -h \"$POSTGRES_PORT_5432_TCP_ADDR\" -p \"$POSTGRES_PORT_5432_TCP_PORT\" -U graphql_sequelize_test'", 21 | "cover": "babel-node node_modules/.bin/isparta cover _mocha -- $npm_package_options_mocha test/**/**/*.test.js test/**/*.test.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/mickhansen/graphql-sequelize.git" 26 | }, 27 | "keywords": [ 28 | "graphql", 29 | "sequelize" 30 | ], 31 | "author": "Mick Hansen ", 32 | "contributors": [ 33 | { 34 | "name": "graphql-sequelize community", 35 | "url": "https://github.com/mickhansen/graphql-sequelize/graphs/contributors" 36 | } 37 | ], 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/mickhansen/graphql-sequelize/issues" 41 | }, 42 | "homepage": "https://github.com/mickhansen/graphql-sequelize", 43 | "dependencies": { 44 | "bluebird": "^3.4.0", 45 | "invariant": "2.2.1", 46 | "lodash": "^4.0.0" 47 | }, 48 | "peerDependencies": { 49 | "graphql": "^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14 || ^15 || ^16", 50 | "graphql-relay": "^0.4.2 || ^0.5.0 || ^0.7.0 || ^0.8.0 || ^0.9.0 || ^0.10.0", 51 | "sequelize": ">=3.0.0" 52 | }, 53 | "devDependencies": { 54 | "babel-cli": "^6.9.0", 55 | "babel-eslint": "^6.0.3", 56 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 57 | "babel-preset-async-to-bluebird": "^1.1.0", 58 | "babel-preset-es2015-node4": "^2.1.0", 59 | "babel-register": "^6.9.0", 60 | "chai": "^3.0.0", 61 | "chai-as-promised": "^5.1.0", 62 | "eslint": "^1.7.3", 63 | "graphql": "^16.6.0", 64 | "graphql-relay": "^0.10.0", 65 | "isparta": "^4.0.0", 66 | "istanbul": "^0.4.0", 67 | "mocha": "^3.0.1", 68 | "mysql": "^2.11.1", 69 | "pg": "^5.0.0", 70 | "pg-hstore": "^2.3.2", 71 | "sequelize": "^6.25.6", 72 | "sinon": "^1.15.4", 73 | "sinon-as-promised": "^4.0.0", 74 | "sinon-chai": "^2.8.0", 75 | "sqlite3": "^5.1.2" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | npm run check -------------------------------------------------------------------------------- /scripts/mocha-bootload: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | 3 | var chai = require('chai'); 4 | 5 | chai.use(require('chai-as-promised')); 6 | chai.use(require('sinon-chai')); 7 | require('sinon-as-promised')(require('bluebird')); 8 | -------------------------------------------------------------------------------- /src/argsToFindOptions.js: -------------------------------------------------------------------------------- 1 | import { replaceWhereOperators } from './replaceWhereOperators'; 2 | 3 | export default function argsToFindOptions(args, targetAttributes) { 4 | var result = {}; 5 | 6 | if (args) { 7 | Object.keys(args).forEach(function (key) { 8 | if (typeof args[key] !== 'undefined') { 9 | if (key === 'limit') { 10 | result.limit = parseInt(args[key], 10); 11 | } else if (key === 'offset') { 12 | result.offset = parseInt(args[key], 10); 13 | } else if (key === 'order') { 14 | if (args[key].indexOf('reverse:') === 0) { 15 | result.order = [[args[key].substring(8), 'DESC']]; 16 | } else { 17 | result.order = [[args[key], 'ASC']]; 18 | } 19 | } else if (key === 'where') { 20 | // setup where 21 | result.where = replaceWhereOperators(args.where); 22 | } else if (~targetAttributes.indexOf(key)) { 23 | result.where = result.where || {}; 24 | result.where[key] = args[key]; 25 | } 26 | } 27 | }); 28 | } 29 | 30 | return result; 31 | } 32 | -------------------------------------------------------------------------------- /src/attributeFields.js: -------------------------------------------------------------------------------- 1 | import * as typeMapper from './typeMapper'; 2 | import { GraphQLNonNull, GraphQLEnumType, GraphQLList } from 'graphql'; 3 | import { globalIdField } from 'graphql-relay'; 4 | 5 | module.exports = function (Model, options = {}) { 6 | var cache = options.cache || {}; 7 | var result = Object.keys(Model.rawAttributes).reduce(function (memo, key) { 8 | if (options.exclude) { 9 | if (typeof options.exclude === 'function' && options.exclude(key)) return memo; 10 | if (Array.isArray(options.exclude) && ~options.exclude.indexOf(key)) return memo; 11 | } 12 | if (options.only) { 13 | if (typeof options.only === 'function' && !options.only(key)) return memo; 14 | if (Array.isArray(options.only) && !~options.only.indexOf(key)) return memo; 15 | } 16 | 17 | var attribute = Model.rawAttributes[key] 18 | , type = attribute.type; 19 | 20 | 21 | if (options.map) { 22 | if (typeof options.map === 'function') { 23 | key = options.map(key) || key; 24 | } else { 25 | key = options.map[key] || key; 26 | } 27 | } 28 | 29 | memo[key] = { 30 | type: typeMapper.toGraphQL(type, Model.sequelize.constructor) 31 | }; 32 | 33 | if (memo[key].type instanceof GraphQLEnumType || 34 | memo[key].type instanceof GraphQLList && memo[key].type.ofType instanceof GraphQLEnumType 35 | ) { 36 | var typeName = `${Model.name}${key}EnumType`; 37 | /* 38 | Cache enum types to prevent duplicate type name error 39 | when calling attributeFields multiple times on the same model 40 | */ 41 | if (cache[typeName]) { 42 | if (memo[key].type.ofType) { 43 | memo[key].type.ofType = cache[typeName]; 44 | } else { 45 | memo[key].type = cache[typeName]; 46 | } 47 | } else if (memo[key].type.ofType) { 48 | memo[key].type.ofType.name = typeName; 49 | cache[typeName] = memo[key].type.ofType; 50 | } else { 51 | memo[key].type.name = typeName; 52 | cache[typeName] = memo[key].type; 53 | } 54 | 55 | } 56 | 57 | if (!options.allowNull) { 58 | if (attribute.allowNull === false || attribute.primaryKey === true) { 59 | memo[key].type = new GraphQLNonNull(memo[key].type); 60 | } 61 | } 62 | 63 | if (options.commentToDescription) { 64 | if (typeof attribute.comment === 'string') { 65 | memo[key].description = attribute.comment; 66 | } 67 | } 68 | 69 | return memo; 70 | }, {}); 71 | 72 | if (options.globalId) { 73 | result.id = globalIdField(Model.name, instance => instance[Model.primaryKeyAttribute]); 74 | } 75 | 76 | return result; 77 | }; 78 | -------------------------------------------------------------------------------- /src/base64.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export function base64(i) { 4 | return (new Buffer(i, 'ascii')).toString('base64'); 5 | } 6 | 7 | export function unbase64(i) { 8 | return (new Buffer(i, 'base64')).toString('ascii'); 9 | } 10 | -------------------------------------------------------------------------------- /src/defaultArgs.js: -------------------------------------------------------------------------------- 1 | import * as typeMapper from './typeMapper'; 2 | import JSONType from './types/jsonType'; 3 | 4 | module.exports = function (Model) { 5 | var result = {} 6 | , keys = Model.primaryKeyAttributes 7 | , type; 8 | 9 | if (keys) { 10 | keys.forEach(key => { 11 | var attribute = Model.rawAttributes[key]; 12 | if (attribute) { 13 | type = typeMapper.toGraphQL(attribute.type, Model.sequelize.constructor); 14 | result[key] = { 15 | type: type 16 | }; 17 | } 18 | }); 19 | } 20 | 21 | // add where 22 | result.where = { 23 | type: JSONType, 24 | description: 'A JSON object conforming the the shape specified in http://docs.sequelizejs.com/en/latest/docs/querying/' 25 | }; 26 | 27 | return result; 28 | }; 29 | -------------------------------------------------------------------------------- /src/defaultListArgs.js: -------------------------------------------------------------------------------- 1 | import { GraphQLInt, GraphQLString } from 'graphql'; 2 | import JSONType from './types/jsonType'; 3 | 4 | module.exports = function () { 5 | return { 6 | limit: { 7 | type: GraphQLInt 8 | }, 9 | order: { 10 | type: GraphQLString 11 | }, 12 | where: { 13 | type: JSONType, 14 | description: 'A JSON object conforming the the shape specified in http://docs.sequelizejs.com/en/latest/docs/querying/' 15 | }, 16 | offset: { 17 | type: GraphQLInt 18 | } 19 | }; 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | argsToFindOptions: require('./argsToFindOptions'), 3 | resolver: require('./resolver'), 4 | defaultListArgs: require('./defaultListArgs'), 5 | defaultArgs: require('./defaultArgs'), 6 | typeMapper: require('./typeMapper'), 7 | attributeFields: require('./attributeFields'), 8 | simplifyAST: require('./simplifyAST'), 9 | relay: require('./relay'), 10 | sequelizeConnection: require('./relay').sequelizeConnection, 11 | createConnection: require('./relay').createConnection, 12 | createConnectionResolver: require('./relay').createConnectionResolver, 13 | createNodeInterface: require('./relay').createNodeInterface, 14 | JSONType: require('./types/jsonType'), 15 | DateType: require('./types/dateType'), 16 | }; 17 | -------------------------------------------------------------------------------- /src/relay.js: -------------------------------------------------------------------------------- 1 | import { 2 | fromGlobalId, 3 | connectionFromArray, 4 | nodeDefinitions, 5 | connectionDefinitions, 6 | connectionArgs 7 | } from 'graphql-relay'; 8 | 9 | import { 10 | GraphQLList 11 | } from 'graphql'; 12 | 13 | import { 14 | base64, 15 | unbase64, 16 | } from './base64.js'; 17 | 18 | import _ from 'lodash'; 19 | import simplifyAST from './simplifyAST'; 20 | 21 | import {Model} from 'sequelize'; 22 | import {replaceWhereOperators} from './replaceWhereOperators.js'; 23 | 24 | function getModelOfInstance(instance) { 25 | return instance instanceof Model ? instance.constructor : instance.Model; 26 | } 27 | 28 | export class NodeTypeMapper { 29 | constructor() { 30 | this.map = { }; 31 | } 32 | 33 | mapTypes(types) { 34 | Object.keys(types).forEach((k) => { 35 | let v = types[k]; 36 | this.map[k] = v.type 37 | ? v 38 | : { type: v }; 39 | }); 40 | } 41 | 42 | item(type) { 43 | return this.map[type]; 44 | } 45 | } 46 | 47 | export function idFetcher(sequelize, nodeTypeMapper) { 48 | return async (globalId, context, info) => { 49 | const {type, id} = fromGlobalId(globalId); 50 | 51 | const nodeType = nodeTypeMapper.item(type); 52 | if (nodeType && typeof nodeType.resolve === 'function') { 53 | const res = await Promise.resolve(nodeType.resolve(globalId, context, info)); 54 | if (res) res.__graphqlType__ = type; 55 | return res; 56 | } 57 | 58 | const model = Object.keys(sequelize.models).find(model => model === type); 59 | if (model) { 60 | return sequelize.models[model].findByPk ? sequelize.models[model].findByPk(id) : sequelize.models[model].findById(id); 61 | } 62 | 63 | if (nodeType) { 64 | return typeof nodeType.type === 'string' ? info.schema.getType(nodeType.type) : nodeType.type; 65 | } 66 | 67 | return null; 68 | }; 69 | } 70 | 71 | export function typeResolver(nodeTypeMapper) { 72 | return (obj, context, info) => { 73 | var type = obj.__graphqlType__ 74 | || (obj.Model 75 | ? obj.Model.options.name.singular 76 | : obj._modelOptions 77 | ? obj._modelOptions.name.singular 78 | : obj.constructor.options 79 | ? obj.constructor.options.name.singular 80 | : obj.name); 81 | 82 | if (!type) { 83 | throw new Error(`Unable to determine type of ${ typeof obj }. ` + 84 | `Either specify a resolve function in 'NodeTypeMapper' object, or specify '__graphqlType__' property on object.`); 85 | } 86 | 87 | const nodeType = nodeTypeMapper.item(type); 88 | if (nodeType) { 89 | return typeof nodeType.type === 'string' ? nodeType.type : nodeType.type.name; 90 | } 91 | 92 | return null; 93 | }; 94 | } 95 | 96 | export function isConnection(type) { 97 | return typeof type.name !== 'undefined' && type.name.endsWith('Connection'); 98 | } 99 | 100 | export function handleConnection(values, args) { 101 | return connectionFromArray(values, args); 102 | } 103 | 104 | export function createNodeInterface(sequelize) { 105 | let nodeTypeMapper = new NodeTypeMapper(); 106 | const nodeObjects = nodeDefinitions( 107 | idFetcher(sequelize, nodeTypeMapper), 108 | typeResolver(nodeTypeMapper) 109 | ); 110 | 111 | return { 112 | nodeTypeMapper, 113 | ...nodeObjects 114 | }; 115 | } 116 | 117 | export {createNodeInterface as sequelizeNodeInterface}; 118 | 119 | export function nodeType(connectionType) { 120 | return connectionType._fields.edges.type.ofType._fields.node.type; 121 | } 122 | 123 | export function createConnectionResolver({ 124 | target: targetMaybeThunk, 125 | before, 126 | after, 127 | where, 128 | orderBy: orderByEnum, 129 | ignoreArgs 130 | }) { 131 | before = before || ((options) => options); 132 | after = after || ((result) => result); 133 | 134 | let orderByAttribute = function (orderAttr, {source, args, context, info}) { 135 | return typeof orderAttr === 'function' ? orderAttr(source, args, context, info) : orderAttr; 136 | }; 137 | 138 | let orderByDirection = function (orderDirection, args) { 139 | if (args.last) { 140 | return orderDirection.indexOf('ASC') >= 0 141 | ? orderDirection.replace('ASC', 'DESC') 142 | : orderDirection.replace('DESC', 'ASC'); 143 | } 144 | return orderDirection; 145 | }; 146 | 147 | /** 148 | * Creates a cursor given a item returned from the Database 149 | * @param {Object} item sequelize row 150 | * @param {Integer} index the index of this item within the results, 0 indexed 151 | * @return {String} The Base64 encoded cursor string 152 | */ 153 | let toCursor = function (item, index) { 154 | const model = getModelOfInstance(item); 155 | const id = model ? 156 | typeof model.primaryKeyAttribute === 'string' ? item[model.primaryKeyAttribute] : null : 157 | item[Object.keys(item)[0]]; 158 | return base64(JSON.stringify([id, index])); 159 | }; 160 | 161 | /** 162 | * Decode a cursor into its component parts 163 | * @param {String} cursor Base64 encoded cursor 164 | * @return {Object} Object containing ID and index 165 | */ 166 | let fromCursor = function (cursor) { 167 | let [id, index] = JSON.parse(unbase64(cursor)); 168 | 169 | return { 170 | id, 171 | index 172 | }; 173 | }; 174 | 175 | let argsToWhere = function (args) { 176 | let result = {}; 177 | 178 | if (where === undefined) return result; 179 | 180 | _.each(args, (value, key) => { 181 | if (ignoreArgs && key in ignoreArgs) return; 182 | Object.assign(result, where(key, value, result)); 183 | }); 184 | 185 | return replaceWhereOperators(result); 186 | }; 187 | 188 | let resolveEdge = function (item, index, queriedCursor, sourceArgs = {}, source) { 189 | let startIndex = null; 190 | if (queriedCursor) startIndex = Number(queriedCursor.index); 191 | if (startIndex !== null) { 192 | startIndex++; 193 | } else { 194 | startIndex = 0; 195 | } 196 | 197 | return { 198 | cursor: toCursor(item, index + startIndex), 199 | node: item, 200 | source: source, 201 | sourceArgs 202 | }; 203 | }; 204 | 205 | let $resolver = require('./resolver')(targetMaybeThunk, { 206 | handleConnection: false, 207 | list: true, 208 | before: function (options, args, context, info) { 209 | const target = info.target; 210 | const model = target.target ? target.target : target; 211 | 212 | if (args.first || args.last) { 213 | options.limit = parseInt(args.first || args.last, 10); 214 | } 215 | 216 | // Grab enum type by name if it's a string 217 | orderByEnum = typeof orderByEnum === 'string' ? info.schema.getType(orderByEnum) : orderByEnum; 218 | 219 | let orderBy = args.orderBy ? args.orderBy : 220 | orderByEnum ? [orderByEnum._values[0].value] : 221 | [[model.primaryKeyAttribute, 'ASC']]; 222 | 223 | if (orderByEnum && typeof orderBy === 'string') { 224 | orderBy = [orderByEnum._nameLookup[args.orderBy].value]; 225 | } 226 | 227 | let orderAttribute = orderByAttribute(orderBy[0][0], { 228 | source: info.source, 229 | args, 230 | context, 231 | info 232 | }); 233 | let orderDirection = orderByDirection(orderBy[0][1], args); 234 | 235 | options.order = [ 236 | [orderAttribute, orderDirection] 237 | ]; 238 | 239 | if (orderAttribute !== model.primaryKeyAttribute) { 240 | options.order.push([model.primaryKeyAttribute, orderByDirection('ASC', args)]); 241 | } 242 | 243 | if (typeof orderAttribute === 'string') { 244 | options.attributes.push(orderAttribute); 245 | } 246 | 247 | if (options.limit && !options.attributes.some(attribute => attribute.length === 2 && attribute[1] === 'full_count')) { 248 | if (model.sequelize.dialect.name === 'postgres') { 249 | options.attributes.push([ 250 | model.sequelize.literal('COUNT(*) OVER()'), 251 | 'full_count' 252 | ]); 253 | } else if (model.sequelize.dialect.name === 'mssql' || model.sequelize.dialect.name === 'sqlite') { 254 | options.attributes.push([ 255 | model.sequelize.literal('COUNT(1) OVER()'), 256 | 'full_count' 257 | ]); 258 | } 259 | } 260 | 261 | options.where = argsToWhere(args); 262 | 263 | if (args.after || args.before) { 264 | let cursor = fromCursor(args.after || args.before); 265 | let startIndex = Number(cursor.index); 266 | 267 | if (startIndex >= 0) options.offset = startIndex + 1; 268 | } 269 | 270 | options.attributes.unshift(model.primaryKeyAttribute); // Ensure the primary key is always the first selected attribute 271 | options.attributes = _.uniq(options.attributes); 272 | return before(options, args, context, info); 273 | }, 274 | after: async function (values, args, context, info) { 275 | const { 276 | source, 277 | target 278 | } = info; 279 | 280 | var cursor = null; 281 | 282 | if (args.after || args.before) { 283 | cursor = fromCursor(args.after || args.before); 284 | } 285 | 286 | let edges = values.map((value, idx) => { 287 | return resolveEdge(value, idx, cursor, args, source); 288 | }); 289 | 290 | let firstEdge = edges[0]; 291 | let lastEdge = edges[edges.length - 1]; 292 | let fullCount = values[0] && 293 | (values[0].dataValues || values[0]).full_count && 294 | parseInt((values[0].dataValues || values[0]).full_count, 10); 295 | 296 | if (!values[0]) { 297 | fullCount = 0; 298 | } 299 | 300 | if ((args.first || args.last) && (fullCount === null || fullCount === undefined)) { 301 | // In case of `OVER()` is not available, we need to get the full count from a second query. 302 | const options = await Promise.resolve(before({ 303 | where: argsToWhere(args) 304 | }, args, context, info)); 305 | 306 | if (target.count) { 307 | if (target.associationType) { 308 | fullCount = await target.count(source, options); 309 | } else { 310 | fullCount = await target.count(options); 311 | } 312 | } else { 313 | fullCount = await target.manyFromSource.count(source, options); 314 | } 315 | } 316 | 317 | let hasNextPage = false; 318 | let hasPreviousPage = false; 319 | if (args.first || args.last) { 320 | const count = parseInt(args.first || args.last, 10); 321 | let index = cursor ? Number(cursor.index) : null; 322 | if (index !== null) { 323 | index++; 324 | } else { 325 | index = 0; 326 | } 327 | 328 | hasNextPage = index + 1 + count <= fullCount; 329 | hasPreviousPage = index - count >= 0; 330 | 331 | if (args.last) { 332 | [hasNextPage, hasPreviousPage] = [hasPreviousPage, hasNextPage]; 333 | } 334 | } 335 | 336 | return after({ 337 | source, 338 | args, 339 | where: argsToWhere(args), 340 | edges, 341 | pageInfo: { 342 | startCursor: firstEdge ? firstEdge.cursor : null, 343 | endCursor: lastEdge ? lastEdge.cursor : null, 344 | hasNextPage: hasNextPage, 345 | hasPreviousPage: hasPreviousPage 346 | }, 347 | fullCount 348 | }, args, context, info); 349 | } 350 | }); 351 | 352 | let resolveConnection = (source, args, context, info) => { 353 | var fieldNodes = info.fieldASTs || info.fieldNodes; 354 | if (simplifyAST(fieldNodes[0], info).fields.edges) { 355 | return $resolver(source, args, context, info); 356 | } 357 | 358 | return after({ 359 | source, 360 | args, 361 | where: argsToWhere(args) 362 | }, args, context, info); 363 | }; 364 | 365 | return { 366 | resolveEdge, 367 | resolveConnection 368 | }; 369 | } 370 | 371 | export function createConnection({ 372 | name, 373 | nodeType, 374 | target: targetMaybeThunk, 375 | orderBy: orderByEnum, 376 | before, 377 | after, 378 | connectionFields, 379 | edgeFields, 380 | where 381 | }) { 382 | const { 383 | edgeType, 384 | connectionType 385 | } = connectionDefinitions({ 386 | name, 387 | nodeType, 388 | connectionFields, 389 | edgeFields 390 | }); 391 | 392 | let $connectionArgs = { 393 | ...connectionArgs 394 | }; 395 | 396 | if (orderByEnum) { 397 | $connectionArgs.orderBy = { 398 | type: new GraphQLList(orderByEnum) 399 | }; 400 | } 401 | 402 | const { 403 | resolveEdge, 404 | resolveConnection 405 | } = createConnectionResolver({ 406 | orderBy: orderByEnum, 407 | target: targetMaybeThunk, 408 | before, 409 | after, 410 | where, 411 | ignoreArgs: $connectionArgs 412 | }); 413 | 414 | return { 415 | connectionType, 416 | edgeType, 417 | nodeType, 418 | resolveEdge, 419 | resolveConnection, 420 | connectionArgs: $connectionArgs, 421 | resolve: resolveConnection 422 | }; 423 | } 424 | 425 | export {createConnection as sequelizeConnection}; 426 | -------------------------------------------------------------------------------- /src/replaceWhereOperators.js: -------------------------------------------------------------------------------- 1 | import sequelizeOps from './sequelizeOps'; 2 | 3 | /** 4 | * Replace a key deeply in an object 5 | * @param obj 6 | * @param keyMap 7 | * @returns {Object} 8 | */ 9 | function replaceKeyDeep(obj, keyMap) { 10 | return Object.getOwnPropertySymbols(obj).concat(Object.keys(obj)).reduce((memo, key)=> { 11 | 12 | // determine which key we are going to use 13 | let targetKey = keyMap[key] ? keyMap[key] : key; 14 | 15 | if (Array.isArray(obj[key])) { 16 | // recurse if an array 17 | memo[targetKey] = obj[key].map((val) => { 18 | if (Object.prototype.toString.call(val) === '[object Object]') { 19 | return replaceKeyDeep(val, keyMap); 20 | } 21 | return val; 22 | }); 23 | } else if (Object.prototype.toString.call(obj[key]) === '[object Object]') { 24 | // recurse if Object 25 | memo[targetKey] = replaceKeyDeep(obj[key], keyMap); 26 | } else { 27 | // assign the new value 28 | memo[targetKey] = obj[key]; 29 | } 30 | 31 | // return the modified object 32 | return memo; 33 | }, {}); 34 | } 35 | 36 | /** 37 | * Replace the where arguments object and return the sequelize compatible version. 38 | * @param where arguments object in GraphQL Safe format meaning no leading "$" chars. 39 | * @returns {Object} 40 | */ 41 | export function replaceWhereOperators(where) { 42 | return replaceKeyDeep(where, sequelizeOps); 43 | } 44 | -------------------------------------------------------------------------------- /src/resolver.js: -------------------------------------------------------------------------------- 1 | import { GraphQLList, GraphQLNonNull } from 'graphql'; 2 | import _ from 'lodash'; 3 | import argsToFindOptions from './argsToFindOptions'; 4 | import { isConnection, handleConnection, nodeType } from './relay'; 5 | import assert from 'assert'; 6 | 7 | function whereQueryVarsToValues(o, vals) { 8 | [ 9 | ...Object.getOwnPropertyNames(o), 10 | ...Object.getOwnPropertySymbols(o) 11 | ].forEach(k => { 12 | if (_.isFunction(o[k])) { 13 | o[k] = o[k](vals); 14 | return; 15 | } 16 | if (_.isObject(o[k])) { 17 | whereQueryVarsToValues(o[k], vals); 18 | } 19 | }); 20 | } 21 | 22 | function checkIsModel(target) { 23 | return !!target.getTableName; 24 | } 25 | 26 | function checkIsAssociation(target) { 27 | return !!target.associationType; 28 | } 29 | 30 | function resolverFactory(targetMaybeThunk, options = {}) { 31 | assert( 32 | typeof targetMaybeThunk === 'function' || checkIsModel(targetMaybeThunk) || checkIsAssociation(targetMaybeThunk), 33 | 'resolverFactory should be called with a model, an association or a function (which resolves to a model or an association)' 34 | ); 35 | 36 | const contextToOptions = _.assign({}, resolverFactory.contextToOptions, options.contextToOptions); 37 | 38 | assert(options.include === undefined, 'Include support has been removed in favor of dataloader batching'); 39 | if (options.before === undefined) options.before = (options) => options; 40 | if (options.after === undefined) options.after = (result) => result; 41 | if (options.handleConnection === undefined) options.handleConnection = true; 42 | 43 | return async function (source, args, context, info) { 44 | let target = typeof targetMaybeThunk === 'function' && !checkIsModel(targetMaybeThunk) ? 45 | await Promise.resolve(targetMaybeThunk(source, args, context, info)) : targetMaybeThunk 46 | , isModel = checkIsModel(target) 47 | , isAssociation = checkIsAssociation(target) 48 | , association = isAssociation && target 49 | , model = isAssociation && target.target || isModel && target 50 | , type = info.returnType 51 | , list = options.list || 52 | type instanceof GraphQLList || 53 | type instanceof GraphQLNonNull && type.ofType instanceof GraphQLList; 54 | 55 | let targetAttributes = Object.keys(model.rawAttributes) 56 | , findOptions = argsToFindOptions(args, targetAttributes); 57 | 58 | info = { 59 | ...info, 60 | type: type, 61 | source: source, 62 | target: target 63 | }; 64 | 65 | context = context || {}; 66 | 67 | if (isConnection(type)) { 68 | type = nodeType(type); 69 | } 70 | 71 | type = type.ofType || type; 72 | 73 | findOptions.attributes = targetAttributes; 74 | findOptions.logging = findOptions.logging || context.logging; 75 | findOptions.graphqlContext = context; 76 | 77 | _.each(contextToOptions, (as, key) => { 78 | findOptions[as] = context[key]; 79 | }); 80 | 81 | return Promise.resolve(options.before(findOptions, args, context, info)).then(function (findOptions) { 82 | if (args.where && !_.isEmpty(info.variableValues)) { 83 | whereQueryVarsToValues(args.where, info.variableValues); 84 | whereQueryVarsToValues(findOptions.where, info.variableValues); 85 | } 86 | 87 | if (list && !findOptions.order) { 88 | findOptions.order = [[model.primaryKeyAttribute, 'ASC']]; 89 | } 90 | 91 | if (association) { 92 | if (source[association.as] !== undefined) { 93 | // The user did a manual include 94 | const result = source[association.as]; 95 | if (options.handleConnection && isConnection(info.returnType)) { 96 | return handleConnection(result, args); 97 | } 98 | 99 | return result; 100 | } else { 101 | return source[association.accessors.get](findOptions).then(function (result) { 102 | if (options.handleConnection && isConnection(info.returnType)) { 103 | return handleConnection(result, args); 104 | } 105 | return result; 106 | }); 107 | } 108 | } 109 | 110 | return model[list ? 'findAll' : 'findOne'](findOptions); 111 | }).then(function (result) { 112 | return options.after(result, args, context, info); 113 | }); 114 | }; 115 | } 116 | 117 | resolverFactory.contextToOptions = {}; 118 | 119 | module.exports = resolverFactory; 120 | -------------------------------------------------------------------------------- /src/sequelizeOps.js: -------------------------------------------------------------------------------- 1 | import {Sequelize} from 'sequelize'; 2 | import {transform} from 'lodash'; 3 | const [seqMajVer] = Sequelize.version.split('.'); 4 | let ops; 5 | 6 | if (seqMajVer <= 3) { 7 | ops = { 8 | eq: '$eq', 9 | ne: '$ne', 10 | gte: '$gte', 11 | gt: '$gt', 12 | lte: '$lte', 13 | lt: '$lt', 14 | not: '$not', 15 | in: '$in', 16 | notIn: '$notIn', 17 | is: '$is', 18 | like: '$like', 19 | notLike: '$notLike', 20 | iLike: '$iLike', 21 | notILike: '$notILike', 22 | regexp: '$regexp', 23 | notRegexp: '$notRegexp', 24 | iRegexp: '$iRegexp', 25 | notIRegexp: '$notIRegexp', 26 | between: '$between', 27 | notBetween: '$notBetween', 28 | overlap: '$overlap', 29 | contains: '$contains', 30 | contained: '$contained', 31 | adjacent: '$adjacent', 32 | strictLeft: '$strictLeft', 33 | strictRight: '$strictRight', 34 | noExtendRight: '$noExtendRight', 35 | noExtendLeft: '$noExtendLeft', 36 | and: '$and', 37 | or: '$or', 38 | any: '$any', 39 | all: '$all', 40 | values: '$values', 41 | col: '$col', 42 | raw: '$raw' 43 | }; 44 | } else { 45 | ops = transform(Sequelize.Op, (o, v, k) => { 46 | if (typeof v !== 'symbol') { 47 | return; 48 | } 49 | o[k] = v; 50 | }); 51 | } 52 | 53 | export default ops; 54 | -------------------------------------------------------------------------------- /src/simplifyAST.js: -------------------------------------------------------------------------------- 1 | function deepMerge(a, b) { 2 | Object.keys(b).forEach(function (key) { 3 | if (['fields', 'args'].indexOf(key) !== -1) return; 4 | 5 | if (a[key] && b[key] && typeof a[key] === 'object' && typeof b[key] === 'object') { 6 | a[key] = deepMerge(a[key], b[key]); 7 | } else { 8 | a[key] = b[key]; 9 | } 10 | }); 11 | 12 | if (a.fields && b.fields) { 13 | a.fields = deepMerge(a.fields, b.fields); 14 | } else if (a.fields || b.fields) { 15 | a.fields = a.fields || b.fields; 16 | } 17 | 18 | return a; 19 | } 20 | 21 | function hasFragments(info) { 22 | return info.fragments && Object.keys(info.fragments).length > 0; 23 | } 24 | 25 | function isFragment(info, ast) { 26 | return hasFragments(info) && 27 | ast.name && 28 | info.fragments[ast.name.value] && 29 | ast.kind !== 'FragmentDefinition'; 30 | } 31 | 32 | function simplifyObjectValue(objectValue) { 33 | return objectValue.fields.reduce((memo, field) => { 34 | memo[field.name.value] = 35 | field.value.kind === 'IntValue' ? parseInt( field.value.value, 10 ) : 36 | field.value.kind === 'FloatValue' ? parseFloat( field.value.value ) : 37 | field.value.kind === 'ObjectValue' ? simplifyObjectValue( field.value ) : 38 | field.value.value; 39 | 40 | return memo; 41 | }, {}); 42 | } 43 | 44 | function simplifyValue(value, info) { 45 | if (value.values) { 46 | return value.values.map(value => simplifyValue(value, info)); 47 | } 48 | if ('value' in value) { 49 | return value.value; 50 | } 51 | if (value.kind === 'ObjectValue') { 52 | return simplifyObjectValue(value); 53 | } 54 | if (value.name && info.variableValues) { 55 | return info.variableValues[value.name.value]; 56 | } 57 | } 58 | 59 | module.exports = function simplifyAST(ast, info, parent) { 60 | var selections; 61 | info = info || {}; 62 | 63 | if (ast.selectionSet) selections = ast.selectionSet.selections; 64 | if (Array.isArray(ast)) { 65 | let simpleAST = {}; 66 | ast.forEach(ast => { 67 | simpleAST = deepMerge( 68 | simpleAST, simplifyAST(ast, info) 69 | ); 70 | }); 71 | 72 | return simpleAST; 73 | } 74 | 75 | if (isFragment(info, ast)) { 76 | return simplifyAST(info.fragments[ast.name.value], info); 77 | } 78 | 79 | if (!selections) return { 80 | fields: {}, 81 | args: {} 82 | }; 83 | 84 | return selections.reduce(function (simpleAST, selection) { 85 | if (selection.kind === 'FragmentSpread' || selection.kind === 'InlineFragment') { 86 | simpleAST = deepMerge( 87 | simpleAST, simplifyAST(selection, info) 88 | ); 89 | return simpleAST; 90 | } 91 | 92 | var name = selection.name.value 93 | , alias = selection.alias && selection.alias.value 94 | , key = alias || name; 95 | 96 | simpleAST.fields[key] = simpleAST.fields[key] || {}; 97 | simpleAST.fields[key] = deepMerge( 98 | simpleAST.fields[key], simplifyAST(selection, info, simpleAST.fields[key]) 99 | ); 100 | 101 | if (alias) { 102 | simpleAST.fields[key].key = name; 103 | } 104 | 105 | simpleAST.fields[key].args = selection.arguments.reduce(function (args, arg) { 106 | args[arg.name.value] = simplifyValue(arg.value, info); 107 | return args; 108 | }, {}); 109 | 110 | if (parent) { 111 | Object.defineProperty(simpleAST.fields[key], '$parent', { value: parent, enumerable: false }); 112 | } 113 | 114 | return simpleAST; 115 | }, { 116 | fields: {}, 117 | args: {} 118 | }); 119 | }; 120 | -------------------------------------------------------------------------------- /src/typeMapper.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInt, 3 | GraphQLString, 4 | GraphQLBoolean, 5 | GraphQLFloat, 6 | GraphQLEnumType, 7 | GraphQLList, 8 | } from 'graphql'; 9 | 10 | import DateType from './types/dateType'; 11 | import JSONType from './types/jsonType'; 12 | import _ from 'lodash'; 13 | 14 | let customTypeMapper; 15 | /** 16 | * A function to set a custom mapping of types 17 | * @param {Function} mapFunc 18 | */ 19 | export function mapType(mapFunc) { 20 | customTypeMapper = mapFunc; 21 | } 22 | 23 | /** 24 | * Checks the type of the sequelize data type and 25 | * returns the corresponding type in GraphQL 26 | * @param {Object} sequelizeType 27 | * @param {Object} sequelizeTypes 28 | * @return {Function} GraphQL type declaration 29 | */ 30 | export function toGraphQL(sequelizeType, sequelizeTypes) { 31 | 32 | // did the user supply a mapping function? 33 | // use their mapping, if it returns truthy 34 | // else use our defaults 35 | if (customTypeMapper) { 36 | let result = customTypeMapper(sequelizeType); 37 | if (result) return result; 38 | } 39 | 40 | const { 41 | BOOLEAN, 42 | ENUM, 43 | FLOAT, 44 | REAL, 45 | CHAR, 46 | DECIMAL, 47 | DOUBLE, 48 | INTEGER, 49 | BIGINT, 50 | STRING, 51 | TEXT, 52 | UUID, 53 | UUIDV4, 54 | DATE, 55 | DATEONLY, 56 | TIME, 57 | ARRAY, 58 | VIRTUAL, 59 | JSON, 60 | JSONB, 61 | CITEXT, 62 | INET, 63 | } = sequelizeTypes; 64 | 65 | // Map of special characters 66 | const specialCharsMap = new Map([ 67 | ['¼', 'frac14'], 68 | ['½', 'frac12'], 69 | ['¾', 'frac34'] 70 | ]); 71 | 72 | if (sequelizeType instanceof BOOLEAN) return GraphQLBoolean; 73 | 74 | if (sequelizeType instanceof FLOAT || 75 | sequelizeType instanceof REAL || 76 | sequelizeType instanceof DOUBLE) return GraphQLFloat; 77 | 78 | if (sequelizeType instanceof DATE) { 79 | return DateType; 80 | } 81 | 82 | if (sequelizeType instanceof CHAR || 83 | sequelizeType instanceof STRING || 84 | sequelizeType instanceof TEXT || 85 | sequelizeType instanceof UUID || 86 | sequelizeType instanceof UUIDV4 || 87 | sequelizeType instanceof DATEONLY || 88 | sequelizeType instanceof TIME || 89 | sequelizeType instanceof BIGINT || 90 | sequelizeType instanceof DECIMAL || 91 | sequelizeType instanceof CITEXT || 92 | sequelizeType instanceof INET) { 93 | return GraphQLString; 94 | } 95 | 96 | if (sequelizeType instanceof INTEGER) { 97 | return GraphQLInt; 98 | } 99 | 100 | if (sequelizeType instanceof ARRAY) { 101 | let elementType = toGraphQL(sequelizeType.type, sequelizeTypes); 102 | return new GraphQLList(elementType); 103 | } 104 | 105 | if (sequelizeType instanceof ENUM) { 106 | return new GraphQLEnumType({ 107 | name: 'tempEnumName', 108 | values: _(sequelizeType.values) 109 | .mapKeys(sanitizeEnumValue) 110 | .mapValues(v => ({value: v})) 111 | .value() 112 | }); 113 | } 114 | 115 | if (sequelizeType instanceof VIRTUAL) { 116 | let returnType = sequelizeType.returnType 117 | ? toGraphQL(sequelizeType.returnType, sequelizeTypes) 118 | : GraphQLString; 119 | return returnType; 120 | } 121 | 122 | if (sequelizeType instanceof JSONB || 123 | sequelizeType instanceof JSON) { 124 | return JSONType; 125 | } 126 | 127 | throw new Error(`Unable to convert ${sequelizeType.key || sequelizeType.toSql()} to a GraphQL type`); 128 | 129 | function sanitizeEnumValue(value) { 130 | return value 131 | .trim() 132 | .replace(/([^_a-zA-Z0-9])/g, (_, p) => specialCharsMap.get(p) || ' ') 133 | .split(' ') 134 | .map((v, i) => i ? _.upperFirst(v) : v) 135 | .join('') 136 | .replace(/(^\d)/, '_$1'); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/types/dateType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLScalarType 3 | } from 'graphql'; 4 | 5 | /** 6 | * A special custom Scalar type for Dates that converts to a ISO formatted string 7 | * @param {String} options.name: 8 | * @param {String} options.description: 9 | * @param {Date} options.serialize(d) 10 | * @param {String} parseValue(value) 11 | * @param {Object} parseLiteral(ast) 12 | */ 13 | export default new GraphQLScalarType({ 14 | name: 'Date', 15 | description: 'A special custom Scalar type for Dates that converts to a ISO formatted string ', 16 | /** 17 | * serialize 18 | * @param {Date} d Date obj 19 | * @return {String} Serialised date object 20 | */ 21 | serialize(d) { 22 | if (!d) { 23 | return null; 24 | } 25 | 26 | if (d instanceof Date) { 27 | return d.toISOString(); 28 | } 29 | return d; 30 | }, 31 | /** 32 | * parseValue 33 | * @param {String} value date string 34 | * @return {Date} Date object 35 | */ 36 | parseValue(value) { 37 | try { 38 | if (!value) { 39 | return null; 40 | } 41 | return new Date(value); 42 | } catch (e) { 43 | return null; 44 | } 45 | }, 46 | parseLiteral(ast) { 47 | return new Date(ast.value); 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /src/types/jsonType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLScalarType, 3 | GraphQLInt, 4 | GraphQLFloat, 5 | GraphQLBoolean, 6 | GraphQLString 7 | } from 'graphql'; 8 | import _ from 'lodash'; 9 | 10 | import { Kind } from 'graphql/language'; 11 | 12 | 13 | const astToJson = { 14 | [Kind.INT](ast) { 15 | return GraphQLInt.parseLiteral(ast); 16 | }, 17 | [Kind.FLOAT](ast) { 18 | return GraphQLFloat.parseLiteral(ast); 19 | }, 20 | [Kind.BOOLEAN](ast) { 21 | return GraphQLBoolean.parseLiteral(ast); 22 | }, 23 | [Kind.STRING](ast) { 24 | return GraphQLString.parseLiteral(ast); 25 | }, 26 | [Kind.ENUM](ast) { 27 | return String(ast.value); 28 | }, 29 | [Kind.LIST](ast) { 30 | return ast.values.map(astItem => { 31 | return JSONType.parseLiteral(astItem); 32 | }); 33 | }, 34 | [Kind.OBJECT](ast) { 35 | let obj = {}; 36 | ast.fields.forEach(field => { 37 | obj[field.name.value] = JSONType.parseLiteral(field.value); 38 | }); 39 | return obj; 40 | }, 41 | [Kind.VARIABLE](ast) { 42 | /* 43 | this way converted query variables would be easily 44 | converted to actual values in the resolver.js by just 45 | passing the query variables object in to function below. 46 | We can`t convert them just in here because query variables 47 | are not accessible from GraphQLScalarType's parseLiteral method 48 | */ 49 | return _.property(ast.name.value); 50 | } 51 | }; 52 | 53 | 54 | const JSONType = new GraphQLScalarType({ 55 | name: 'SequelizeJSON', 56 | description: 'The `JSON` scalar type represents raw JSON as values.', 57 | serialize: value => value, 58 | parseValue: value => typeof value === 'string' ? JSON.parse(value) : value, 59 | parseLiteral: ast => { 60 | const parser = astToJson[ast.kind]; 61 | return parser ? parser.call(this, ast) : null; 62 | } 63 | }); 64 | 65 | 66 | export default JSONType; 67 | -------------------------------------------------------------------------------- /test/benchmark.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | 3 | var express = require('express'); 4 | var graphqlHTTP = require('express-graphql'); 5 | var schema = require('./benchmark/schema').schema; 6 | var app = express(); 7 | 8 | /** 9 | * HOW TO run the benchmarks: 10 | * 11 | * sudo docker-compose up -d postgres 12 | * sudo docker-compose run benchmark_server node test/benchmark/seed.js 13 | * npm run build && sudo docker-compose kill benchmark_server && sudo docker-compose up -d benchmark_server 14 | * ab -p test/benchmark/[FILE].json -T application/json -n 500 -c 20 http://localhost:4001/graphql 15 | */ 16 | 17 | app.use('/graphql', graphqlHTTP({ 18 | schema, 19 | formatError: error => { 20 | console.log(error.stack); 21 | return { 22 | message: error.message, 23 | locations: error.locations, 24 | stack: error.stack 25 | }; 26 | } 27 | })); 28 | 29 | app.listen(4001, function () { 30 | console.log('Benchmarking server listening on port 4001'); 31 | }); 32 | -------------------------------------------------------------------------------- /test/benchmark/hasManyWhere.json: -------------------------------------------------------------------------------- 1 | { "query": "{ users(limit: 25) { tasks(completed: true) { edges { node { name, completed } } } } }" } 2 | -------------------------------------------------------------------------------- /test/benchmark/models.js: -------------------------------------------------------------------------------- 1 | import {createSequelize} from '../support/helper'; 2 | import Sequelize from 'sequelize'; 3 | const sequelize = createSequelize({ 4 | pool: { 5 | max: 25 6 | } 7 | }); 8 | 9 | const User = sequelize.define('user', { 10 | name: Sequelize.STRING 11 | }, { 12 | timestamps: false 13 | }); 14 | 15 | const Task = sequelize.define('task', { 16 | name: Sequelize.STRING, 17 | completed: Sequelize.BOOLEAN 18 | }, { 19 | timestamps: false 20 | }); 21 | 22 | const Project = sequelize.define('project'); 23 | const ProjectUser = sequelize.define('project_user'); 24 | 25 | User.Tasks = User.hasMany(Task, {as: 'taskItems', foreignKey: 'user_id'}); 26 | User.Subordinates = User.hasMany(User, { as: 'subordinates', foreignKey: 'manager_id', constraints: false }); 27 | Task.User = Task.belongsTo(User, { foreignKey: 'user_id' }); 28 | Task.SubTask = Task.hasMany(Task, { as: 'subTasks', foreignKey: 'parent_id', constraints: false}); 29 | Project.Users = Project.belongsToMany(User, { through: ProjectUser, foreignKey: 'project_id', otherKey: 'user_id' }); 30 | User.Projects = User.belongsToMany(Project, { through: ProjectUser, foreignKey: 'user_id', otherKey: 'project_id' }); 31 | 32 | const models = { 33 | User, 34 | Task, 35 | Project, 36 | ProjectUser 37 | }; 38 | 39 | export { 40 | sequelize, 41 | models 42 | }; 43 | -------------------------------------------------------------------------------- /test/benchmark/nestedBelongsToMany.json: -------------------------------------------------------------------------------- 1 | { "query": "{ projects { users { edges { node { name, tasks { edges { node { name } } } } } } } }" } 2 | -------------------------------------------------------------------------------- /test/benchmark/nestedBelongsToManyLimit.json: -------------------------------------------------------------------------------- 1 | { "query": "{ projects { users(first: 15) { edges { node { name, tasks { edges { node { name } } } } } } } }" } 2 | -------------------------------------------------------------------------------- /test/benchmark/nestedBelongsToManyWhere.json: -------------------------------------------------------------------------------- 1 | { "query": "{ projects { users { edges { node { name, tasks (completed: true) { edges { node { name } } } } } } } }" } 2 | -------------------------------------------------------------------------------- /test/benchmark/nestedHasMany.json: -------------------------------------------------------------------------------- 1 | { "query": "{ users(limit: 250) { tasks(completed: true) { edges { node { name, subTasks { edges { node { name } } } } } } } }" } 2 | -------------------------------------------------------------------------------- /test/benchmark/schema.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLString, 3 | GraphQLInt, 4 | GraphQLList, 5 | GraphQLObjectType, 6 | GraphQLSchema, 7 | GraphQLBoolean 8 | } from 'graphql'; 9 | 10 | import { 11 | globalIdField, 12 | connectionDefinitions, 13 | connectionArgs 14 | } from 'graphql-relay'; 15 | 16 | import resolver from '../../lib/resolver'; 17 | 18 | import { 19 | sequelizeNodeInterface 20 | } from '../../lib/relay'; 21 | 22 | import { sequelize, models } from './models'; 23 | 24 | const node = sequelizeNodeInterface(sequelize); 25 | const nodeInterface = node.nodeInterface; 26 | 27 | const taskType = new GraphQLObjectType({ 28 | name: 'Task', 29 | fields: () => ({ 30 | id: globalIdField('Task'), 31 | completed: { 32 | type: GraphQLBoolean 33 | }, 34 | name: { 35 | type: GraphQLString 36 | }, 37 | user: { 38 | type: userType, 39 | resolve: resolver(models.Task.User) 40 | }, 41 | subTasks: { 42 | type: subtaskConnection.connectionType, 43 | args: connectionArgs, 44 | resolve: resolver(models.Task.SubTask) 45 | } 46 | }), 47 | interfaces: [nodeInterface] 48 | }); 49 | 50 | const userType = new GraphQLObjectType({ 51 | name: 'User', 52 | fields: () => ({ 53 | id: globalIdField('User'), 54 | name: { 55 | type: GraphQLString 56 | }, 57 | tasks: { 58 | type: taskConnection.connectionType, 59 | args: { 60 | completed: { 61 | type: GraphQLBoolean, 62 | }, 63 | ...connectionArgs 64 | }, 65 | resolve: resolver(models.User.Tasks, { 66 | before: (options, args) => { 67 | if (args.hasOwnProperty('completed')) { 68 | options.where = { 69 | completed: args.completed 70 | }; 71 | } 72 | 73 | return options; 74 | } 75 | }) 76 | }, 77 | subordinates: { 78 | type: subordinateConnection.connectionType, 79 | args: connectionArgs, 80 | resolve: resolver(models.User.Subordinates) 81 | } 82 | }), 83 | interfaces: [nodeInterface] 84 | }); 85 | 86 | const projectType = new GraphQLObjectType({ 87 | name: 'Project', 88 | fields: () => ({ 89 | id: globalIdField('Project'), 90 | users: { 91 | type: userConnection.connectionType, 92 | args: connectionArgs, 93 | resolve: resolver(models.Project.Users) 94 | } 95 | }), 96 | interfaces: [nodeInterface] 97 | }); 98 | 99 | const taskConnection = connectionDefinitions({name: 'Task', nodeType: taskType}) 100 | , subordinateConnection = connectionDefinitions({name: 'Subordinate', nodeType: userType}) 101 | , subtaskConnection = connectionDefinitions({name: 'Subtask', nodeType: taskType}) 102 | , userConnection = connectionDefinitions({name: 'User', nodeType: userType}); 103 | 104 | const schema = new GraphQLSchema({ 105 | query: new GraphQLObjectType({ 106 | name: 'RootQueryType', 107 | fields: { 108 | projects: { 109 | type: new GraphQLList(projectType), 110 | args: { 111 | limit: { 112 | type: GraphQLInt 113 | }, 114 | order: { 115 | type: GraphQLString 116 | } 117 | }, 118 | resolve: resolver(models.Project) 119 | }, 120 | users: { 121 | type: new GraphQLList(userType), 122 | args: { 123 | limit: { 124 | type: GraphQLInt 125 | }, 126 | order: { 127 | type: GraphQLString 128 | } 129 | }, 130 | resolve: resolver(models.User) 131 | }, 132 | tasks: { 133 | type: new GraphQLList(taskType), 134 | args: { 135 | limit: { 136 | type: GraphQLInt 137 | }, 138 | order: { 139 | type: GraphQLString 140 | } 141 | }, 142 | resolve: resolver(models.Task, { 143 | before: findOptions => { 144 | // we only want top-level tasks, not subtasks 145 | findOptions.where = { 146 | user_id: { // eslint-disable-line 147 | $not: null 148 | } 149 | }; 150 | return findOptions; 151 | } 152 | }) 153 | } 154 | } 155 | }) 156 | }); 157 | 158 | export { 159 | schema 160 | }; 161 | -------------------------------------------------------------------------------- /test/benchmark/seed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel-register'); 4 | 5 | var models = require('./models').models 6 | , sequelize = require('./models').sequelize; 7 | 8 | const NO_USERS = 1000; 9 | const NO_TASKS = 10000; 10 | const NO_SUBTASKS = 10; 11 | const NO_PROJECTS = 10; 12 | 13 | function randomInt(max) { 14 | const min = 1; 15 | return Math.floor(Math.random() * (max - min + 1)) + min; 16 | } 17 | 18 | return sequelize.sync({ force: true, logging: console.log }).then(function () { 19 | let users = []; 20 | 21 | for (var i = 0; i < NO_USERS; i++) { 22 | users.push({ 23 | name: Math.random().toString(), 24 | manager_id: randomInt(NO_USERS) // eslint-disable-line 25 | }); 26 | } 27 | 28 | return models.User.bulkCreate(users); 29 | }).then(function () { 30 | let tasks = []; 31 | 32 | for (var i = 0; i < NO_TASKS; i++) { 33 | tasks.push({ 34 | name: Math.random().toString(), 35 | completed: Math.random() > 0.5, 36 | user_id: randomInt(NO_USERS) // eslint-disable-line 37 | }); 38 | } 39 | 40 | return models.Task.bulkCreate(tasks); 41 | }).then(() => { 42 | let subTasks = []; 43 | 44 | for (var i = 1; i <= NO_TASKS; i++) { 45 | for (var j = 0; j < NO_SUBTASKS; j++) { 46 | subTasks.push({ 47 | name: Math.random().toString(), 48 | completed: Math.random() > 0.5, 49 | parent_id: i // eslint-disable-line 50 | }); 51 | } 52 | } 53 | 54 | return models.Task.bulkCreate(subTasks); 55 | }).then(() => { 56 | let projects = []; 57 | 58 | for (var i = 0; i < NO_PROJECTS; i++) { 59 | projects.push({}); 60 | } 61 | 62 | return models.Project.bulkCreate(projects); 63 | }).then(() => { 64 | let projectUsers = []; 65 | 66 | for (var i = 1; i <= NO_PROJECTS; i++) { 67 | let userIds = []; 68 | while (userIds.length < 25) { 69 | let userId = randomInt(NO_USERS); 70 | if (userIds.indexOf(userId) === -1) { 71 | userIds.push(userId); 72 | } 73 | } 74 | /* eslint-disable camelcase */ 75 | userIds.forEach(user_id => { 76 | projectUsers.push({ 77 | project_id: i, 78 | user_id 79 | }); 80 | }); 81 | /* eslint-enable camelcase */ 82 | } 83 | 84 | return models.ProjectUser.bulkCreate(projectUsers); 85 | }).catch(e => { 86 | console.log(e); 87 | throw e; 88 | }).then(() => sequelize.close()); 89 | -------------------------------------------------------------------------------- /test/benchmark/singleBelongsTo.json: -------------------------------------------------------------------------------- 1 | { "query": "{ tasks(limit: 250) { user { name } } }" } 2 | -------------------------------------------------------------------------------- /test/benchmark/singleBelongsToMany.json: -------------------------------------------------------------------------------- 1 | { "query": "{ projects { users { edges { node { name } } } } }" } 2 | -------------------------------------------------------------------------------- /test/benchmark/singleBelongsToManyLimit.json: -------------------------------------------------------------------------------- 1 | { "query": "{ projects { users(first: 15) { edges { node { name } } } } }" } 2 | -------------------------------------------------------------------------------- /test/benchmark/singleHasMany.json: -------------------------------------------------------------------------------- 1 | { "query": "{ users(limit: 25) { tasks { edges { node { name } } } } }" } 2 | -------------------------------------------------------------------------------- /test/benchmark/twoHasMany.json: -------------------------------------------------------------------------------- 1 | { "query": "{ users(limit: 25) { tasks { edges { node { name } } }, subordinates { edges { node { name } } } } }" } 2 | -------------------------------------------------------------------------------- /test/integration/relay.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { sequelize, beforeRemoveAllTables } from '../support/helper'; 4 | 5 | import { expect } from 'chai'; 6 | import resolver from '../../src/resolver'; 7 | import Sequelize from 'sequelize'; 8 | import sinon from'sinon'; 9 | 10 | import { 11 | GraphQLString, 12 | GraphQLInt, 13 | GraphQLNonNull, 14 | GraphQLList, 15 | GraphQLObjectType, 16 | GraphQLSchema, 17 | graphql 18 | } from 'graphql'; 19 | 20 | import { 21 | sequelizeNodeInterface 22 | } from '../../src/relay'; 23 | 24 | import { 25 | globalIdField, 26 | toGlobalId, 27 | fromGlobalId, 28 | connectionDefinitions, 29 | connectionArgs 30 | } from 'graphql-relay'; 31 | 32 | function generateTask(id) { 33 | return { 34 | id: id, 35 | name: Math.random().toString() 36 | }; 37 | } 38 | 39 | async function generateCustom(id) { 40 | return { 41 | id, 42 | value: `custom type ${ id }` 43 | }; 44 | } 45 | 46 | describe('relay', function () { 47 | beforeRemoveAllTables(); 48 | 49 | var User 50 | , Task 51 | , userType 52 | , taskType 53 | , nodeInterface 54 | , Project 55 | , projectType 56 | , viewerType 57 | , nodeField 58 | , schema; 59 | 60 | before(() => { 61 | sequelize.modelManager.models = []; 62 | sequelize.models = {}; 63 | User = sequelize.define('User', { 64 | name: { 65 | type: Sequelize.STRING 66 | } 67 | }, { 68 | timestamps: false 69 | }); 70 | 71 | Task = sequelize.define('Task', { 72 | name: { 73 | type: Sequelize.STRING 74 | } 75 | }, { 76 | timestamps: false 77 | }); 78 | 79 | Project = sequelize.define('Project', { 80 | name: { 81 | type: Sequelize.STRING 82 | } 83 | }, { 84 | timestamps: false 85 | }); 86 | 87 | User.Tasks = User.hasMany(Task, {as: 'taskItems'}); // Specifically different from connection type name 88 | Project.Users = Project.hasMany(User, {as: 'users'}); 89 | 90 | 91 | var node = sequelizeNodeInterface(sequelize); 92 | nodeInterface = node.nodeInterface; 93 | nodeField = node.nodeField; 94 | var nodeTypeMapper = node.nodeTypeMapper; 95 | 96 | taskType = new GraphQLObjectType({ 97 | name: 'Task', 98 | fields: { 99 | id: globalIdField('Task'), 100 | name: { 101 | type: GraphQLString 102 | } 103 | }, 104 | interfaces: [nodeInterface] 105 | }); 106 | 107 | var taskConnection = connectionDefinitions({name: 'Task', nodeType: taskType}); 108 | 109 | userType = new GraphQLObjectType({ 110 | name: 'User', 111 | fields: { 112 | id: globalIdField('User'), 113 | name: { 114 | type: GraphQLString 115 | }, 116 | tasks: { 117 | type: taskConnection.connectionType, 118 | args: connectionArgs, 119 | resolve: resolver(User.Tasks) 120 | } 121 | }, 122 | interfaces: [nodeInterface] 123 | }); 124 | 125 | var userConnection = connectionDefinitions({name: 'User', nodeType: userType}); 126 | 127 | projectType = new GraphQLObjectType({ 128 | name: 'Project', 129 | fields: { 130 | id: globalIdField('User'), 131 | name: { 132 | type: GraphQLString 133 | }, 134 | users: { 135 | type: userConnection.connectionType, 136 | args: connectionArgs, 137 | resolve: resolver(Project.Users) 138 | } 139 | }, 140 | interfaces: [nodeInterface] 141 | }); 142 | 143 | viewerType = new GraphQLObjectType({ 144 | name: 'Viewer', 145 | description: 'root viewer for queries', 146 | fields: () => ({ 147 | id: globalIdField('Viewer', () => 1), 148 | name: { 149 | type: GraphQLString, 150 | resolve: () => 'Viewer!' 151 | }, 152 | allProjects: { 153 | type: new GraphQLList(projectType), 154 | resolve: resolver(Project) 155 | } 156 | }), 157 | interfaces: [nodeInterface] 158 | }); 159 | 160 | const customType = new GraphQLObjectType({ 161 | name: 'Custom', 162 | description: 'Custom type to test custom idFetcher', 163 | fields: { 164 | id: globalIdField('Custom'), 165 | value: { 166 | type: GraphQLString, 167 | } 168 | }, 169 | interfaces: [nodeInterface] 170 | }); 171 | 172 | nodeTypeMapper.mapTypes({ 173 | [User.name]: { type: 'User' }, 174 | [Project.name]: { type: projectType}, 175 | [Task.name]: { type: taskType }, 176 | Viewer: { type: viewerType }, 177 | [customType.name]: { 178 | type: 'Custom', 179 | resolve(globalId) { 180 | const { id } = fromGlobalId(globalId); 181 | return generateCustom(id); 182 | } 183 | } 184 | }); 185 | 186 | schema = new GraphQLSchema({ 187 | query: new GraphQLObjectType({ 188 | name: 'RootQueryType', 189 | fields: { 190 | viewer: { 191 | type: viewerType, 192 | resolve: () => ({ 193 | name: 'Viewer!', 194 | id: 1 195 | }) 196 | }, 197 | user: { 198 | type: userType, 199 | args: { 200 | id: { 201 | type: new GraphQLNonNull(GraphQLInt) 202 | } 203 | }, 204 | resolve: resolver(User) 205 | }, 206 | users: { 207 | type: new GraphQLList(userType), 208 | args: { 209 | limit: { 210 | type: GraphQLInt 211 | }, 212 | order: { 213 | type: GraphQLString 214 | } 215 | }, 216 | resolve: resolver(User) 217 | }, 218 | project: { 219 | type: projectType, 220 | args: { 221 | id: { 222 | type: new GraphQLNonNull(GraphQLInt) 223 | } 224 | }, 225 | resolve: resolver(Project) 226 | }, 227 | custom: { 228 | type: customType, 229 | args: { 230 | id: { 231 | type: new GraphQLNonNull(GraphQLInt) 232 | } 233 | }, 234 | resolve: generateCustom 235 | }, 236 | node: nodeField 237 | } 238 | }) 239 | }); 240 | }); 241 | 242 | before(() => { 243 | var userId = 1 244 | , projectId = 1 245 | , taskId = 1; 246 | 247 | return sequelize.sync({ force: true }).then(() => { 248 | return Promise.all([ 249 | Project.create({ 250 | id: projectId++, 251 | name: 'project-' + Math.random().toString() 252 | }), 253 | User.create({ 254 | id: userId++, 255 | name: 'a' + Math.random().toString(), 256 | [User.Tasks.as]: [generateTask(taskId++), generateTask(taskId++), generateTask(taskId++)] 257 | }, { 258 | include: [User.Tasks] 259 | }), 260 | User.create({ 261 | id: userId++, 262 | name: 'b' + Math.random().toString(), 263 | [User.Tasks.as]: [generateTask(taskId++), generateTask(taskId++)] 264 | }, { 265 | include: [User.Tasks] 266 | }) 267 | ]).then(([project, userA, userB]) => { 268 | this.project = project; 269 | this.userA = userA; 270 | this.userB = userB; 271 | this.users = [userA, userB]; 272 | }); 273 | }); 274 | }); 275 | 276 | before(() => { 277 | return this.project.setUsers([this.userA.id, this.userB.id]); 278 | }); 279 | 280 | it('should support unassociated GraphQL types', () => { 281 | var globalId = toGlobalId('Viewer', 1); 282 | 283 | return graphql({ 284 | schema, 285 | source: ` 286 | { 287 | node(id: "${globalId}") { 288 | id 289 | } 290 | } 291 | `}).then(result => { 292 | expect(result.data.node.id).to.equal(globalId); 293 | }); 294 | 295 | }); 296 | 297 | it('should return userA when running a node query', () => { 298 | var user = this.userA 299 | , globalId = toGlobalId('User', user.id); 300 | 301 | return graphql({ 302 | schema, 303 | source: ` 304 | { 305 | node(id: "${globalId}") { 306 | id 307 | ... on User { 308 | name 309 | } 310 | } 311 | } 312 | ` 313 | }).then(result => { 314 | expect(result.data.node.id).to.equal(globalId); 315 | expect(result.data.node.name).to.equal(user.name); 316 | }); 317 | }); 318 | 319 | describe('node queries', () => { 320 | it('should allow returning a custom entity', () => { 321 | generateCustom(1).then(async custom => { 322 | const globalId = toGlobalId('Custom', custom.id); 323 | 324 | return graphql({ 325 | schema, 326 | source: ` 327 | { 328 | node(id: "${globalId}") { 329 | id 330 | ... on Custom { 331 | value 332 | } 333 | } 334 | } 335 | ` 336 | }).then(result => { 337 | expect(result.data.node.id).to.equal(globalId); 338 | expect(result.data.node.value).to.equal(custom.value); 339 | }); 340 | }); 341 | }); 342 | 343 | it('should merge nested queries from multiple fragments', () => { 344 | var globalId = toGlobalId('Viewer', 1); 345 | 346 | return graphql({ 347 | schema, 348 | source: ` 349 | { 350 | node(id: "${globalId}") { 351 | id 352 | ...F0 353 | ...F1 354 | } 355 | } 356 | fragment F0 on Viewer { 357 | allProjects { 358 | id 359 | } 360 | } 361 | fragment F1 on Viewer { 362 | allProjects { 363 | id 364 | name 365 | } 366 | } 367 | ` 368 | }).then(result => { 369 | if (result.errors) throw result.errors[0]; 370 | 371 | expect(result.data.node.allProjects[0].id).to.not.be.null; 372 | expect(result.data.node.allProjects[0].name).to.not.be.null; 373 | }); 374 | }); 375 | }); 376 | 377 | it('should support first queries on connections', () => { 378 | var user = this.userB; 379 | 380 | return graphql({ 381 | schema, 382 | source: ` 383 | { 384 | user(id: ${user.id}) { 385 | name 386 | tasks(first: 1) { 387 | edges { 388 | node { 389 | name 390 | } 391 | } 392 | } 393 | } 394 | } 395 | ` 396 | }).then((result) => { 397 | if (result.errors) throw new Error(result.errors[0].stack); 398 | 399 | expect(result.data).to.deep.equal({ 400 | user: { 401 | name: user.name, 402 | tasks: { 403 | edges: [ 404 | { 405 | node: { 406 | name: user.taskItems[0].name 407 | } 408 | } 409 | ] 410 | } 411 | } 412 | }); 413 | }); 414 | }); 415 | 416 | it('should support last queries on connections', () => { 417 | var user = this.userB; 418 | 419 | return graphql({ 420 | schema, 421 | source: ` 422 | { 423 | user(id: ${user.id}) { 424 | name 425 | tasks(last: 1) { 426 | edges { 427 | node { 428 | name 429 | } 430 | } 431 | } 432 | } 433 | } 434 | ` 435 | }).then((result) => { 436 | if (result.errors) throw new Error(result.errors[0].stack); 437 | 438 | expect(result.data).to.deep.equal({ 439 | user: { 440 | name: user.name, 441 | tasks: { 442 | edges: [ 443 | { 444 | node: { 445 | name: user[User.Tasks.as][user[User.Tasks.as].length - 1].name 446 | } 447 | } 448 | ] 449 | } 450 | } 451 | }); 452 | }); 453 | }); 454 | 455 | // these two tests are not determenistic on postgres currently 456 | it('should support after queries on connections', () => { 457 | var user = this.userA; 458 | 459 | return graphql({ 460 | schema, 461 | source: ` 462 | { 463 | user(id: ${user.id}) { 464 | name 465 | tasks(first: 1) { 466 | pageInfo { 467 | hasNextPage, 468 | startCursor 469 | }, 470 | edges { 471 | node { 472 | name 473 | } 474 | } 475 | } 476 | } 477 | } 478 | ` 479 | }) 480 | .then((result) => { 481 | return graphql({ 482 | schema, 483 | source: ` 484 | { 485 | user(id: ${user.id}) { 486 | name 487 | tasks(first: 1, after: "${result.data.user.tasks.pageInfo.startCursor}") { 488 | edges { 489 | node { 490 | name 491 | } 492 | } 493 | } 494 | } 495 | } 496 | ` 497 | }); 498 | }).then((result) => { 499 | expect(result.data.user.tasks.edges[0].node.name).to.equal(user.taskItems[1].name); 500 | }); 501 | }); 502 | 503 | it('should resolve a plain result with a single connection', () => { 504 | var user = this.userB; 505 | 506 | return graphql({ 507 | schema, 508 | source: ` 509 | { 510 | user(id: ${user.id}) { 511 | name 512 | tasks { 513 | edges { 514 | node { 515 | name 516 | } 517 | } 518 | } 519 | } 520 | } 521 | ` 522 | }).then((result) => { 523 | if (result.errors) throw new Error(result.errors[0].stack); 524 | 525 | expect(result.data).to.deep.equal({ 526 | user: { 527 | name: user.name, 528 | tasks: { 529 | edges: [ 530 | { 531 | node: { 532 | name: user.taskItems[0].name 533 | } 534 | }, 535 | { 536 | node: { 537 | name: user.taskItems[1].name 538 | } 539 | } 540 | ] 541 | } 542 | } 543 | }); 544 | }); 545 | }); 546 | 547 | it('should resolve an array of objects containing connections', () => { 548 | var users = this.users; 549 | 550 | return graphql({ 551 | schema, 552 | source: ` 553 | { 554 | users { 555 | name 556 | tasks { 557 | edges { 558 | node { 559 | name 560 | } 561 | } 562 | } 563 | } 564 | } 565 | ` 566 | }).then((result) => { 567 | if (result.errors) throw new Error(result.errors[0].stack); 568 | 569 | expect(result.data.users.length).to.equal(users.length); 570 | result.data.users.forEach(function (user) { 571 | expect(user.tasks.edges).to.have.length.above(0); 572 | }); 573 | 574 | }); 575 | }); 576 | 577 | it('should resolve nested connections', () => { 578 | var sqlSpy = sinon.spy(); 579 | 580 | return graphql({ 581 | schema, 582 | source: ` 583 | { 584 | project(id: 1) { 585 | users { 586 | edges { 587 | node { 588 | name 589 | tasks { 590 | edges { 591 | node { 592 | name 593 | } 594 | } 595 | } 596 | } 597 | } 598 | } 599 | } 600 | } 601 | `, 602 | }).then(result => { 603 | if (result.errors) throw new Error(result.errors[0].stack); 604 | 605 | expect(result.data.project.users.edges).to.have.length(2); 606 | let [nodeA, nodeB] = result.data.project.users.edges; 607 | let userA = nodeA.node; 608 | let userB = nodeB.node; 609 | 610 | expect(userA).to.have.property('tasks'); 611 | expect(userA.tasks.edges).to.have.length.above(0); 612 | expect(userA.tasks.edges[0].node.name).to.be.ok; 613 | 614 | expect(userB).to.have.property('tasks'); 615 | expect(userB.tasks.edges).to.have.length.above(0); 616 | expect(userB.tasks.edges[0].node.name).to.be.ok; 617 | }); 618 | }); 619 | 620 | it('should support fragments', () => { 621 | return graphql({ 622 | schema, 623 | source: ` 624 | { 625 | project(id: 1) { 626 | ...getNames 627 | } 628 | } 629 | fragment getNames on Project { 630 | name 631 | } 632 | ` 633 | }).then(result => { 634 | if (result.errors) throw new Error(result.errors[0].stack); 635 | }); 636 | }); 637 | 638 | it('should support inline fragments', () => { 639 | return graphql({ 640 | schema, 641 | source: ` 642 | { 643 | project(id: 1) { 644 | ... on Project { 645 | name 646 | } 647 | } 648 | } 649 | ` 650 | }).then(result => { 651 | if (result.errors) throw new Error(result.errors[0].stack); 652 | }); 653 | }); 654 | 655 | it('should not support fragments on the wrong type', () => { 656 | return graphql({ 657 | schema, 658 | source: ` 659 | { 660 | project(id: 1) { 661 | ...getNames 662 | } 663 | } 664 | fragment getNames on User { 665 | name 666 | } 667 | ` 668 | }).then(result => { 669 | expect(result.errors).to.exist.and.have.length(1); 670 | }); 671 | }); 672 | }); 673 | -------------------------------------------------------------------------------- /test/integration/resolver.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { delay, sequelize, beforeRemoveAllTables } from '../support/helper'; 4 | 5 | import { expect } from 'chai'; 6 | import sinon from 'sinon'; 7 | import Sequelize, { Op } from 'sequelize'; 8 | 9 | import resolver from '../../src/resolver'; 10 | import JSONType from '../../src/types/jsonType'; 11 | 12 | import { 13 | graphql, 14 | GraphQLSchema, 15 | GraphQLObjectType, 16 | GraphQLString, 17 | GraphQLInt, 18 | GraphQLNonNull, 19 | GraphQLList 20 | } from 'graphql'; 21 | 22 | describe('resolver', function () { 23 | beforeRemoveAllTables(); 24 | 25 | var User 26 | , Task 27 | , Project 28 | , Label 29 | , taskType 30 | , userType 31 | , projectType 32 | , labelType 33 | , schema; 34 | 35 | /** 36 | * Setup the a) testing db schema and b) the according GraphQL types 37 | * 38 | * The schema consists of a User that has Tasks. 39 | * A Task belongs to a Project, which can have Labels. 40 | */ 41 | before(() => { 42 | this.sandbox = sinon.sandbox.create(); 43 | 44 | sequelize.modelManager.models = []; 45 | sequelize.models = {}; 46 | 47 | User = sequelize.define('user', { 48 | name: Sequelize.STRING, 49 | myVirtual: { 50 | type: Sequelize.VIRTUAL, 51 | get: function () { 52 | return 'lol'; 53 | } 54 | } 55 | }); 56 | 57 | Task = sequelize.define('task', { 58 | title: Sequelize.STRING, 59 | createdAt: { 60 | type: Sequelize.DATE, 61 | field: 'created_at', 62 | defaultValue: Sequelize.NOW 63 | }, 64 | taskVirtual: { 65 | type: Sequelize.VIRTUAL, 66 | get: function () { 67 | return 'tasktask'; 68 | } 69 | } 70 | }, { 71 | timestamps: false 72 | }); 73 | 74 | Project = sequelize.define('project', { 75 | name: Sequelize.STRING 76 | }, { 77 | timestamps: false 78 | }); 79 | 80 | Label = sequelize.define('label', { 81 | name: Sequelize.STRING 82 | }, { 83 | timestamps: false 84 | }); 85 | 86 | User.Tasks = User.hasMany(Task, {as: 'tasks', foreignKey: 'userId'}); 87 | Task.User = Task.belongsTo(User, {as: 'user', foreignKey: 'userId'}); 88 | 89 | Task.Project = Task.belongsTo(Project, {as: 'project', foreignKey: 'projectId'}); 90 | Project.Labels = Project.hasMany(Label, {as: 'labels'}); 91 | 92 | labelType = new GraphQLObjectType({ 93 | name: 'Label', 94 | fields: { 95 | id: { 96 | type: new GraphQLNonNull(GraphQLInt) 97 | }, 98 | name: { 99 | type: GraphQLString 100 | } 101 | } 102 | }); 103 | 104 | projectType = new GraphQLObjectType({ 105 | name: 'Project', 106 | fields: { 107 | id: { 108 | type: new GraphQLNonNull(GraphQLInt) 109 | }, 110 | name: { 111 | type: GraphQLString 112 | }, 113 | labels: { 114 | type: new GraphQLList(labelType), 115 | resolve: resolver(Project.Labels) 116 | } 117 | } 118 | }); 119 | 120 | taskType = new GraphQLObjectType({ 121 | name: 'Task', 122 | description: 'A task', 123 | fields: { 124 | id: { 125 | type: new GraphQLNonNull(GraphQLInt) 126 | }, 127 | title: { 128 | type: GraphQLString 129 | }, 130 | taskVirtual: { 131 | type: GraphQLString 132 | }, 133 | project: { 134 | type: projectType, 135 | resolve: resolver(Task.Project) 136 | } 137 | } 138 | }); 139 | 140 | userType = new GraphQLObjectType({ 141 | name: 'User', 142 | description: 'A user', 143 | fields: { 144 | id: { 145 | type: new GraphQLNonNull(GraphQLInt), 146 | }, 147 | name: { 148 | type: GraphQLString, 149 | }, 150 | myVirtual: { 151 | type: GraphQLString 152 | }, 153 | tasks: { 154 | type: new GraphQLList(taskType), 155 | args: { 156 | limit: { 157 | type: GraphQLInt 158 | }, 159 | offset: { 160 | type: GraphQLInt 161 | }, 162 | order: { 163 | type: GraphQLString 164 | }, 165 | first: { 166 | type: GraphQLInt 167 | } 168 | }, 169 | resolve: resolver(() => User.Tasks, { 170 | before: function (options, args) { 171 | if (args.first) { 172 | options.order = options.order || []; 173 | options.order.push(['created_at', 'ASC']); 174 | 175 | if (args.first !== 0) { 176 | options.limit = args.first; 177 | } 178 | } 179 | 180 | return options; 181 | } 182 | }) 183 | }, 184 | tasksByIds: { 185 | type: new GraphQLList(taskType), 186 | args: { 187 | ids: { 188 | type: new GraphQLList(GraphQLInt) 189 | } 190 | }, 191 | resolve: resolver(User.Tasks, { 192 | before: (options, args) => { 193 | options.where = options.where || {}; 194 | options.where.id = { [Op.in]: args.ids }; 195 | 196 | return options; 197 | } 198 | }) 199 | } 200 | } 201 | }); 202 | 203 | schema = new GraphQLSchema({ 204 | query: new GraphQLObjectType({ 205 | name: 'RootQueryType', 206 | fields: { 207 | user: { 208 | type: userType, 209 | args: { 210 | id: { 211 | type: new GraphQLNonNull(GraphQLInt) 212 | } 213 | }, 214 | resolve: resolver(User, { 215 | contextToOptions: { 216 | a: 'a', 217 | b: 'c' 218 | } 219 | }) 220 | }, 221 | users: { 222 | type: new GraphQLList(userType), 223 | args: { 224 | limit: { 225 | type: GraphQLInt 226 | }, 227 | order: { 228 | type: GraphQLString 229 | }, 230 | where: { 231 | type: JSONType 232 | } 233 | }, 234 | resolve: resolver(User) 235 | } 236 | } 237 | }) 238 | }); 239 | }); 240 | 241 | /** 242 | * Now fill the testing DB with fixture values 243 | * We'll have projectA & projectB with two random labels each, 244 | * and two users each with some tasks that belong to those projects. 245 | */ 246 | before(() => { 247 | var taskId = 0 248 | , projectId = 0; 249 | 250 | return sequelize.sync({force: true}).then(() => { 251 | return Promise.all([ 252 | Project.create({ 253 | id: ++projectId, 254 | name: 'b' + Math.random().toString(), 255 | labels: [ 256 | {name: Math.random().toString()}, 257 | {name: Math.random().toString()} 258 | ] 259 | }, { 260 | include: [ 261 | Project.Labels 262 | ] 263 | }), 264 | Project.create({ 265 | id: ++projectId, 266 | name: 'a' + Math.random().toString(), 267 | labels: [ 268 | {name: Math.random().toString()}, 269 | {name: Math.random().toString()} 270 | ] 271 | }, { 272 | include: [ 273 | Project.Labels 274 | ] 275 | }) 276 | ]).then(([projectA, projectB]) => { 277 | this.projectA = projectA; 278 | this.projectB = projectB; 279 | }).then(() => { 280 | return Promise.all([ 281 | User.create({ 282 | id: 1, 283 | name: 'b' + Math.random().toString(), 284 | tasks: [ 285 | { 286 | id: ++taskId, 287 | title: Math.random().toString(), 288 | createdAt: new Date(Date.UTC(2014, 5, 11)), 289 | projectId: this.projectA.id 290 | }, 291 | { 292 | id: ++taskId, 293 | title: Math.random().toString(), 294 | createdAt: new Date(Date.UTC(2014, 5, 16)), 295 | projectId: this.projectB.id 296 | }, 297 | { 298 | id: ++taskId, 299 | title: Math.random().toString(), 300 | createdAt: new Date(Date.UTC(2014, 5, 20)), 301 | projectId: this.projectA.id 302 | } 303 | ] 304 | }, { 305 | include: [User.Tasks] 306 | }), 307 | User.create({ 308 | id: 2, 309 | name: 'a' + Math.random().toString(), 310 | tasks: [ 311 | { 312 | id: ++taskId, 313 | title: Math.random().toString(), 314 | projectId: this.projectB.id 315 | }, 316 | { 317 | id: ++taskId, 318 | title: Math.random().toString(), 319 | projectId: this.projectB.id 320 | } 321 | ] 322 | }, { 323 | include: [User.Tasks] 324 | }), 325 | ]).then(([userA, userB]) => { 326 | this.userA = userA; 327 | this.userB = userB; 328 | 329 | this.users = [userA, userB]; 330 | }); 331 | }); 332 | }); 333 | }); 334 | 335 | beforeEach(() => { 336 | this.sandbox.spy(User, 'findOne'); 337 | }); 338 | afterEach(() => { 339 | this.sandbox.restore(); 340 | }); 341 | 342 | it('should resolve a plain result with a single model', () => { 343 | var user = this.userB; 344 | 345 | return graphql({ 346 | schema: schema, 347 | source: ` 348 | { 349 | user(id: ${user.id}) { 350 | name 351 | myVirtual 352 | } 353 | } 354 | `, 355 | contextValue: {}, 356 | }).then((result) => { 357 | if (result.errors) throw new Error(result.errors[0].stack); 358 | 359 | expect(result.data).to.deep.equal({ 360 | user: { 361 | name: user.name, 362 | myVirtual: 'lol' 363 | } 364 | }); 365 | }); 366 | }); 367 | 368 | it('should map context to find options', () => { 369 | var user = this.userB; 370 | 371 | return graphql({ 372 | schema, 373 | source: ` 374 | { 375 | user(id: ${user.id}) { 376 | name 377 | myVirtual 378 | } 379 | } 380 | `, 381 | contextValue: {a: 1, b: 2} 382 | }).then((result) => { 383 | if (result.errors) throw new Error(result.errors[0].stack); 384 | 385 | expect(result.data).to.deep.equal({ 386 | user: { 387 | name: user.name, 388 | myVirtual: 'lol' 389 | } 390 | }); 391 | 392 | expect(User.findOne.firstCall.args[0].a).to.equal(1); 393 | expect(User.findOne.firstCall.args[0].c).to.equal(2); 394 | }); 395 | }); 396 | 397 | it('should resolve a plain result with an aliased field', () => { 398 | var user = this.userB; 399 | 400 | return graphql({ 401 | schema, 402 | source: ` 403 | { 404 | user(id: ${user.id}) { 405 | name 406 | magic: myVirtual 407 | } 408 | } 409 | `, 410 | contextValue: {}, 411 | }).then((result) => { 412 | if (result.errors) throw new Error(result.errors[0].stack); 413 | 414 | expect(result.data).to.deep.equal({ 415 | user: { 416 | name: user.name, 417 | magic: 'lol' 418 | } 419 | }); 420 | }); 421 | }); 422 | 423 | it('should resolve a plain result with a single model and aliases', () => { 424 | var userA = this.userA 425 | , userB = this.userB; 426 | 427 | return graphql({ 428 | schema, 429 | source: ` 430 | { 431 | userA: user(id: ${userA.id}) { 432 | name 433 | myVirtual 434 | } 435 | userB: user(id: ${userB.id}) { 436 | name 437 | myVirtual 438 | } 439 | } 440 | `, 441 | contextValue: {}, 442 | }).then((result) => { 443 | if (result.errors) throw new Error(result.errors[0].stack); 444 | 445 | expect(result.data).to.deep.equal({ 446 | userA: { 447 | name: userA.name, 448 | myVirtual: 'lol' 449 | }, 450 | userB: { 451 | name: userB.name, 452 | myVirtual: 'lol' 453 | } 454 | }); 455 | }); 456 | }); 457 | 458 | it('should resolve a array result with a model and aliased includes', () => { 459 | return graphql({ 460 | schema, 461 | source: ` 462 | { 463 | users { 464 | name 465 | 466 | first: tasks(limit: 1) { 467 | title 468 | } 469 | 470 | rest: tasks(offset: 1, limit: 99) { 471 | title 472 | } 473 | } 474 | } 475 | `, 476 | contextValue: {}, 477 | }).then((result) => { 478 | if (result.errors) throw new Error(result.errors[0].stack); 479 | 480 | result.data.users.forEach(function (user) { 481 | expect(user.first).to.be.ok; 482 | expect(user.rest).to.be.ok; 483 | }); 484 | }); 485 | }); 486 | 487 | it('should resolve a array result with a model and aliased includes and __typename', () => { 488 | return graphql({ 489 | schema, 490 | source: ` 491 | { 492 | users { 493 | name 494 | 495 | first: tasks(limit: 1) { 496 | title 497 | __typename 498 | } 499 | } 500 | } 501 | ` 502 | }).then((result) => { 503 | if (result.errors) throw new Error(result.errors[0].stack); 504 | 505 | result.data.users.forEach(function (user) { 506 | expect(user.first[0].__typename).to.equal('Task'); 507 | }); 508 | }); 509 | }); 510 | 511 | it('should resolve an array result with a single model', () => { 512 | var users = this.users; 513 | 514 | return graphql({ 515 | schema, 516 | source: ` 517 | { 518 | users { 519 | name 520 | } 521 | } 522 | `, 523 | }).then((result) => { 524 | if (result.errors) throw new Error(result.errors[0].stack); 525 | 526 | expect(result.data.users).to.have.length.above(0); 527 | 528 | const usersNames = users.map(user => ({name: user.name})); 529 | // As the GraphQL query doesn't specify an ordering, 530 | // the order of the two lists can not be asserted. 531 | expect(result.data.users).to.deep.have.members(usersNames); 532 | }); 533 | }); 534 | 535 | it('should allow amending the find for a array result with a single model', () => { 536 | var user = this.userA 537 | , schema; 538 | 539 | schema = new GraphQLSchema({ 540 | query: new GraphQLObjectType({ 541 | name: 'RootQueryType', 542 | fields: { 543 | users: { 544 | type: new GraphQLList(userType), 545 | args: { 546 | limit: { 547 | type: GraphQLInt 548 | }, 549 | order: { 550 | type: GraphQLString 551 | } 552 | }, 553 | resolve: resolver(User, { 554 | before: function (options, args, {name}) { 555 | options.where = options.where || {}; 556 | options.where.name = name; 557 | return options; 558 | } 559 | }) 560 | } 561 | } 562 | }) 563 | }); 564 | 565 | return graphql({ 566 | schema, 567 | source: ` 568 | { 569 | users { 570 | name 571 | } 572 | } 573 | `, 574 | contextValue: { 575 | name: user.name 576 | }, 577 | }).then((result) => { 578 | if (result.errors) throw new Error(result.errors[0].stack); 579 | 580 | expect(result.data.users).to.have.length(1); 581 | expect(result.data.users[0].name).to.equal(user.name); 582 | }); 583 | }); 584 | 585 | it('should allow parsing the find for a array result with a single model', () => { 586 | var users = this.users 587 | , schema; 588 | 589 | schema = new GraphQLSchema({ 590 | query: new GraphQLObjectType({ 591 | name: 'RootQueryType', 592 | fields: { 593 | users: { 594 | type: new GraphQLList(userType), 595 | args: { 596 | limit: { 597 | type: GraphQLInt 598 | }, 599 | order: { 600 | type: GraphQLString 601 | } 602 | }, 603 | resolve: resolver(User, { 604 | after: function (result) { 605 | return result.map(function () { 606 | return { 607 | name: '11!!' 608 | }; 609 | }); 610 | } 611 | }) 612 | } 613 | } 614 | }) 615 | }); 616 | 617 | return graphql({ 618 | schema, 619 | source: ` 620 | { 621 | users { 622 | name 623 | } 624 | } 625 | `, 626 | }).then((result) => { 627 | if (result.errors) throw new Error(result.errors[0].stack); 628 | 629 | expect(result.data.users).to.have.length(users.length); 630 | result.data.users.forEach(function (user) { 631 | expect(user.name).to.equal('11!!'); 632 | }); 633 | }); 634 | }); 635 | 636 | it('should work with a resolver through a proxy', () => { 637 | var users = this.users 638 | , schema 639 | , userType 640 | , taskType 641 | , spy = sinon.spy(); 642 | 643 | taskType = new GraphQLObjectType({ 644 | name: 'Task', 645 | description: 'A task', 646 | fields: { 647 | id: { 648 | type: new GraphQLNonNull(GraphQLInt) 649 | }, 650 | title: { 651 | type: GraphQLString 652 | } 653 | } 654 | }); 655 | 656 | userType = new GraphQLObjectType({ 657 | name: 'User', 658 | description: 'A user', 659 | fields: { 660 | id: { 661 | type: new GraphQLNonNull(GraphQLInt), 662 | }, 663 | name: { 664 | type: GraphQLString, 665 | }, 666 | tasks: { 667 | type: new GraphQLList(taskType), 668 | resolve: (function () { 669 | var $resolver = resolver(User.Tasks) 670 | , $proxy; 671 | 672 | $proxy = function () { 673 | return $resolver.apply(null, Array.prototype.slice.call(arguments)); 674 | }; 675 | 676 | $proxy.$proxy = $resolver; 677 | return $proxy; 678 | }()) 679 | } 680 | } 681 | }); 682 | 683 | schema = new GraphQLSchema({ 684 | query: new GraphQLObjectType({ 685 | name: 'RootQueryType', 686 | fields: { 687 | users: { 688 | type: new GraphQLList(userType), 689 | args: { 690 | limit: { 691 | type: GraphQLInt 692 | }, 693 | order: { 694 | type: GraphQLString 695 | } 696 | }, 697 | resolve: resolver(User) 698 | } 699 | } 700 | }) 701 | }); 702 | 703 | return graphql({ 704 | schema, 705 | source: ` 706 | { 707 | users { 708 | name, 709 | tasks { 710 | title 711 | } 712 | } 713 | } 714 | `, 715 | }).then((result) => { 716 | if (result.errors) throw new Error(result.errors[0].stack); 717 | 718 | expect(result.data.users).to.have.length(users.length); 719 | result.data.users.forEach(function (user) { 720 | expect(user.tasks).to.have.length.above(0); 721 | }); 722 | }); 723 | }); 724 | 725 | it('should work with a passthrough resolver and a duplicated query', () => { 726 | var users = this.users 727 | , schema 728 | , userType 729 | , taskType 730 | , spy = sinon.spy(); 731 | 732 | taskType = new GraphQLObjectType({ 733 | name: 'Task', 734 | description: 'A task', 735 | fields: { 736 | id: { 737 | type: new GraphQLNonNull(GraphQLInt) 738 | }, 739 | title: { 740 | type: GraphQLString 741 | } 742 | } 743 | }); 744 | 745 | userType = new GraphQLObjectType({ 746 | name: 'User', 747 | description: 'A user', 748 | fields: { 749 | id: { 750 | type: new GraphQLNonNull(GraphQLInt), 751 | }, 752 | name: { 753 | type: GraphQLString, 754 | }, 755 | tasks: { 756 | type: new GraphQLObjectType({ 757 | name: 'Tasks', 758 | fields: { 759 | nodes: { 760 | type: new GraphQLList(taskType), 761 | resolve: resolver(User.Tasks) 762 | } 763 | } 764 | }), 765 | resolve: (function () { 766 | var $resolver; 767 | 768 | $resolver = function (source) { 769 | return source; 770 | }; 771 | 772 | $resolver.$passthrough = true; 773 | 774 | return $resolver; 775 | }()) 776 | } 777 | } 778 | }); 779 | 780 | schema = new GraphQLSchema({ 781 | query: new GraphQLObjectType({ 782 | name: 'RootQueryType', 783 | fields: { 784 | users: { 785 | type: new GraphQLList(userType), 786 | args: { 787 | limit: { 788 | type: GraphQLInt 789 | }, 790 | order: { 791 | type: GraphQLString 792 | } 793 | }, 794 | resolve: resolver(User) 795 | } 796 | } 797 | }) 798 | }); 799 | 800 | return graphql({ 801 | schema, 802 | source: ` 803 | { 804 | users { 805 | name, 806 | tasks { 807 | nodes { 808 | title 809 | } 810 | nodes { 811 | id 812 | } 813 | } 814 | } 815 | } 816 | `, 817 | }).then((result) => { 818 | if (result.errors) throw new Error(result.errors[0].stack); 819 | 820 | expect(result.data.users).to.have.length(users.length); 821 | result.data.users.forEach(function (user) { 822 | expect(user.tasks.nodes).to.have.length.above(0); 823 | user.tasks.nodes.forEach(function (task) { 824 | expect(task.title).to.be.ok; 825 | expect(task.id).to.be.ok; 826 | }); 827 | }); 828 | }); 829 | }); 830 | 831 | it('should resolve an array result with a single model and limit', () => { 832 | return graphql({ 833 | schema, 834 | source: ` 835 | { 836 | users(limit: 1) { 837 | name 838 | } 839 | } 840 | `, 841 | }).then((result) => { 842 | if (result.errors) throw new Error(result.errors[0].stack); 843 | 844 | expect(result.data.users).to.have.length(1); 845 | }); 846 | }); 847 | 848 | it('should resolve a plain result with a single hasMany association', () => { 849 | const user = this.userB; 850 | 851 | return graphql({ 852 | schema, 853 | source: ` 854 | { 855 | user(id: ${user.id}) { 856 | name 857 | tasks { 858 | title 859 | taskVirtual 860 | } 861 | } 862 | } 863 | `, 864 | contextValue: { 865 | yolo: 'swag' 866 | }, 867 | }).then((result) => { 868 | if (result.errors) throw new Error(result.errors[0].stack); 869 | 870 | expect(result.data.user.name).to.equal(user.name); 871 | 872 | expect(result.data.user.tasks).to.have.length.above(0); 873 | // As the order of user.tasks is nondeterministic, we only assert on equal members 874 | // of both the user's tasks and the tasks the graphql query responded with. 875 | const userTasks = user.tasks.map(task => ({title: task.title, taskVirtual: 'tasktask'})); 876 | expect(result.data.user.tasks).to.deep.have.members(userTasks); 877 | }); 878 | 879 | }); 880 | 881 | it('should resolve a plain result with a single limited hasMany association', () => { 882 | var user = this.userB; 883 | 884 | return graphql({ 885 | schema, 886 | source: ` 887 | { 888 | user(id: ${user.id}) { 889 | name 890 | tasks(limit: 1) { 891 | title 892 | } 893 | } 894 | } 895 | `, 896 | }).then((result) => { 897 | if (result.errors) throw new Error(result.errors[0].stack); 898 | 899 | expect(result.data.user.tasks).to.have.length(1); 900 | }); 901 | }); 902 | 903 | it('should resolve a array result with a single hasMany association', () => { 904 | var users = this.users; 905 | 906 | return graphql({ 907 | schema, 908 | source: ` 909 | { 910 | users(order: "id") { 911 | name 912 | tasks(order: "id") { 913 | title 914 | } 915 | } 916 | } 917 | `, 918 | }).then((result) => { 919 | if (result.errors) throw new Error(result.errors[0].stack); 920 | 921 | expect(result.data.users.length).to.equal(users.length); 922 | result.data.users.forEach(function (user) { 923 | expect(user.tasks).length.to.be.above(0); 924 | }); 925 | 926 | expect(result.data).to.deep.equal({ 927 | users: users.map(function (user) { 928 | return { 929 | name: user.name, 930 | tasks: user.tasks.map(task => ({title: task.title})) 931 | }; 932 | }) 933 | }); 934 | }); 935 | }); 936 | 937 | it('should resolve a array result with a single limited hasMany association', () => { 938 | var users = this.users; 939 | 940 | return graphql({ 941 | schema, 942 | source: ` 943 | { 944 | users { 945 | name 946 | tasks(limit: 1) { 947 | title 948 | } 949 | } 950 | } 951 | ` 952 | }).then((result) => { 953 | if (result.errors) throw new Error(result.errors[0].stack); 954 | 955 | expect(result.data.users.length).to.equal(users.length); 956 | result.data.users.forEach(function (user) { 957 | expect(user.tasks).length.to.be(1); 958 | }); 959 | }); 960 | }); 961 | 962 | it('should resolve a array result with a single limited hasMany association with a nested belongsTo relation', () => { 963 | var users = this.users 964 | , sqlSpy = sinon.spy(); 965 | 966 | return graphql({ 967 | schema, 968 | source: ` 969 | { 970 | users { 971 | tasks(limit: 2) { 972 | title 973 | project { 974 | name 975 | } 976 | } 977 | } 978 | } 979 | ` 980 | }).then((result) => { 981 | if (result.errors) throw new Error(result.errors[0].stack); 982 | 983 | expect(result.data.users.length).to.equal(users.length); 984 | result.data.users.forEach(function (user) { 985 | expect(user.tasks).length.to.be(2); 986 | user.tasks.forEach(function (task) { 987 | expect(task.project.name).to.be.ok; 988 | }); 989 | }); 990 | }); 991 | }); 992 | 993 | it('should resolve a array result with a single hasMany association with a nested belongsTo relation', () => { 994 | var users = this.users 995 | , sqlSpy = sinon.spy(); 996 | 997 | return graphql({ 998 | schema, 999 | source: ` 1000 | { 1001 | users { 1002 | tasks { 1003 | title 1004 | project { 1005 | name 1006 | } 1007 | } 1008 | } 1009 | } 1010 | `, 1011 | }).then((result) => { 1012 | if (result.errors) throw new Error(result.errors[0].stack); 1013 | 1014 | expect(result.data.users.length).to.equal(users.length); 1015 | result.data.users.forEach(function (user) { 1016 | expect(user.tasks).length.to.be.above(0); 1017 | user.tasks.forEach(function (task) { 1018 | expect(task.project.name).to.be.ok; 1019 | }); 1020 | }); 1021 | }); 1022 | }); 1023 | 1024 | it('should resolve a array result with a single hasMany association' + 1025 | 'with a nested belongsTo relation with a nested hasMany relation', () => { 1026 | var users = this.users 1027 | , sqlSpy = sinon.spy(); 1028 | 1029 | return graphql({ 1030 | schema, 1031 | source: ` 1032 | { 1033 | users { 1034 | tasks { 1035 | title 1036 | project { 1037 | name 1038 | labels { 1039 | name 1040 | } 1041 | } 1042 | } 1043 | } 1044 | } 1045 | `, 1046 | }).then((result) => { 1047 | if (result.errors) throw new Error(result.errors[0].stack); 1048 | 1049 | expect(result.data.users.length).to.equal(users.length); 1050 | result.data.users.forEach(function (user) { 1051 | expect(user.tasks).length.to.be.above(0); 1052 | user.tasks.forEach(function (task) { 1053 | expect(task.project.name).to.be.ok; 1054 | 1055 | expect(task.project.labels).length.to.be.above(0); 1056 | task.project.labels.forEach(function (label) { 1057 | expect(label.name).to.be.ok; 1058 | }); 1059 | }); 1060 | }); 1061 | }); 1062 | }); 1063 | 1064 | it('should resolve a array result with a single limited hasMany association with a before filter', () => { 1065 | var users = this.users; 1066 | 1067 | return graphql({ 1068 | schema, 1069 | source: ` 1070 | { 1071 | users { 1072 | tasks(first: 2) { 1073 | title 1074 | } 1075 | } 1076 | } 1077 | `, 1078 | }).then((result) => { 1079 | if (result.errors) throw new Error(result.errors[0].stack); 1080 | 1081 | expect(result.data.users.length).to.equal(users.length); 1082 | result.data.users.forEach(function (user) { 1083 | expect(user.tasks).length.to.be(2); 1084 | }); 1085 | }); 1086 | }); 1087 | 1088 | it('should not call association getter if user manually included', () => { 1089 | this.sandbox.spy(Task, 'findAll'); 1090 | this.sandbox.spy(User, 'findAll'); 1091 | 1092 | var schema = new GraphQLSchema({ 1093 | query: new GraphQLObjectType({ 1094 | name: 'RootQueryType', 1095 | fields: { 1096 | users: { 1097 | type: new GraphQLList(userType), 1098 | resolve: resolver(User, { 1099 | before: function (options) { 1100 | options.include = [User.Tasks]; 1101 | options.order = [ 1102 | ['id'], 1103 | [{ model: Task, as: 'tasks' }, 'id', 'ASC'] 1104 | ]; 1105 | return options; 1106 | } 1107 | }) 1108 | } 1109 | } 1110 | }) 1111 | }); 1112 | 1113 | return graphql({ 1114 | schema, 1115 | source: ` 1116 | { 1117 | users { 1118 | tasks { 1119 | title 1120 | } 1121 | } 1122 | } 1123 | `, 1124 | }).then(result => { 1125 | if (result.errors) throw new Error(result.errors[0].stack); 1126 | 1127 | expect(Task.findAll.callCount).to.equal(0); 1128 | expect(User.findAll.callCount).to.equal(1); 1129 | expect(User.findAll.getCall(0).args[0].include).to.have.length(1); 1130 | expect(User.findAll.getCall(0).args[0].include[0].name).to.equal(User.Tasks.name); 1131 | 1132 | result.data.users.forEach(function (user) { 1133 | expect(user.tasks).length.to.be.above(0); 1134 | }); 1135 | 1136 | expect(result.data).to.deep.equal({ 1137 | users: this.users.map(function (user) { 1138 | return { 1139 | tasks: user.tasks.map(task => ({title: task.title})) 1140 | }; 1141 | }) 1142 | }); 1143 | }); 1144 | }); 1145 | 1146 | it('should allow async before and after', () => { 1147 | var users = this.users 1148 | , schema; 1149 | 1150 | schema = new GraphQLSchema({ 1151 | query: new GraphQLObjectType({ 1152 | name: 'RootQueryType', 1153 | fields: { 1154 | users: { 1155 | type: new GraphQLList(userType), 1156 | args: { 1157 | limit: { 1158 | type: GraphQLInt 1159 | }, 1160 | order: { 1161 | type: GraphQLString 1162 | } 1163 | }, 1164 | resolve: resolver(User, { 1165 | before: function (options) { 1166 | return Promise.resolve(options); 1167 | }, 1168 | after: async function (result) { 1169 | await delay(100); 1170 | return result.map(function () { 1171 | return { 1172 | name: 'Delayed!' 1173 | }; 1174 | }); 1175 | } 1176 | }) 1177 | } 1178 | } 1179 | }) 1180 | }); 1181 | 1182 | return graphql({ 1183 | schema, 1184 | source: ` 1185 | { 1186 | users { 1187 | name 1188 | } 1189 | } 1190 | `, 1191 | }).then((result) => { 1192 | if (result.errors) throw new Error(result.errors[0].stack); 1193 | 1194 | expect(result.data.users).to.have.length(users.length); 1195 | result.data.users.forEach(function (user) { 1196 | expect(user.name).to.equal('Delayed!'); 1197 | }); 1198 | }); 1199 | }); 1200 | 1201 | it('should resolve args from array to before', () => { 1202 | var user = this.userB; 1203 | 1204 | return graphql({ 1205 | schema, 1206 | source: ` 1207 | { 1208 | user(id: ${user.get('id')}) { 1209 | tasksByIds(ids: [${user.tasks[0].get('id')}]) { 1210 | id 1211 | } 1212 | } 1213 | } 1214 | `, 1215 | }).then((result) => { 1216 | if (result.errors) throw new Error(result.errors[0].stack); 1217 | 1218 | expect(result.data.user.tasksByIds.length).to.equal(1); 1219 | }); 1220 | }); 1221 | 1222 | it('should resolve query variables inside where parameter', () => { 1223 | return graphql({ 1224 | schema, 1225 | source: ` 1226 | query($where: SequelizeJSON) { 1227 | users(where: $where) { 1228 | id 1229 | } 1230 | } 1231 | `, 1232 | variableValues: { 1233 | where: '{"name": {"like": "a%"}}', 1234 | }, 1235 | }).then((result) => { 1236 | if (result.errors) throw new Error(result.errors[0].stack); 1237 | 1238 | expect(result.data.users[0].id).to.equal(2); 1239 | expect(result.data.users.length).to.equal(1); 1240 | }); 1241 | }); 1242 | 1243 | it('should resolve query variables inside where parameter', () => { 1244 | return graphql({ 1245 | schema, 1246 | source: ` 1247 | query($name: String) { 1248 | users(where: {name: {like: $name}}) { 1249 | id 1250 | } 1251 | } 1252 | `, 1253 | variableValues: {name: 'a%'} 1254 | }).then((result) => { 1255 | if (result.errors) throw new Error(result.errors[0].stack); 1256 | 1257 | expect(result.data.users[0].id).to.equal(2); 1258 | expect(result.data.users.length).to.equal(1); 1259 | }); 1260 | }); 1261 | 1262 | it('should allow list queries set as NonNullable', () => { 1263 | var user = this.userA 1264 | , schema; 1265 | 1266 | schema = new GraphQLSchema({ 1267 | query: new GraphQLObjectType({ 1268 | name: 'RootQueryType', 1269 | fields: { 1270 | users: { 1271 | type: new GraphQLNonNull(new GraphQLList(userType)), 1272 | resolve: resolver(User, { 1273 | before: function (options, args, { name }) { 1274 | options.where = options.where || {}; 1275 | options.where.name = name; 1276 | return options; 1277 | } 1278 | }) 1279 | } 1280 | } 1281 | }) 1282 | }); 1283 | 1284 | return graphql({ 1285 | schema, 1286 | source: ` 1287 | { 1288 | users { 1289 | name 1290 | } 1291 | } 1292 | `, 1293 | contextValue: { 1294 | name: user.name, 1295 | }, 1296 | }).then((result) => { 1297 | if (result.errors) throw new Error(result.errors[0].stack); 1298 | 1299 | expect(result.data.users).to.have.length(1); 1300 | expect(result.data.users[0].name).to.equal(user.name); 1301 | }); 1302 | }); 1303 | }); 1304 | -------------------------------------------------------------------------------- /test/support/helper.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | 3 | export const sequelize = createSequelize(); 4 | 5 | export function createSequelize(options = {}) { 6 | const env = process.env; 7 | const dialect = env.DIALECT || 'sqlite'; 8 | const config = Object.assign( 9 | { 10 | host: 'localhost', 11 | user: 'graphql_sequelize_test', 12 | password: 'graphql_sequelize_test', 13 | database: 'graphql_sequelize_test' 14 | }, 15 | dialect === 'postgres' && { 16 | host: env.POSTGRES_PORT_5432_TCP_ADDR, 17 | user: env.POSTGRES_ENV_POSTGRES_USER, 18 | password: env.POSTGRES_ENV_POSTGRES_PASSWORD, 19 | database: env.POSTGRES_ENV_POSTGRES_DATABASE 20 | }, 21 | dialect === 'mysql' && { 22 | host: env.MYSQL_PORT_3306_TCP_ADDR, 23 | user: env.MYSQL_ENV_MYSQL_USER, 24 | password: env.MYSQL_ENV_MYSQL_PASSWORD, 25 | database: env.MYSQL_ENV_MYSQL_DATABASE 26 | }, 27 | dialect === 'postgres' && env.CI && { 28 | user: 'postgres', 29 | password: '', 30 | database: 'test' 31 | }, 32 | dialect === 'mysql' && env.CI && { 33 | user: 'travis', 34 | password: '', 35 | database: 'test' 36 | } 37 | ); 38 | 39 | return new Sequelize(config.database, config.user, config.password, { 40 | host: config.host, 41 | dialect: dialect, 42 | logging: false, 43 | ...options 44 | }); 45 | } 46 | 47 | export function beforeRemoveAllTables() { 48 | before(function () { 49 | if (sequelize.dialect.name === 'mysql') { 50 | this.timeout(10000); 51 | return removeAllTables(sequelize); 52 | } 53 | }); 54 | } 55 | 56 | export function delay(ms) { 57 | return new Promise(resolve => setTimeout(resolve, ms)); 58 | } 59 | 60 | // Not nice too, MySQL does not supports same name for foreign keys 61 | // Solution ? Force remove all tables! 62 | export function removeAllTables(sequelize) { 63 | function getTables() { 64 | return sequelize.query('show tables').then(tables => tables[0].map((table) => table.Tables_in_test)); 65 | } 66 | 67 | return getTables() 68 | .then(tables => { 69 | return Promise.all(tables.map(table => { 70 | return sequelize.query('drop table ' + table).catch(() => {}); 71 | })); 72 | }) 73 | .then(() => { 74 | return getTables(); 75 | }) 76 | .then(tables => { 77 | if (tables.length) { 78 | return removeAllTables(sequelize); 79 | } 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /test/unit/argsToFindOptions.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { expect } from 'chai'; 4 | import argsToFindOptions from '../../src/argsToFindOptions'; 5 | 6 | describe('argsToFindOptions', function () { 7 | var targetAttributes = ['order', 'limit', 'offset']; 8 | 9 | it('should return empty with no args or attributes', function () { 10 | var findOptions = argsToFindOptions(null, null); 11 | expect(findOptions).to.be.empty; 12 | }); 13 | 14 | it('should not include "order" when present in both args and targetAttributes', function () { 15 | var findOptions = argsToFindOptions({ where: { property: 1 }, order: 'order' }, targetAttributes); 16 | 17 | expect(findOptions).to.have.ownProperty('where'); 18 | expect(findOptions.where).not.to.have.ownProperty('order'); 19 | expect(findOptions).to.have.ownProperty('order'); 20 | expect(findOptions.order).to.be.an.instanceOf(Array); 21 | }); 22 | 23 | it('should not include "limit" when present in both args targetAttributes', function () { 24 | var findOptions = argsToFindOptions({ where: { property: 1 }, limit: 1 }, targetAttributes); 25 | 26 | expect(findOptions).to.have.ownProperty('where'); 27 | expect(findOptions.where).not.to.have.ownProperty('limit'); 28 | expect(findOptions).to.have.ownProperty('limit'); 29 | expect(findOptions.limit).to.equal(1); 30 | }); 31 | 32 | it('should not include "offset" when present in both args and targetAttributes', function () { 33 | var findOptions = argsToFindOptions({ where: { property: 1 }, offset: 1 }, targetAttributes); 34 | 35 | expect(findOptions).to.have.ownProperty('where'); 36 | expect(findOptions.where).not.to.have.ownProperty('offset'); 37 | expect(findOptions).to.have.ownProperty('offset'); 38 | expect(findOptions.offset).to.be.equal(1); 39 | }); 40 | 41 | it('should allow filtering by "order" column when in targetAttributes', function () { 42 | var findOptions = argsToFindOptions({ where: { order: 1 } }); 43 | expect(findOptions).to.have.ownProperty('where'); 44 | expect(findOptions.where).to.have.ownProperty('order'); 45 | }); 46 | 47 | it('should allow filtering and ordering by "order" column when in targetAttributes', function () { 48 | var findOptions = argsToFindOptions({ where: { order: 1 }, order: 'order' }); 49 | expect(findOptions).to.have.ownProperty('where'); 50 | expect(findOptions.where).to.have.ownProperty('order'); 51 | expect(findOptions).to.have.ownProperty('order'); 52 | expect(findOptions.order).to.be.an.instanceOf(Array); 53 | }); 54 | 55 | it('should allow value = 0', function () { 56 | var findOptions = argsToFindOptions({ where: { order: 0 }, offset: 0, limit: 0 }, []); 57 | expect(findOptions).to.have.ownProperty('where'); 58 | expect(findOptions.where).to.have.ownProperty('order'); 59 | expect(findOptions).to.have.ownProperty('offset'); 60 | expect(findOptions.where.order).to.be.equal(0); 61 | expect(findOptions.offset).to.be.equal(0); 62 | expect(findOptions.limit).to.be.equal(0); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/unit/attributeFields.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {expect} from 'chai'; 4 | import Sequelize from 'sequelize'; 5 | import attributeFields from '../../src/attributeFields'; 6 | import DateType from '../../src/types/dateType'; 7 | 8 | import { sequelize } from '../support/helper'; 9 | 10 | 11 | import { 12 | GraphQLString, 13 | GraphQLInt, 14 | GraphQLFloat, 15 | GraphQLNonNull, 16 | GraphQLBoolean, 17 | GraphQLEnumType, 18 | GraphQLList, 19 | GraphQLObjectType, 20 | GraphQLSchema 21 | } from 'graphql'; 22 | 23 | import { 24 | toGlobalId 25 | } from 'graphql-relay'; 26 | 27 | describe('attributeFields', function () { 28 | var Model; 29 | var modelName = Math.random().toString(); 30 | before(function () { 31 | Model = sequelize.define(modelName, { 32 | email: { 33 | type: Sequelize.STRING, 34 | allowNull: false 35 | }, 36 | firstName: { 37 | type: Sequelize.STRING 38 | }, 39 | lastName: { 40 | type: Sequelize.STRING 41 | }, 42 | char: { 43 | type: Sequelize.CHAR 44 | }, 45 | float: { 46 | type: Sequelize.FLOAT 47 | }, 48 | decimal: { 49 | type: Sequelize.DECIMAL 50 | }, 51 | enum: { 52 | type: Sequelize.ENUM('first', 'second') 53 | }, 54 | enumSpecial: { 55 | type: Sequelize.ENUM('foo_bar', 'foo-bar', '25.8', 'two--specials', '¼', ' ¼--½_¾ - ') 56 | }, 57 | enumArray: { 58 | type: Sequelize.ARRAY(Sequelize.ENUM('first', 'second')) 59 | }, 60 | list: { 61 | type: Sequelize.ARRAY(Sequelize.STRING) 62 | }, 63 | virtualInteger: { 64 | type: new Sequelize.VIRTUAL(Sequelize.INTEGER) 65 | }, 66 | virtualBoolean: { 67 | type: new Sequelize.VIRTUAL(Sequelize.BOOLEAN) 68 | }, 69 | date: { 70 | type: Sequelize.DATE 71 | }, 72 | time: { 73 | type: Sequelize.TIME 74 | }, 75 | dateonly: { 76 | type: Sequelize.DATEONLY 77 | }, 78 | comment: { 79 | type: Sequelize.STRING, 80 | comment: 'This is a comment' 81 | } 82 | }, { 83 | timestamps: false 84 | }); 85 | }); 86 | 87 | it('should return fields for a simple model', function () { 88 | var fields = attributeFields(Model); 89 | 90 | expect(Object.keys(fields)).to.deep.equal([ 91 | 'id', 'email', 'firstName', 'lastName', 92 | 'char', 'float', 'decimal', 93 | 'enum', 'enumSpecial', 'enumArray', 94 | 'list', 'virtualInteger', 'virtualBoolean', 95 | 'date', 'time', 'dateonly', 'comment' 96 | ]); 97 | 98 | expect(fields.id.type).to.be.an.instanceOf(GraphQLNonNull); 99 | expect(fields.id.type.ofType).to.equal(GraphQLInt); 100 | 101 | expect(fields.email.type).to.be.an.instanceOf(GraphQLNonNull); 102 | expect(fields.email.type.ofType).to.equal(GraphQLString); 103 | 104 | expect(fields.firstName.type).to.equal(GraphQLString); 105 | 106 | expect(fields.lastName.type).to.equal(GraphQLString); 107 | 108 | expect(fields.char.type).to.equal(GraphQLString); 109 | 110 | expect(fields.enum.type).to.be.an.instanceOf(GraphQLEnumType); 111 | 112 | expect(fields.enumSpecial.type).to.be.an.instanceOf(GraphQLEnumType); 113 | 114 | expect(fields.enumArray.type).to.be.an.instanceOf(GraphQLList); 115 | expect(fields.enumArray.type.ofType).to.be.an.instanceOf(GraphQLEnumType); 116 | 117 | expect(fields.list.type).to.be.an.instanceOf(GraphQLList); 118 | 119 | expect(fields.float.type).to.equal(GraphQLFloat); 120 | 121 | expect(fields.decimal.type).to.equal(GraphQLString); 122 | 123 | expect(fields.virtualInteger.type).to.equal(GraphQLInt); 124 | 125 | expect(fields.virtualBoolean.type).to.equal(GraphQLBoolean); 126 | 127 | expect(fields.date.type).to.equal(DateType); 128 | 129 | expect(fields.time.type).to.equal(GraphQLString); 130 | 131 | expect(fields.dateonly.type).to.equal(GraphQLString); 132 | }); 133 | 134 | it('should be possible to rename fields with a object map',function () { 135 | var fields = attributeFields(Model, {map: {id: 'mappedId'}}); 136 | expect(Object.keys(fields)).to.deep.equal([ 137 | 'mappedId', 'email', 'firstName', 'lastName', 'char', 'float', 'decimal', 138 | 'enum', 'enumSpecial', 'enumArray', 139 | 'list', 'virtualInteger', 'virtualBoolean', 'date', 140 | 'time', 'dateonly', 'comment' 141 | ]); 142 | }); 143 | 144 | it('should be possible to rename fields with a function that maps keys',function () { 145 | var fields = attributeFields(Model, { 146 | map: k => k + 's' 147 | }); 148 | expect(Object.keys(fields)).to.deep.equal([ 149 | 'ids', 'emails', 'firstNames', 'lastNames', 'chars', 'floats', 'decimals', 150 | 'enums', 'enumSpecials', 'enumArrays', 151 | 'lists', 'virtualIntegers', 'virtualBooleans', 152 | 'dates', 'times', 'dateonlys', 'comments' 153 | ]); 154 | }); 155 | 156 | it('should be possible to exclude fields', function () { 157 | var fields = attributeFields(Model, { 158 | exclude: [ 159 | 'id', 'email', 'char', 'float', 'decimal', 160 | 'enum', 'enumSpecial', 'enumArray', 161 | 'list', 'virtualInteger', 'virtualBoolean', 162 | 'date','time','dateonly','comment' 163 | ] 164 | }); 165 | 166 | expect(Object.keys(fields)).to.deep.equal(['firstName', 'lastName']); 167 | }); 168 | 169 | it('should be able to exclude fields via a function', function () { 170 | var fields = attributeFields(Model, { 171 | exclude: field => ~[ 172 | 'id', 'email', 'char', 'float', 'decimal', 173 | 'enum', 'enumSpecial', 'enumArray', 174 | 'list', 'virtualInteger', 'virtualBoolean', 175 | 'date','time','dateonly','comment' 176 | ].indexOf(field) 177 | }); 178 | 179 | expect(Object.keys(fields)).to.deep.equal(['firstName', 'lastName']); 180 | }); 181 | 182 | it('should be possible to specify specific fields', function () { 183 | var fields = attributeFields(Model, { 184 | only: ['id', 'email', 'list'] 185 | }); 186 | 187 | expect(Object.keys(fields)).to.deep.equal(['id', 'email', 'list']); 188 | }); 189 | 190 | it('should be possible to specify specific fields via a function', function () { 191 | var fields = attributeFields(Model, { 192 | only: field => ~['id', 'email', 'list'].indexOf(field), 193 | }); 194 | 195 | expect(Object.keys(fields)).to.deep.equal(['id', 'email', 'list']); 196 | }); 197 | 198 | it('should be possible to automatically set a relay globalId', function () { 199 | var fields = attributeFields(Model, { 200 | globalId: true 201 | }); 202 | 203 | expect(fields.id.resolve).to.be.ok; 204 | expect(fields.id.type.ofType.name).to.equal('ID'); 205 | expect(fields.id.resolve({ 206 | id: 23 207 | })).to.equal(toGlobalId(Model.name, 23)); 208 | }); 209 | 210 | it('should automatically name enum types', function () { 211 | var fields = attributeFields(Model); 212 | 213 | expect(fields.enum.type.name).to.not.be.undefined; 214 | expect(fields.enumSpecial.type.name).to.not.be.undefined; 215 | 216 | expect(fields.enum.type.name).to.equal(modelName + 'enum' + 'EnumType'); 217 | expect(fields.enumSpecial.type.name).to.equal(modelName + 'enumSpecial' + 'EnumType'); 218 | expect(fields.enumArray.type.ofType.name).to.equal(modelName + 'enumArray' + 'EnumType'); 219 | }); 220 | 221 | it('should support enum values with characters not allowed by GraphQL', function () { 222 | const fields = attributeFields(Model); 223 | const enums = fields.enumSpecial.type.getValues(); 224 | 225 | expect(enums).to.not.be.undefined; 226 | expect(enums[0].name).to.equal('foo_bar'); 227 | expect(enums[0].value).to.equal('foo_bar'); 228 | expect(enums[1].name).to.equal('fooBar'); 229 | expect(enums[1].value).to.equal('foo-bar'); 230 | expect(enums[2].name).to.equal('_258'); 231 | expect(enums[2].value).to.equal('25.8'); 232 | expect(enums[3].name).to.equal('twoSpecials'); 233 | expect(enums[3].value).to.equal('two--specials'); 234 | expect(enums[4].name).to.equal('frac14'); 235 | expect(enums[4].value).to.equal('¼'); 236 | expect(enums[5].name).to.equal('frac14Frac12_frac34'); 237 | expect(enums[5].value).to.equal(' ¼--½_¾ - '); 238 | }); 239 | 240 | it('should support enum values with underscores', function () { 241 | const fields = attributeFields(Model); 242 | const enums = fields.enumSpecial.type.getValues(); 243 | 244 | expect(enums).to.not.be.undefined; 245 | expect(enums[0].name).to.equal('foo_bar'); 246 | expect(enums[0].value).to.equal('foo_bar'); 247 | }); 248 | 249 | it('should not create multiple enum types with same name when using cache', function () { 250 | 251 | // Create Schema 252 | var schemaFn = function (fields1, fields2) { 253 | return function () { 254 | var object1 = new GraphQLObjectType({ 255 | name: 'Object1', 256 | fields: fields1 257 | }); 258 | var object2 = new GraphQLObjectType({ 259 | name: 'Object2', 260 | fields: fields2 261 | }); 262 | return new GraphQLSchema({ 263 | query: new GraphQLObjectType({ 264 | name: 'RootQueryType', 265 | fields: { 266 | object1: { 267 | type: object1, 268 | resolve: function () { 269 | return {}; 270 | } 271 | }, 272 | object2: { 273 | type: object2, 274 | resolve: function () { 275 | return {}; 276 | } 277 | } 278 | } 279 | }) 280 | }); 281 | }; 282 | }; 283 | 284 | // Bad: Will create multiple/duplicate types with same name 285 | var fields1a = attributeFields(Model); 286 | var fields2a = attributeFields(Model); 287 | 288 | expect(schemaFn(fields1a, fields2a)).to.throw(Error); 289 | 290 | // Good: Will use cache and not create mutliple/duplicate types with same name 291 | var cache = {}; 292 | var fields1b = attributeFields(Model, {cache: cache}); 293 | var fields2b = attributeFields(Model, {cache: cache}); 294 | 295 | expect(schemaFn(fields1b, fields2b)).to.not.throw(Error); 296 | 297 | }); 298 | 299 | describe('with non-default primary key', function () { 300 | var ModelWithoutId; 301 | var modelName = Math.random().toString(); 302 | before(function () { 303 | ModelWithoutId = sequelize.define(modelName, { 304 | email: { 305 | primaryKey: true, 306 | type: Sequelize.STRING, 307 | }, 308 | firstName: { 309 | type: Sequelize.STRING 310 | }, 311 | lastName: { 312 | type: Sequelize.STRING 313 | }, 314 | float: { 315 | type: Sequelize.FLOAT 316 | }, 317 | }, { 318 | timestamps: false 319 | }); 320 | }); 321 | 322 | it('should return fields', function () { 323 | var fields = attributeFields(ModelWithoutId); 324 | 325 | expect(Object.keys(fields)).to.deep.equal(['email', 'firstName', 'lastName', 'float']); 326 | 327 | expect(fields.email.type).to.be.an.instanceOf(GraphQLNonNull); 328 | expect(fields.email.type.ofType).to.equal(GraphQLString); 329 | 330 | expect(fields.firstName.type).to.equal(GraphQLString); 331 | 332 | expect(fields.lastName.type).to.equal(GraphQLString); 333 | 334 | expect(fields.float.type).to.equal(GraphQLFloat); 335 | }); 336 | 337 | it('should be possible to automatically set a relay globalId', function () { 338 | var fields = attributeFields(ModelWithoutId, { 339 | globalId: true 340 | }); 341 | 342 | expect(fields.id.resolve).to.be.ok; 343 | expect(fields.id.type.ofType.name).to.equal('ID'); 344 | expect(fields.id.resolve({ 345 | email: 'idris@example.com' 346 | })).to.equal(toGlobalId(ModelWithoutId.name, 'idris@example.com')); 347 | }); 348 | 349 | it('should be possible to bypass NonNull', function () { 350 | var fields = attributeFields(Model, { 351 | allowNull: true, 352 | }); 353 | 354 | expect(fields.email.type).to.not.be.an.instanceOf(GraphQLNonNull); 355 | expect(fields.email.type).to.equal(GraphQLString); 356 | }); 357 | 358 | it('should be possible to comment attributes', function () { 359 | var fields = attributeFields(Model, { 360 | commentToDescription: true 361 | }); 362 | 363 | expect(fields.comment.description).to.equal('This is a comment'); 364 | }); 365 | 366 | }); 367 | }); 368 | -------------------------------------------------------------------------------- /test/unit/defaultArgs.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | import {expect} from 'chai'; 5 | import Sequelize from 'sequelize'; 6 | import defaultArgs from '../../src/defaultArgs'; 7 | import DateType from '../../src/types/dateType'; 8 | 9 | import { sequelize } from '../support/helper'; 10 | 11 | import { 12 | GraphQLString, 13 | GraphQLInt, 14 | GraphQLScalarType 15 | } from 'graphql'; 16 | 17 | describe('defaultArgs', function () { 18 | it('should return a key for a integer primary key', function () { 19 | var Model 20 | , args; 21 | 22 | Model = sequelize.define('DefaultArgModel', {}); 23 | 24 | args = defaultArgs(Model); 25 | 26 | expect(args).to.have.ownProperty('id'); 27 | expect(args.id.type).to.equal(GraphQLInt); 28 | }); 29 | 30 | it('should return a key for a string primary key', function () { 31 | var Model 32 | , args; 33 | 34 | Model = sequelize.define('DefaultArgModel', { 35 | modelId: { 36 | type: Sequelize.STRING, 37 | primaryKey: true 38 | } 39 | }); 40 | 41 | args = defaultArgs(Model); 42 | 43 | expect(args.modelId.type).to.equal(GraphQLString); 44 | }); 45 | 46 | it('should return a key for a UUID primary key', function () { 47 | var Model 48 | , args; 49 | 50 | Model = sequelize.define('DefaultArgModel', { 51 | uuid: { 52 | type: Sequelize.UUID, 53 | primaryKey: true 54 | } 55 | }); 56 | 57 | args = defaultArgs(Model); 58 | 59 | expect(args.uuid.type).to.equal(GraphQLString); 60 | }); 61 | 62 | it('should return a key for a UUIDV4 primary key', function () { 63 | var Model 64 | , args; 65 | 66 | Model = sequelize.define('DefaultArgModel', { 67 | uuidv4: { 68 | type: Sequelize.UUIDV4, 69 | primaryKey: true 70 | } 71 | }); 72 | 73 | args = defaultArgs(Model); 74 | 75 | expect(args.uuidv4.type).to.equal(GraphQLString); 76 | }); 77 | 78 | it('should return multiple keys for a compound primary key', function () { 79 | var Model 80 | , args; 81 | 82 | Model = sequelize.define('UserHistory', { 83 | userId: { 84 | type: Sequelize.INTEGER, 85 | primaryKey: true, 86 | }, 87 | timestamp: { 88 | type: Sequelize.DATE, 89 | primaryKey: true, 90 | }, 91 | }); 92 | 93 | args = defaultArgs(Model); 94 | 95 | expect(args.userId.type).to.equal(GraphQLInt); 96 | expect(args.timestamp.type).to.equal(DateType); 97 | }); 98 | 99 | describe('will have an "where" argument', function () { 100 | 101 | it('that is an GraphQLScalarType', function () { 102 | var Model 103 | , args; 104 | 105 | Model = sequelize.define('DefaultArgModel', { 106 | modelId: { 107 | type: Sequelize.STRING, 108 | primaryKey: true 109 | } 110 | }); 111 | 112 | args = defaultArgs(Model); 113 | 114 | expect(args).to.have.ownProperty('where'); 115 | expect(args.where.type).to.be.an.instanceOf(GraphQLScalarType); 116 | }); 117 | 118 | }); 119 | 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /test/unit/defaultListArgs.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {expect} from 'chai'; 4 | import Sequelize from 'sequelize'; 5 | import defaultListArgs from '../../src/defaultListArgs'; 6 | 7 | import { sequelize } from '../support/helper'; 8 | 9 | import { 10 | GraphQLString, 11 | GraphQLInt, 12 | GraphQLScalarType 13 | } from 'graphql'; 14 | 15 | describe('defaultListArgs', function () { 16 | it('should return a limit key', function () { 17 | var args = defaultListArgs(); 18 | 19 | expect(args).to.have.ownProperty('limit'); 20 | expect(args.limit.type).to.equal(GraphQLInt); 21 | }); 22 | 23 | it('should return a order key', function () { 24 | var args = defaultListArgs(); 25 | 26 | expect(args).to.have.ownProperty('order'); 27 | expect(args.order.type).to.equal(GraphQLString); 28 | }); 29 | 30 | describe('will have an "where" argument', function () { 31 | 32 | it('that is an GraphQLScalarType', function () { 33 | var Model 34 | , args; 35 | 36 | Model = sequelize.define('DefaultArgModel', { 37 | modelId: { 38 | type: Sequelize.STRING, 39 | primaryKey: true 40 | } 41 | }); 42 | 43 | args = defaultListArgs(Model); 44 | 45 | expect(args).to.have.ownProperty('where'); 46 | expect(args.where.type).to.be.an.instanceOf(GraphQLScalarType); 47 | }); 48 | 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /test/unit/relay/connection.test.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import {expect} from 'chai'; 3 | import sinon from 'sinon'; 4 | import { sequelize } from '../../support/helper'; 5 | import attributeFields from '../../../src/attributeFields'; 6 | 7 | import { 8 | GraphQLObjectType, 9 | GraphQLSchema, 10 | graphql 11 | } from 'graphql'; 12 | 13 | import { 14 | globalIdField 15 | } from 'graphql-relay'; 16 | 17 | import { 18 | sequelizeConnection 19 | } from '../../../src/relay'; 20 | 21 | describe('relay', function () { 22 | describe('connections', function () { 23 | before(function () { 24 | this.User = sequelize.define('user', {}, {timestamps: false}); 25 | this.Task = sequelize.define('task', {title: Sequelize.STRING}, {timestamps: false}); 26 | 27 | this.User.Tasks = this.User.hasMany(this.Task, {as: 'tasks', foreignKey: 'userId'}); 28 | 29 | this.taskType = new GraphQLObjectType({ 30 | name: this.Task.name, 31 | fields: { 32 | ...attributeFields(this.Task), 33 | id: globalIdField(this.Task.name) 34 | } 35 | }); 36 | 37 | this.beforeSpy = sinon.spy(options => options); 38 | this.afterSpy = sinon.spy(options => options); 39 | 40 | this.viewerTaskConnection = sequelizeConnection({ 41 | name: 'Viewer' + this.Task.name, 42 | nodeType: this.taskType, 43 | target: this.User.Tasks, 44 | before: this.beforeSpy, 45 | after: this.afterSpy 46 | }); 47 | 48 | this.viewerType = new GraphQLObjectType({ 49 | name: 'Viewer', 50 | fields: { 51 | tasks: { 52 | type: this.viewerTaskConnection.connectionType, 53 | args: this.viewerTaskConnection.connectionArgs, 54 | resolve: this.viewerTaskConnection.resolve 55 | } 56 | } 57 | }); 58 | 59 | this.schema = new GraphQLSchema({ 60 | query: new GraphQLObjectType({ 61 | name: 'RootQueryType', 62 | fields: { 63 | viewer: { 64 | type: this.viewerType, 65 | resolve: function (source, args, {viewer}) { 66 | return viewer; 67 | } 68 | } 69 | } 70 | }) 71 | }); 72 | }); 73 | 74 | beforeEach(function () { 75 | this.sinon = sinon.sandbox.create(); 76 | 77 | this.viewer = this.User.build({ 78 | id: Math.ceil(Math.random() * 999) 79 | }); 80 | 81 | const task = this.Task.build({ 82 | id: 1, 83 | }); 84 | 85 | task.dataValues.full_count = Math.random() * 999; 86 | this.sinon.stub(this.Task, 'findAll').resolves([task]); 87 | this.sinon.stub(this.User, this.User.findByPk ? 'findByPk' : 'findById').resolves(this.User.build()); 88 | }); 89 | 90 | afterEach(function () { 91 | this.sinon.restore(); 92 | }); 93 | 94 | it('passes context, root and info to before', async function () { 95 | const result = await graphql({ 96 | schema: this.schema, 97 | source: ` 98 | query { 99 | viewer { 100 | tasks { 101 | edges { 102 | node { 103 | id 104 | } 105 | } 106 | } 107 | } 108 | } 109 | `, 110 | contextValue: { 111 | viewer: this.viewer 112 | }, 113 | }); 114 | 115 | if (result.errors) throw new Error(result.errors[0]); 116 | 117 | expect(this.beforeSpy).to.have.been.calledOnce; 118 | expect(this.beforeSpy).to.have.been.calledWithMatch( 119 | sinon.match.any, 120 | sinon.match({ 121 | first: sinon.match.any 122 | }), 123 | sinon.match({ 124 | viewer: { 125 | id: this.viewer.id 126 | } 127 | }), 128 | sinon.match({ 129 | ast: sinon.match.any 130 | }) 131 | ); 132 | 133 | expect(this.afterSpy).to.have.been.calledWithMatch( 134 | sinon.match({ 135 | fullCount: sinon.match.number 136 | }), 137 | sinon.match({ 138 | first: sinon.match.any 139 | }), 140 | sinon.match({ 141 | viewer: { 142 | id: this.viewer.id 143 | } 144 | }), 145 | sinon.match({ 146 | path: sinon.match.any 147 | }) 148 | ); 149 | 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /test/unit/relay/mutation.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {expect} from 'chai'; 4 | import Sequelize from 'sequelize'; 5 | import sinon from 'sinon'; 6 | import attributeFields from '../../../src/attributeFields'; 7 | import { sequelize } from '../../support/helper' 8 | 9 | import { 10 | sequelizeConnection 11 | } from '../../../src/relay'; 12 | 13 | import { 14 | GraphQLString, 15 | GraphQLInt, 16 | GraphQLFloat, 17 | GraphQLNonNull, 18 | GraphQLBoolean, 19 | GraphQLEnumType, 20 | GraphQLList, 21 | GraphQLObjectType, 22 | GraphQLSchema, 23 | GraphQLID, 24 | graphql 25 | } from 'graphql'; 26 | 27 | import { 28 | globalIdField, 29 | toGlobalId, 30 | fromGlobalId, 31 | mutationWithClientMutationId 32 | } from 'graphql-relay'; 33 | 34 | describe('relay', function () { 35 | describe('mutation', function () { 36 | describe('connections', function () { 37 | before(function () { 38 | this.User = sequelize.define('user', {}, {timestamps: false}); 39 | this.Task = sequelize.define('task', {title: Sequelize.STRING}, {timestamps: false}); 40 | 41 | this.User.Tasks = this.User.hasMany(this.Task, {as: 'tasks', foreignKey: 'userId'}); 42 | 43 | this.taskType = new GraphQLObjectType({ 44 | name: this.Task.name, 45 | fields: { 46 | ...attributeFields(this.Task), 47 | id: globalIdField(this.Task.name) 48 | } 49 | }); 50 | 51 | this.viewerTaskConnection = sequelizeConnection({ 52 | name: 'Viewer' + this.Task.name, 53 | nodeType: this.taskType, 54 | target: this.User.Tasks, 55 | orderBy: new GraphQLEnumType({ 56 | name: 'Viewer' + this.Task.name + 'ConnectionOrder', 57 | values: { 58 | ID: {value: [this.Task.primaryKeyAttribute, 'ASC']}, 59 | } 60 | }) 61 | }); 62 | 63 | this.viewerType = new GraphQLObjectType({ 64 | name: 'Viewer', 65 | fields: { 66 | tasks: { 67 | type: this.viewerTaskConnection.connectionType, 68 | args: this.viewerTaskConnection.connectionArgs, 69 | resolve: this.viewerTaskConnection.resolve 70 | } 71 | } 72 | }); 73 | 74 | const addTaskMutation = mutationWithClientMutationId({ 75 | name: 'addTask', 76 | inputFields: { 77 | title: { 78 | type: new GraphQLNonNull(GraphQLString) 79 | } 80 | }, 81 | outputFields: () => ({ 82 | viewer: { 83 | type: this.viewerType, 84 | resolve: (payload, {viewer}) => { 85 | return viewer; 86 | } 87 | }, 88 | task: { 89 | type: this.taskType, 90 | resolve: (payload) => payload.task 91 | }, 92 | newTaskEdge: { 93 | type: this.viewerTaskConnection.edgeType, 94 | resolve: (payload) => this.viewerTaskConnection.resolveEdge(payload.task) 95 | } 96 | }), 97 | mutateAndGetPayload: async ({title}, {viewer}) => { 98 | let task = await this.Task.create({ 99 | title: title, 100 | userId: viewer.id 101 | }); 102 | 103 | return { 104 | task: task 105 | }; 106 | } 107 | }); 108 | 109 | this.schema = new GraphQLSchema({ 110 | query: new GraphQLObjectType({ 111 | name: 'RootQueryType', 112 | fields: { 113 | viewer: { 114 | type: this.viewerType, 115 | resolve: function (source, args, {viewer}) { 116 | return viewer; 117 | } 118 | } 119 | } 120 | }), 121 | mutation: new GraphQLObjectType({ 122 | name: 'Mutation', 123 | fields: { 124 | addTask: addTaskMutation 125 | } 126 | }) 127 | }); 128 | }); 129 | 130 | beforeEach(function () { 131 | this.sinon = sinon.sandbox.create(); 132 | 133 | this.viewer = this.User.build({ 134 | id: Math.ceil(Math.random() * 999) 135 | }); 136 | 137 | this.sinon.stub(this.Task, 'create').resolves(); 138 | }); 139 | 140 | afterEach(function () { 141 | this.sinon.restore(); 142 | }); 143 | 144 | describe('addEdgeMutation', function () { 145 | it('should return a appropriate cursor and node', async function () { 146 | let title = Math.random().toString() 147 | , id = Math.ceil(Math.random() * 999); 148 | 149 | this.Task.create.resolves(this.Task.build({ 150 | id: id, 151 | title: title, 152 | userId: this.viewer.get('id') 153 | })); 154 | 155 | let result = await graphql({ 156 | schema: this.schema, 157 | source: ` 158 | mutation { 159 | addTask(input: {title: "${title}", clientMutationId: "${Math.random().toString()}"}) { 160 | task { 161 | id 162 | } 163 | 164 | newTaskEdge { 165 | cursor 166 | node { 167 | id 168 | title 169 | } 170 | } 171 | } 172 | } 173 | `, 174 | contextValue: { 175 | viewer: this.viewer 176 | } 177 | }); 178 | 179 | if (result.errors) throw new Error(result.errors[0].stack); 180 | 181 | expect(result.data.addTask.task.id).to.equal(toGlobalId(this.Task.name, id)); 182 | expect(result.data.addTask.newTaskEdge.cursor).to.be.ok; 183 | expect(result.data.addTask.newTaskEdge.node.id).to.equal(toGlobalId(this.Task.name, id)); 184 | expect(result.data.addTask.newTaskEdge.node.title).to.equal(title); 185 | }); 186 | }); 187 | }); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /test/unit/replaceWhereOperators.test.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {replaceWhereOperators} from '../../src/replaceWhereOperators'; 3 | import {Sequelize} from 'sequelize'; 4 | 5 | const [seqMajVer] = Sequelize.version.split('.'); 6 | 7 | describe('replaceWhereOperators', () => { 8 | it('should take an Object of grapqhl-friendly keys and replace with the correct sequelize operators', ()=> { 9 | 10 | let before = { 11 | and: 1, 12 | or: '1', 13 | gt: [{and: '1', or: '1'}, {between: '1', overlap: '1'}], 14 | gte: 1, 15 | lt: { 16 | and: { 17 | test: [{or: '1'}] 18 | } 19 | }, 20 | lte: 1, 21 | ne: 1, 22 | between: 1, 23 | notBetween: 1, 24 | in: 1, 25 | notIn: 1, 26 | notLike: 1, 27 | iLike: 1, 28 | notILike: 1, 29 | like: 1, 30 | overlap: 1, 31 | contains: 1, 32 | contained: 1, 33 | any: 1, 34 | col: 1 35 | }; 36 | 37 | let after; 38 | if (seqMajVer <= 3) { 39 | after = { 40 | $and: 1, 41 | $or: '1', 42 | $gt: [{$and: '1', $or: '1'}, {$between: '1', $overlap: '1'}], 43 | $gte: 1, 44 | $lt: { 45 | $and: { 46 | test: [{$or: '1'}] 47 | } 48 | }, 49 | $lte: 1, 50 | $ne: 1, 51 | $between: 1, 52 | $notBetween: 1, 53 | $in: 1, 54 | $notIn: 1, 55 | $notLike: 1, 56 | $iLike: 1, 57 | $notILike: 1, 58 | $like: 1, 59 | $overlap: 1, 60 | $contains: 1, 61 | $contained: 1, 62 | $any: 1, 63 | $col: 1 64 | }; 65 | } else { 66 | after = { 67 | [Sequelize.Op.and]: 1, 68 | [Sequelize.Op.or]: '1', 69 | [Sequelize.Op.gt]: [ 70 | { 71 | [Sequelize.Op.and]: '1', 72 | [Sequelize.Op.or]: '1' 73 | }, 74 | { 75 | [Sequelize.Op.between]: '1', 76 | [Sequelize.Op.overlap]: '1' 77 | } 78 | ], 79 | [Sequelize.Op.gte]: 1, 80 | [Sequelize.Op.lt]: { 81 | [Sequelize.Op.and]: { 82 | test: [{[Sequelize.Op.or]: '1'}] 83 | } 84 | }, 85 | [Sequelize.Op.lte]: 1, 86 | [Sequelize.Op.ne]: 1, 87 | [Sequelize.Op.between]: 1, 88 | [Sequelize.Op.notBetween]: 1, 89 | [Sequelize.Op.in]: 1, 90 | [Sequelize.Op.notIn]: 1, 91 | [Sequelize.Op.notLike]: 1, 92 | [Sequelize.Op.iLike]: 1, 93 | [Sequelize.Op.notILike]: 1, 94 | [Sequelize.Op.like]: 1, 95 | [Sequelize.Op.overlap]: 1, 96 | [Sequelize.Op.contains]: 1, 97 | [Sequelize.Op.contained]: 1, 98 | [Sequelize.Op.any]: 1, 99 | [Sequelize.Op.col]: 1 100 | }; 101 | 102 | } 103 | expect(replaceWhereOperators(before)).to.deep.equal(after); 104 | }); 105 | 106 | it('should not mutate argument', () => { 107 | const before = { 108 | prop1: {gt: 12}, 109 | prop2: {or: [{eq: 3}, {eq: 4}]} 110 | }; 111 | function proxify(target) { 112 | return new Proxy(target, { 113 | get(target, prop) { 114 | const value = target[prop]; 115 | return typeof value === 'object' ? proxify(value) : value; 116 | }, 117 | set(target, prop, value) { 118 | expect.fail('It tryes to change argument'); 119 | } 120 | }); 121 | } 122 | let after; 123 | if (seqMajVer <= 3) { 124 | after = { 125 | prop1: {$gt: 12}, 126 | prop2: {$or: [{$eq: 3}, {$eq: 4}]} 127 | }; 128 | } else { 129 | after = { 130 | prop1: {[Sequelize.Op.gt]: 12}, 131 | prop2: {[Sequelize.Op.or]: [{[Sequelize.Op.eq]: 3}, {[Sequelize.Op.eq]: 4}]} 132 | } 133 | } 134 | expect(replaceWhereOperators(proxify(before))).to.deep.equal(after); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /test/unit/simplifyAST.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {expect} from 'chai'; 4 | import simplifyAST from '../../src/simplifyAST'; 5 | var parser = require('graphql/language/parser').parse // eslint-disable-line 6 | , parse = function (query) { 7 | return parser(query).definitions[0]; 8 | }; 9 | 10 | describe('simplifyAST', function () { 11 | it('should simplify a basic nested structure', function () { 12 | expect(simplifyAST(parse(` 13 | { 14 | users { 15 | name 16 | projects { 17 | name 18 | } 19 | } 20 | } 21 | `))).to.deep.equal({ 22 | args: {}, 23 | fields: { 24 | users: { 25 | args: {}, 26 | fields: { 27 | name: { 28 | args: {}, 29 | fields: {} 30 | }, 31 | projects: { 32 | args: {}, 33 | fields: { 34 | name: { 35 | args: {}, 36 | fields: {} 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | }); 44 | }); 45 | 46 | it('should simplify a basic structure with args', function () { 47 | expect(simplifyAST(parse(` 48 | { 49 | user(id: 1) { 50 | name 51 | } 52 | } 53 | `))).to.deep.equal({ 54 | args: {}, 55 | fields: { 56 | user: { 57 | args: { 58 | id: '1' 59 | }, 60 | fields: { 61 | name: { 62 | args: {}, 63 | fields: {} 64 | } 65 | } 66 | } 67 | } 68 | }); 69 | }); 70 | 71 | it('should simplify a basic structure with array args', function () { 72 | expect(simplifyAST(parse(` 73 | { 74 | luke: human(id: ["1000", "1003"]) { 75 | name 76 | } 77 | } 78 | `))).to.deep.equal({ 79 | args: {}, 80 | fields: { 81 | luke: { 82 | key: 'human', 83 | args: { 84 | id: ['1000', '1003'] 85 | }, 86 | fields: { 87 | name: { 88 | args: {}, 89 | fields: {} 90 | } 91 | } 92 | } 93 | } 94 | }); 95 | }); 96 | 97 | it('should simplify a basic structure with object args', function () { 98 | expect(simplifyAST(parse(` 99 | { 100 | luke: human(contact: { phone: "91264646" }) { 101 | name 102 | } 103 | } 104 | `))).to.deep.equal({ 105 | args: {}, 106 | fields: { 107 | luke: { 108 | key: 'human', 109 | args: { 110 | contact: { phone: '91264646' } 111 | }, 112 | fields: { 113 | name: { 114 | args: {}, 115 | fields: {} 116 | } 117 | } 118 | } 119 | } 120 | }); 121 | }); 122 | 123 | it('should simplify a basic structure with nested array args', function () { 124 | expect(simplifyAST(parse(` 125 | { 126 | user(units: ["1", "2", ["3", ["4"], [["5"], "6"], "7"]]) { 127 | name 128 | } 129 | } 130 | `))).to.deep.equal({ 131 | args: {}, 132 | fields: { 133 | user: { 134 | args: { 135 | units: ['1', '2', ['3', ['4'], [['5'], '6'], '7']] 136 | }, 137 | fields: { 138 | name: { 139 | args: {}, 140 | fields: {} 141 | } 142 | } 143 | } 144 | } 145 | }); 146 | }); 147 | 148 | it('should simplify a basic structure with variable args', function () { 149 | expect(simplifyAST(parse(` 150 | { 151 | user(id: $id) { 152 | name 153 | } 154 | } 155 | `), { 156 | variableValues: { 157 | id: '1' 158 | } 159 | })).to.deep.equal({ 160 | args: {}, 161 | fields: { 162 | user: { 163 | args: { 164 | id: '1' 165 | }, 166 | fields: { 167 | name: { 168 | args: {}, 169 | fields: {} 170 | } 171 | } 172 | } 173 | } 174 | }); 175 | }); 176 | 177 | it('should simplify a basic structure with an inline fragment', function () { 178 | expect(simplifyAST(parse(` 179 | { 180 | user { 181 | ... on User { 182 | name 183 | } 184 | } 185 | } 186 | `))).to.deep.equal({ 187 | args: {}, 188 | fields: { 189 | user: { 190 | args: {}, 191 | fields: { 192 | name: { 193 | args: {}, 194 | fields: {} 195 | } 196 | } 197 | } 198 | } 199 | }); 200 | }); 201 | 202 | it('should expose a $parent', function () { 203 | var ast = simplifyAST(parse(` 204 | { 205 | users { 206 | name 207 | projects(first: 1) { 208 | nodes { 209 | name 210 | } 211 | } 212 | } 213 | } 214 | `)); 215 | 216 | expect(ast.fields.users.fields.projects.fields.nodes.$parent).to.be.ok; 217 | expect(ast.fields.users.fields.projects.fields.nodes.$parent.args).to.deep.equal({ 218 | first: '1' 219 | }); 220 | }); 221 | 222 | it('should simplify a nested structure at the lowest level', function () { 223 | expect(simplifyAST(parse(` 224 | { 225 | users { 226 | name 227 | projects { 228 | node { 229 | name 230 | } 231 | node { 232 | id 233 | } 234 | } 235 | } 236 | } 237 | `))).to.deep.equal({ 238 | args: {}, 239 | fields: { 240 | users: { 241 | args: {}, 242 | fields: { 243 | name: { 244 | args: {}, 245 | fields: {} 246 | }, 247 | projects: { 248 | args: {}, 249 | fields: { 250 | node: { 251 | args: {}, 252 | fields: { 253 | name: { 254 | args: {}, 255 | fields: {} 256 | }, 257 | id: { 258 | args: {}, 259 | fields: {} 260 | } 261 | } 262 | } 263 | } 264 | } 265 | } 266 | } 267 | } 268 | }); 269 | }); 270 | 271 | it('should simplify a nested structure duplicated at a high level', function () { 272 | expect(simplifyAST(parse(` 273 | { 274 | users { 275 | name 276 | projects { 277 | node { 278 | name 279 | } 280 | } 281 | projects { 282 | node { 283 | id 284 | } 285 | } 286 | } 287 | } 288 | `))).to.deep.equal({ 289 | args: {}, 290 | fields: { 291 | users: { 292 | args: {}, 293 | fields: { 294 | name: { 295 | args: {}, 296 | fields: {} 297 | }, 298 | projects: { 299 | args: {}, 300 | fields: { 301 | node: { 302 | args: {}, 303 | fields: { 304 | name: { 305 | args: {}, 306 | fields: {} 307 | }, 308 | id: { 309 | args: {}, 310 | fields: {} 311 | } 312 | } 313 | } 314 | } 315 | } 316 | } 317 | } 318 | } 319 | }); 320 | }); 321 | 322 | it('should simplify a structure with aliases', function () { 323 | expect(simplifyAST(parse(` 324 | { 325 | luke: human(id: "1000") { 326 | name 327 | } 328 | leia: human(id: "1003") { 329 | firstName: name 330 | } 331 | } 332 | `))).to.deep.equal({ 333 | args: {}, 334 | fields: { 335 | luke: { 336 | key: 'human', 337 | args: { 338 | id: '1000' 339 | }, 340 | fields: { 341 | name: { 342 | args: {}, 343 | fields: {} 344 | } 345 | } 346 | }, 347 | leia: { 348 | key: 'human', 349 | args: { 350 | id: '1003' 351 | }, 352 | fields: { 353 | firstName: { 354 | key: 'name', 355 | args: {}, 356 | fields: {} 357 | } 358 | } 359 | } 360 | } 361 | }); 362 | }); 363 | }); 364 | -------------------------------------------------------------------------------- /test/unit/typeMapper.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { mapType, toGraphQL } from '../../src/typeMapper'; 3 | import JSONType from '../../src/types/jsonType'; 4 | import DateType from '../../src/types/dateType'; 5 | 6 | import Sequelize from 'sequelize'; 7 | 8 | const { 9 | BOOLEAN, 10 | ENUM, 11 | FLOAT, 12 | REAL, 13 | CHAR, 14 | DECIMAL, 15 | DOUBLE, 16 | INTEGER, 17 | BIGINT, 18 | STRING, 19 | TEXT, 20 | UUID, 21 | UUIDV4, 22 | DATE, 23 | DATEONLY, 24 | TIME, 25 | ARRAY, 26 | VIRTUAL, 27 | JSON, 28 | JSONB, 29 | INET, 30 | } = Sequelize; 31 | 32 | import { 33 | GraphQLString, 34 | GraphQLInt, 35 | GraphQLBoolean, 36 | GraphQLFloat, 37 | GraphQLEnumType, 38 | GraphQLList 39 | } from 'graphql'; 40 | 41 | describe('typeMapper', () => { 42 | 43 | describe('ARRAY', function () { 44 | it('should map to instance of GraphQLList', function () { 45 | expect(toGraphQL(new ARRAY(STRING), Sequelize)).to.instanceof(GraphQLList); 46 | }); 47 | }); 48 | 49 | describe('BIGINT', function () { 50 | it('should map to GraphQLString', function () { 51 | expect(toGraphQL(new BIGINT(), Sequelize)).to.equal(GraphQLString); 52 | }); 53 | }); 54 | 55 | describe('BOOLEAN', function () { 56 | it('should map to GraphQLBoolean', function () { 57 | expect(toGraphQL(new BOOLEAN(), Sequelize)).to.equal(GraphQLBoolean); 58 | }); 59 | }); 60 | 61 | describe('CHAR', function () { 62 | it('should map to GraphQLString', function () { 63 | expect(toGraphQL(new CHAR(), Sequelize)).to.equal(GraphQLString); 64 | }); 65 | }); 66 | 67 | describe('CUSTOM', function () { 68 | before(function () { 69 | // setup mapping 70 | mapType((type)=> { 71 | if (type instanceof BOOLEAN) { 72 | return GraphQLString; 73 | } 74 | if (type instanceof FLOAT) { 75 | return false; 76 | } 77 | }); 78 | }); 79 | it('should fallback to default types if it returns false', function () { 80 | expect(toGraphQL(new FLOAT(), Sequelize)).to.equal(GraphQLFloat); 81 | }); 82 | it('should allow the user to map types to anything', function () { 83 | expect(toGraphQL(new BOOLEAN(), Sequelize)).to.equal(GraphQLString); 84 | }); 85 | 86 | // reset mapType 87 | after(function () { 88 | mapType(null); 89 | }); 90 | 91 | }); 92 | 93 | describe('DATE', function () { 94 | it('should map to DateType', function () { 95 | expect(toGraphQL(new DATE(), Sequelize)).to.equal(DateType); 96 | }); 97 | }); 98 | 99 | describe('DATEONLY', function () { 100 | it('should map to GraphQLString', function () { 101 | expect(toGraphQL(new DATEONLY(), Sequelize)).to.equal(GraphQLString); 102 | }); 103 | }); 104 | 105 | describe('DECIMAL', function () { 106 | it('should map to GraphQLString', function () { 107 | expect(toGraphQL(new DECIMAL(), Sequelize)).to.equal(GraphQLString); 108 | }); 109 | }); 110 | 111 | describe('DOUBLE', function () { 112 | it('should map to GraphQLFloat', function () { 113 | expect(toGraphQL(new DOUBLE(), Sequelize)).to.equal(GraphQLFloat); 114 | }); 115 | }); 116 | 117 | describe('ENUM', function () { 118 | it('should map to instance of GraphQLEnumType', function () { 119 | expect( 120 | toGraphQL( 121 | new ENUM( 122 | 'value', 123 | 'another value', 124 | 'two--specials', 125 | '25.8', 126 | '¼', 127 | '¼½', 128 | '¼ ½', 129 | '¼_½', 130 | ' ¼--½_¾ - ' 131 | ) 132 | , Sequelize 133 | ) 134 | ).to.instanceof(GraphQLEnumType); 135 | }); 136 | }); 137 | 138 | describe('FLOAT', function () { 139 | it('should map to GraphQLFloat', function () { 140 | expect(toGraphQL(new FLOAT(), Sequelize)).to.equal(GraphQLFloat); 141 | }); 142 | }); 143 | 144 | describe('REAL', function () { 145 | it('should map to GraphQLFloat', function () { 146 | expect(toGraphQL(new REAL(), Sequelize)).to.equal(GraphQLFloat); 147 | }); 148 | }); 149 | 150 | describe('INTEGER', function () { 151 | it('should map to GraphQLInt', function () { 152 | expect(toGraphQL(new INTEGER(), Sequelize)).to.equal(GraphQLInt); 153 | }); 154 | }); 155 | 156 | describe('STRING', function () { 157 | it('should map to GraphQLString', function () { 158 | expect(toGraphQL(new STRING(), Sequelize)).to.equal(GraphQLString); 159 | }); 160 | }); 161 | 162 | describe('TEXT', function () { 163 | it('should map to GraphQLString', function () { 164 | expect(toGraphQL(new TEXT(), Sequelize)).to.equal(GraphQLString); 165 | }); 166 | }); 167 | 168 | describe('TIME', function () { 169 | it('should map to GraphQLString', function () { 170 | expect(toGraphQL(new TIME(), Sequelize)).to.equal(GraphQLString); 171 | }); 172 | }); 173 | 174 | describe('UUID', function () { 175 | it('should map to GraphQLString', function () { 176 | expect(toGraphQL(new UUID(), Sequelize)).to.equal(GraphQLString); 177 | }); 178 | }); 179 | 180 | describe('UUIDV4', function () { 181 | it('should map to GraphQLString', function () { 182 | expect(toGraphQL(new UUIDV4(), Sequelize)).to.equal(GraphQLString); 183 | }); 184 | }); 185 | 186 | describe('VIRTUAL', function () { 187 | 188 | it('should map to the sequelize return type', function () { 189 | expect(toGraphQL(new VIRTUAL(BOOLEAN, ['createdAt']), Sequelize)).to.equal(GraphQLBoolean); 190 | }); 191 | 192 | it('should default to a GraphQLString is a return type is not provided', function () { 193 | expect(toGraphQL(new VIRTUAL(), Sequelize)).to.equal(GraphQLString); 194 | }); 195 | 196 | }); 197 | 198 | describe('JSON', function () { 199 | it('should map to JSONType', function () { 200 | expect(toGraphQL(new JSON(), Sequelize)).to.equal(JSONType); // eslint-disable-line 201 | }); 202 | }); 203 | 204 | describe('JSONB', function () { 205 | it('should map to JSONType', function () { 206 | expect(toGraphQL(new JSONB(), Sequelize)).to.equal(JSONType); 207 | }); 208 | }); 209 | 210 | describe('INET', function () { 211 | it('should map to instance of GraphQLString', function () { 212 | expect(toGraphQL(new INET('127.0.0.1'), Sequelize)).to.equal(GraphQLString); 213 | }); 214 | }); 215 | }); 216 | --------------------------------------------------------------------------------