├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── server.js ├── lib ├── RestQL.js ├── common.js ├── loaders.js ├── methods.js ├── middlewares.js └── router.js ├── package.json └── test ├── belongsTo.js ├── belongsToMany.js ├── common.js ├── hasMany.js ├── hasOne.js ├── lib ├── assert.js ├── config.js ├── prepare.js └── test.js ├── mock ├── data.js └── models │ ├── gameofthrones │ ├── character.js │ ├── house.js │ └── seat.js │ ├── user.js │ └── user_characters.js ├── models.js ├── query-attributes.js ├── query-group.js ├── query-include.js ├── query-order.js ├── query-pagination.js ├── query-subQuery.js ├── query-through.js ├── query-where.js ├── restql.js └── setup.js /.editorconfig: -------------------------------------------------------------------------------- 1 | ;.editorconfig 2 | 3 | root = true 4 | 5 | [**.js] 6 | indent_style = space 7 | indent_size = 2 8 | jslint_happy = true 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | .idea 37 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | ._* 3 | .DS_Store 4 | .git 5 | .hg 6 | .npmrc 7 | .lock-wscript 8 | .svn 9 | .wafpickle-* 10 | config.gypi 11 | CVS 12 | npm-debug.log 13 | test 14 | examples 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | before_script: 7 | - mysql -u root -e 'create database test;' 8 | 9 | env: 10 | - TEST_DB="mysql://root@localhost/test#UT8" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 koa-restql contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # koa-restql 2 | 3 | [![Travis branch][travis-image]][travis-url] 4 | [![NPM version][npm-image]][npm-url] 5 | 6 | Build **real** RESTful APIs without writing one line of code. Cool, right? 7 | 8 | Now it works perfectly with MySQL. 9 | 10 | [toc] 11 | 12 | ## Installation 13 | 14 | koa-restql requires node v6.0.0 or higher for (partial) ES2015 support. 15 | 16 | ```sh 17 | npm install --save koa-restql 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```js 23 | const koa = require('koa') 24 | const RestQL = require('koa-restql') 25 | 26 | let app = koa() 27 | let restql = new RestQL(sequelize.models) // Build APIs from `sequelize.models` 28 | app.use(restql.routes()) 29 | ``` 30 | 31 | ## How to request ***real*** RESTful APIs 32 | 33 | ### Basic 34 | 35 | ``` 36 | GET /user 37 | ``` 38 | 39 | If you just have one database table and sequelize model both named `user`, just choose [the right HTTP method][9] to visit path as exactly same name as it. 40 | 41 | Using `querystring` in your url can add condition or limit for the request. For more details, read about [`querystring`][2]. 42 | 43 | ### List 44 | 45 | * Request 46 | 47 | ``` 48 | GET /user 49 | ``` 50 | 51 | * Response 52 | 53 | ``` 54 | HTTP/1.1 206 Partial Content 55 | X-Range: items 0-2/10 56 | ``` 57 | 58 | ``` 59 | [ 60 | { 61 | "id": 1, 62 | "name": "Li Xin" 63 | }, 64 | { 65 | "id": 2, 66 | "name": "Zhang Chi" 67 | } 68 | ] 69 | ``` 70 | 71 | ***Note***: 72 | * Request for a list will always respond an array. 73 | * This response example include necessary HTTP headers to explain how `Partial Content` works. If the response was just part of the list, the API would like to response HTTP status code [206][1]. 74 | 75 | ### Single 76 | 77 | * Request 78 | 79 | ``` 80 | GET /user/1 81 | ``` 82 | 83 | * Response 84 | 85 | ``` 86 | { 87 | "id": 1, 88 | "name": "Li Xin" 89 | } 90 | ``` 91 | 92 | ***Note***: Request path with id will always respond an object. 93 | 94 | ### Association 95 | 96 | #### 1:1 97 | 98 | To define an 1:1 association with sequelize, use [`model.hasOne()`][3] or [`model.belongsTo()`][4]. 99 | 100 | * Request 101 | 102 | ``` 103 | GET /user/1/profile 104 | ``` 105 | 106 | * Response 107 | 108 | ``` 109 | { 110 | "id": 1, 111 | "user_id": 1, 112 | "site": "https://github.com/crzidea" 113 | } 114 | ``` 115 | ***Note***: This example is for `hasOne()`. If the `profile` was an association defined with `belongTo()`, there should not be `user_id` field. 116 | 117 | #### 1:N 118 | 119 | To define an 1:N association with sequelize, use [`model.belongsTo()`][5]. 120 | 121 | ##### List 122 | 123 | * Request 124 | 125 | ``` 126 | GET /user/1/messages 127 | ``` 128 | 129 | * Response 130 | 131 | ``` 132 | [ 133 | { 134 | "id": 1, 135 | "content": "hello" 136 | }, 137 | { 138 | "id": 2, 139 | "content": "world" 140 | } 141 | ] 142 | ``` 143 | 144 | ##### Single 145 | 146 | * Request 147 | 148 | ``` 149 | GET /user/1/messages/2 150 | ``` 151 | 152 | * Response 153 | 154 | ``` 155 | { 156 | "id": 2, 157 | "content": "world" 158 | } 159 | ``` 160 | 161 | #### N:M 162 | 163 | To define an N:M association with sequelize, use [`model.belongsToMany()`][6]. 164 | 165 | Basicly, you can use the same way to request n:n association as [1:N association][7]. The difference is response. 166 | 167 | * Request 168 | 169 | ``` 170 | GET /user/1/friends/2 171 | ``` 172 | 173 | * Response 174 | 175 | ``` 176 | { 177 | "id": 2, 178 | "name": "Zhang Chi", 179 | "friendship": { 180 | "id": 1, 181 | "user_id": 1, 182 | "friend_id": 2 183 | } 184 | } 185 | ``` 186 | ***Note***: RestQL will respond the target model with another model referred `through` option. 187 | 188 | Another noticeable problem is, you can not do the following query with association path although it is supported by sequelize: 189 | 190 | ```js 191 | models.user.findAll( 192 | { 193 | include: models.user.association.friends 194 | } 195 | ) 196 | ``` 197 | 198 | But, fortunately, you can implement the query with `querystring` like this: 199 | 200 | ``` 201 | GET /user?_include%5B0%5D=friends 202 | ``` 203 | 204 | [Read more.][2] 205 | 206 | ### CRUD 207 | 208 | RestQL could do all CRUD operations for you. Just choose the right HTTP method to access either the resource or the association path. 209 | 210 | Supported HTTP verbs: 211 | 212 | HTTP verb | CRUD | 213 | --------- | ------------- | 214 | GET | Read | 215 | POST | Create | 216 | PUT | Create/Update | 217 | DELETE | Delete | 218 | 219 | 220 | Supported HTTP method with body: 221 | 222 | HTTP verb | List | Single | 223 | --------- | ------------ | ------ | 224 | POST | Array/Object | × | 225 | PUT | Array/Object | Object | 226 | 227 | 228 | * `List` path examples: 229 | * `/resource` 230 | * `/resource/:id/association`, association is `1:n` relationship 231 | * `/resource/:id/association`, association is `n:m` relationship 232 | * `Single` path examples: 233 | * `/resource/:id` 234 | * `/resource/:id/association`, association is `1:1` relationship 235 | * `/resource/:id/association/:id`, association is `1:n` relationship 236 | * `/resource/:id/association/:id`, association is `n:m` relationship 237 | 238 | ***Note***: `PUT` method must be used with `unique key(s)`, which means you can not use `PUT` method with a request body without an `unique key`. 239 | 240 | To use `POST` or `PUT` method, you should put data into request body. Example: 241 | 242 | ``` 243 | POST /user 244 | 245 | { 246 | "name": "Li Xin" 247 | } 248 | ``` 249 | 250 | ### querystring 251 | 252 | It's strongly recommended that use [`qs`][8] to stringify nesting `querystring`s. And this document will assume you will use `qs` to stringify querystring from JavaScript object. 253 | 254 | Example: 255 | 256 | ```js 257 | qs.stringify({a: 1, b:2}) // => a=1&b=2 258 | ``` 259 | 260 | To understand RestQL querystring, there are only 3 rules: 261 | 262 | * Every keys in querystring **not** start with `_`, will be directly used as `where` option for `sequelize#query()`. Example: 263 | 264 | ```js 265 | // query 266 | { 267 | name: "Li Xin" 268 | } 269 | // option for sequelize 270 | { 271 | where: { 272 | name: "Li Xin" 273 | } 274 | } 275 | ``` 276 | 277 | * Every keys in querystring start with `_`, will be directly used as `sequelize#query()`. 278 | 279 | ```js 280 | // query 281 | { 282 | _limit: 10 283 | } 284 | // option for sequelize 285 | { 286 | limit: 10 287 | } 288 | ``` 289 | 290 | * `include` option for `sequelize#query()` should be passed as `String` of association name. 291 | 292 | ```js 293 | // query 294 | { 295 | _include: ['friends'] 296 | } 297 | // option for sequelize 298 | { 299 | include: [ 300 | models.user.association.friends 301 | ] 302 | } 303 | ``` 304 | 305 | Sometimes, you want modify `query` in your own middleware. To do so, you should modify `this.restql.query` instead of `this.request.query` or `this.query`, because the `query` MUST be parsed with the package `qs`, not `querystring` (which is default package of koa). 306 | 307 | ### Access Control 308 | 309 | There are at least 2 ways to implement the `Access Control`: 310 | 311 | 1. Add another middleware before request be handled by RestQL. 312 | 2. Add options on `sequelize#model#associations`, RestQL will handle the options. 313 | 314 | This document will only talk about the 2nd way. And the option was only support with associations, not with models. 315 | 316 | 1. To specify which association should not be accessed by RestQL, add `ignore` option. Example: 317 | 318 | ```js 319 | models.user.hasOne( 320 | models.privacy, 321 | { 322 | restql: { 323 | ignore: true 324 | } 325 | } 326 | ) 327 | ``` 328 | 329 | 2. To specify an association should not be accessed by specific HTTP method, add the method to `ignore` as an array element. Example: 330 | 331 | ```js 332 | models.user.hasOne( 333 | models.privacy, 334 | { 335 | restql: { 336 | ignore: ['get'] 337 | } 338 | } 339 | ) 340 | ``` 341 | 342 | ## Running tests 343 | 344 | ```sh 345 | npm test 346 | ``` 347 | 348 | ## License 349 | 350 | MIT 351 | 352 | [travis-image]: https://img.shields.io/travis/Meituan-Dianping/koa-restql/master.svg?maxAge=0 353 | [travis-url]: https://travis-ci.org/Meituan-Dianping/koa-restql 354 | [npm-image]: https://img.shields.io/npm/v/koa-restql.svg?maxAge=0 355 | [npm-url]: https://www.npmjs.com/package/koa-restql 356 | [1]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html 357 | [2]: #querystring 358 | [3]: https://github.com/sequelize/sequelize/blob/master/docs/api/associations/has-one.md 359 | [4]: https://github.com/sequelize/sequelize/blob/master/docs/api/associations/belongs-to.md 360 | [5]: https://github.com/sequelize/sequelize/blob/master/docs/api/associations/has-many.md 361 | [6]: https://github.com/sequelize/sequelize/blob/master/docs/api/associations/belongs-to-many.md 362 | [7]: #1:N 363 | [8]: https://github.com/ljharb/qs 364 | [9]: #CRUD 365 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const koa = require('koa') 4 | 5 | const prepare = require('../test/lib/prepare') 6 | const RestQL = require('../lib/RestQL') 7 | 8 | const models = prepare.sequelize.models 9 | 10 | const app = koa() 11 | const restql = new RestQL(models) 12 | 13 | app.use(restql.routes()) 14 | app.listen('3000', '0.0.0.0') 15 | -------------------------------------------------------------------------------- /lib/RestQL.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash') 4 | const router = require('./router'); 5 | 6 | /** 7 | * @module koa-restql 8 | */ 9 | 10 | module.exports = RestQL; 11 | 12 | /** 13 | * Create a new Restql 14 | * 15 | * @param {Object} [models={}] 16 | * @param {Object} [options={}] 17 | */ 18 | 19 | function RestQL (models, options) { 20 | 21 | if (!(this instanceof RestQL)) { 22 | return new RestQL(models, options); 23 | } 24 | 25 | options = options || {} 26 | 27 | this.options = _.defaultsDeep(options, { 28 | query: { 29 | _limit: 20 30 | }, 31 | qs: { 32 | arrayLimit : 1000, 33 | strictNullHandling : true 34 | } 35 | }) 36 | 37 | if (!models) { 38 | throw new Error('paramter models does not exist') 39 | } 40 | 41 | this.models = models 42 | this.router = router.load(models, this.options) 43 | 44 | this.routes = () => { 45 | return this.router.routes() 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const debug = require('debug')('koa-restql:common') 5 | 6 | function switchByType (param, callbacks) { 7 | 8 | callbacks = callbacks || {} 9 | 10 | const { 11 | object, array, string, bool, number, defaults 12 | } = callbacks 13 | 14 | let callback 15 | 16 | switch (typeof param) { 17 | case 'object': 18 | if (Array.isArray(param)) { 19 | callback = array 20 | } else { 21 | callback = object 22 | } 23 | break 24 | case 'string': 25 | callback = string 26 | break 27 | case 'boolean': 28 | callback = bool 29 | break 30 | case 'number': 31 | callback = number 32 | break 33 | default: 34 | callback = defaults 35 | break 36 | } 37 | 38 | if (callback !== undefined) { 39 | if ('function' === typeof callback) { 40 | return callback(param) 41 | } else { 42 | return callback 43 | } 44 | } 45 | } 46 | 47 | function shouldIgnoreAssociation (method, options) { 48 | 49 | options = options || {} 50 | 51 | let ignore = options.ignore 52 | 53 | return switchByType(ignore, { 54 | array : () => 55 | ignore.find(ignoreMethod => ignoreMethod.toLowerCase() === method), 56 | bool : () => ignore 57 | }) 58 | 59 | } 60 | 61 | function parseAttributes (_attributes, attributes) { 62 | let attrs 63 | 64 | if (_attributes) 65 | 66 | return switchByType(_attributes, { 67 | array : () => (_attributes.filter(attr => attributes[attr])), 68 | string : () => (_attributes.split(/,/).filter(attr => attributes[attr])) 69 | }) 70 | } 71 | 72 | function unionWhere (_where) { 73 | 74 | return switchByType(_where, { 75 | object : () => { 76 | 77 | let where 78 | 79 | Object.keys(_where).forEach(key => { 80 | if (!/^_/.test(key)) { 81 | where = where || {} 82 | where[key] = _where[key] 83 | } 84 | }) 85 | 86 | return where 87 | } 88 | }) 89 | } 90 | 91 | function parseInclude (_include, associations, method) { 92 | 93 | return switchByType(_include, { 94 | string : () => { 95 | 96 | let association = associations[_include] 97 | 98 | if (!association) 99 | return 100 | 101 | if (shouldIgnoreAssociation(method, association.options.restql)) 102 | return 103 | 104 | return association 105 | }, 106 | 107 | object : () => { 108 | 109 | let include = [] 110 | , where = _include.where 111 | , attributes = _include.attributes 112 | , subQuery = _include.subQuery 113 | , required = !!+_include.required 114 | , through = _include.through 115 | , association = associations[_include.association] 116 | 117 | subQuery = subQuery === undefined ? subQuery : !!+subQuery 118 | 119 | if (!association) 120 | return 121 | 122 | let options = association.options 123 | 124 | if (shouldIgnoreAssociation(method,options.restql)) 125 | return 126 | 127 | if (_include.include) { 128 | include = unionInclude(_include.include, association.target.associations) 129 | } 130 | 131 | return { 132 | where, attributes, through, association, required, include, subQuery 133 | } 134 | 135 | } 136 | 137 | }) 138 | 139 | } 140 | 141 | function unionInclude (_include, associations, method) { 142 | 143 | return switchByType(_include, { 144 | array: () => { 145 | return _include 146 | .map(item => parseInclude(item, associations, method)) 147 | .filter(item => item) 148 | }, 149 | 150 | object: () => { 151 | let include = parseInclude(_include, associations, method) 152 | return include ? [ include ] : [] 153 | }, 154 | 155 | string: () => { 156 | let include = parseInclude(_include, associations, method) 157 | return include ? [ include ] : [] 158 | }, 159 | 160 | defaults: () => ([]) 161 | }) 162 | 163 | } 164 | 165 | function unionLimit (_limit, options) { 166 | 167 | if (_limit !== null) { 168 | return +_limit || +options.query._limit 169 | } 170 | 171 | } 172 | 173 | function parseQuery (query, model, method, options) { 174 | 175 | const queryParsers = { 176 | '_include' : (include) => unionInclude(include, model.associations, method), 177 | '_limit' : (limit) => unionLimit(limit, options), 178 | '_offset' : (offset) => +offset || 0, 179 | '_distinct' : (distinct) => !!+distinct, 180 | '_subQuery' : (subQuery) => subQuery === undefined ? subQuery : !!+subQuery, 181 | '_ignoreDuplicates': (ignoreDuplicates) => !!+ignoreDuplicates 182 | } 183 | 184 | const parsedQuery = {} 185 | 186 | _.keys(query).forEach(key => { 187 | 188 | const regex = /^_/ 189 | const parser = queryParsers[key] 190 | 191 | if (!regex.test(key)) 192 | return 193 | 194 | const propName = key.replace(regex, '') 195 | const propValue = parser ? parser(query[key]) : query[key] 196 | 197 | if (propValue !== undefined) { 198 | parsedQuery[propName] = propValue 199 | } 200 | 201 | }) 202 | 203 | parsedQuery.where = parsedQuery.where || {} 204 | _.assign(parsedQuery.where, unionWhere(query)) 205 | 206 | if (parsedQuery.limit === undefined) { 207 | parsedQuery.limit = queryParsers['_limit'](query._limit) 208 | } 209 | 210 | if (parsedQuery.offset === undefined) { 211 | parsedQuery.offset = queryParsers['_offset'](query._offset) 212 | } 213 | 214 | debug(parsedQuery) 215 | 216 | return parsedQuery 217 | 218 | } 219 | 220 | module.exports.parseQuery = parseQuery 221 | module.exports.switchByType = switchByType 222 | module.exports.shouldIgnoreAssociation = shouldIgnoreAssociation 223 | -------------------------------------------------------------------------------- /lib/loaders.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const co = require('co') 4 | const _ = require('lodash') 5 | const parse = require('co-body') 6 | 7 | const debug = require('debug')('koa-restql:loaders') 8 | const middlewares = require('./middlewares') 9 | const methods = require('./methods') 10 | const common = require('./common') 11 | 12 | const capitalizeFirstLetter = (string) => { 13 | return string.charAt(0).toUpperCase() + string.slice(1) 14 | } 15 | 16 | const { 17 | switchByType 18 | } = common 19 | 20 | const loaders = {} 21 | loaders.model = {} 22 | loaders.model.association = {} 23 | loaders.model.association.singular = {} 24 | loaders.model.association.singular.hasOne = {} 25 | loaders.model.association.singular.belongsTo = {} 26 | loaders.model.association.plural = {} 27 | loaders.model.association.plural.hasMany = {} 28 | loaders.model.association.plural.belongsToMany = {} 29 | 30 | function hasPluralAssociation (include) { 31 | 32 | include = include || [] 33 | 34 | return include.some(include => 35 | include.association && include.association.isMultiAssociation) 36 | 37 | } 38 | 39 | /** 40 | * load GET /user and GET /user/:id 41 | */ 42 | loaders.model.get = (router, base, model, options) => { 43 | 44 | router.get(base, 45 | middlewares.before(), 46 | middlewares.parseQuery(model, options), 47 | function * (next) { 48 | 49 | const { 50 | request, response, query 51 | } = this.restql 52 | 53 | if (hasPluralAssociation(query.include)) { 54 | query.distinct = true 55 | } 56 | 57 | response.body = 58 | yield model.findAndCount(query) 59 | 60 | yield* next 61 | 62 | }, 63 | middlewares.pagination(model), 64 | middlewares.after()) 65 | 66 | router.get(`${base}/:id`, 67 | middlewares.before(), 68 | middlewares.parseQuery(model, options), 69 | function * (next) { 70 | 71 | const { 72 | response, query 73 | } = this.restql 74 | 75 | const id = +this.params.id 76 | 77 | response.body = 78 | yield model.findById(id, query) 79 | 80 | if (!response.body) { 81 | this.throw(`RestQL: ${model.name} not found`, 404) 82 | } 83 | 84 | yield* next 85 | 86 | }, 87 | middlewares.after()) 88 | 89 | } 90 | 91 | /** 92 | * load POST /user 93 | */ 94 | loaders.model.post = (router, base, model, options) => { 95 | 96 | router.post(base, 97 | middlewares.before(), 98 | middlewares.parseRequestBody(['object', 'array']), 99 | middlewares.parseQuery(model, options), 100 | middlewares.create(model), 101 | middlewares.bulkCreate(model), 102 | middlewares.after()) 103 | 104 | } 105 | 106 | /** 107 | * load PUT /user and PUT /user/:id 108 | */ 109 | loaders.model.put = (router, base, model, options) => { 110 | 111 | router.put(base, 112 | middlewares.before(), 113 | middlewares.parseRequestBody(['object', 'array']), 114 | middlewares.upsert(model), 115 | middlewares.bulkUpsert(model), 116 | middlewares.after()) 117 | 118 | router.put(`${base}/:id`, 119 | middlewares.before(), 120 | middlewares.findById(model), 121 | middlewares.parseRequestBody(['object']), 122 | function * (next) { 123 | 124 | const { 125 | request 126 | } = this.restql 127 | 128 | request.body.id = +this.params.id 129 | 130 | yield* next 131 | 132 | }, 133 | middlewares.upsert(model), 134 | middlewares.after()) 135 | 136 | } 137 | 138 | /** 139 | * load DELETE /user and DELETE /user/:id 140 | */ 141 | loaders.model.del = (router, base, model, options) => { 142 | 143 | router.del(base, 144 | middlewares.before(), 145 | middlewares.parseQuery(model, options), 146 | middlewares.destroy(model), 147 | middlewares.after()) 148 | 149 | router.del(`${base}/:id`, 150 | middlewares.before(), 151 | middlewares.findById(model), 152 | function * (next) { 153 | 154 | const { 155 | response 156 | } = this.restql 157 | 158 | const id = this.params.id 159 | yield model.destroy({ 160 | where: { id } 161 | }) 162 | 163 | response.status = 204 164 | 165 | yield* next 166 | 167 | }, 168 | middlewares.after()) 169 | 170 | } 171 | 172 | /** 173 | * load GET /gameofthrones/house/:id/seat or GET /gameofthrones/seat/:id/house 174 | */ 175 | loaders.model.association.singular.get = (router, base, model, association, options) => { 176 | 177 | const { 178 | foreignKey, as 179 | } = association 180 | 181 | const { 182 | singular 183 | } = association.options.name 184 | 185 | const get = `get${capitalizeFirstLetter(singular)}` 186 | 187 | router.get(base, 188 | middlewares.before(), 189 | middlewares.parseQuery(association.target, options), 190 | middlewares.findById(model), 191 | function * (next) { 192 | 193 | const { 194 | response, query 195 | } = this.restql 196 | 197 | const { 198 | body 199 | } = response 200 | 201 | const data = 202 | yield body[get](query) 203 | 204 | if (!data) 205 | this.throw(`RestQL: ${as} not found`, 404) 206 | 207 | response.body = data 208 | 209 | yield* next 210 | 211 | }, 212 | middlewares.after()) 213 | 214 | } 215 | 216 | /** 217 | * load PUT /gameofthrones/house/:id/seat 218 | */ 219 | loaders.model.association.singular.hasOne.put = (router, base, model, association, options) => { 220 | 221 | const { 222 | foreignKey, as 223 | } = association 224 | 225 | const query = { 226 | include: [ association ] 227 | } 228 | 229 | router.put(base, 230 | middlewares.before(), 231 | middlewares.parseRequestBody(['object']), 232 | middlewares.findById(model, query), 233 | function * (next) { 234 | 235 | const { 236 | request, response 237 | } = this.restql 238 | 239 | const { 240 | body 241 | } = response 242 | 243 | const data = _.assign({}, 244 | body[as] && body[as].dataValues, 245 | request.body) 246 | 247 | data[foreignKey] = +this.params.id 248 | 249 | request.body = data 250 | yield* next 251 | 252 | }, 253 | middlewares.upsert(association.target), 254 | middlewares.after()) 255 | 256 | } 257 | 258 | /** 259 | * load PUT /gameofthrones/seat/:id/house 260 | */ 261 | loaders.model.association.singular.belongsTo.put = (router, base, model, association, options) => { 262 | 263 | const { 264 | foreignKey, as 265 | } = association 266 | 267 | const query = { 268 | include: [ association ] 269 | } 270 | 271 | router.put(base, 272 | middlewares.before(), 273 | middlewares.parseRequestBody(['object']), 274 | middlewares.findById(model, query), 275 | function * (next) { 276 | 277 | const { 278 | request, response, params 279 | } = this.restql 280 | 281 | const { 282 | body 283 | } = response 284 | 285 | params.data = body 286 | 287 | const data = _.assign({}, 288 | body[as] && body[as].dataValues, 289 | request.body) 290 | 291 | request.body = data 292 | yield* next 293 | 294 | }, 295 | middlewares.upsert(association.target), 296 | function * (next) { 297 | 298 | const { 299 | request, response, params 300 | } = this.restql 301 | 302 | const data = response.body 303 | const value = {} 304 | 305 | value[foreignKey] = data.id 306 | yield params.data.update(value) 307 | 308 | yield* next 309 | }, 310 | middlewares.after()) 311 | 312 | } 313 | 314 | /** 315 | * load DELETE /house/:id/seat or DELETE /seat/:id/house 316 | */ 317 | loaders.model.association.singular.del = (router, base, model, association, options) => { 318 | 319 | const { 320 | foreignKey, as 321 | } = association 322 | 323 | const query = { 324 | include: [ association ] 325 | } 326 | 327 | router.del(base, 328 | middlewares.before(), 329 | middlewares.findById(model, query), 330 | function * (next) { 331 | 332 | const { 333 | request, response 334 | } = this.restql 335 | 336 | const { 337 | body 338 | } = response 339 | 340 | if (!body[as]) 341 | this.throw(`RestQL: ${model.name} ${as} not found`, 404) 342 | 343 | yield body[as].destroy() 344 | 345 | response.status = 204 346 | 347 | yield* next 348 | 349 | }, 350 | middlewares.after()) 351 | } 352 | 353 | loaders.model.association.plural.get = (router, base, model, association, options) => { 354 | 355 | const { 356 | foreignKey, as, target, through, associationType 357 | } = association 358 | 359 | const hasManyQueryGenerator = (q, id) => { 360 | 361 | const query = _.cloneDeep(q) 362 | query.where = query.where || {} 363 | 364 | if (association.scope) { 365 | _.assign(query.where, association.scope) 366 | } 367 | 368 | query.where[foreignKey] = id 369 | 370 | if (hasPluralAssociation(query.include)) { 371 | query.distinct = true 372 | } 373 | 374 | return query 375 | 376 | } 377 | 378 | const belongsToManyQueryGenerator = (q, id) => { 379 | 380 | const query = _.cloneDeep(q) 381 | 382 | let scopeWhere 383 | 384 | if (association.scope) { 385 | scopeWhere = _.clone(association.scope) 386 | } 387 | 388 | query.where = { 389 | $and: [ 390 | scopeWhere, query.where 391 | ] 392 | } 393 | 394 | if (through.model) { 395 | 396 | let throughWhere = {} 397 | throughWhere[foreignKey] = id 398 | 399 | if (through.scope) { 400 | _.assign(throughWhere, through.scope) 401 | } 402 | 403 | if (query.through && query.through.where) { 404 | throughWhere = { 405 | $and: [ 406 | throughWhere, query.through.where 407 | ] 408 | } 409 | } 410 | 411 | query.include = query.include || [] 412 | query.include.push({ 413 | association : association.oneFromTarget, 414 | attributes: query.joinTableAttributes, 415 | require : true, 416 | where : throughWhere 417 | }) 418 | 419 | } 420 | 421 | if (hasPluralAssociation(query.include)) { 422 | query.distinct = true 423 | } 424 | 425 | return query 426 | 427 | } 428 | 429 | const queryGenerators = { 430 | 'hasMany': hasManyQueryGenerator, 431 | 'belongsToMany': belongsToManyQueryGenerator 432 | } 433 | 434 | const associationTypeName = 435 | associationType.replace(/^(.)/, $1 => $1.toLowerCase()) 436 | 437 | const queryGenerator = queryGenerators[associationTypeName] 438 | 439 | router.get(base, 440 | middlewares.before(), 441 | middlewares.parseQuery(association.target, options), 442 | middlewares.findById(model), 443 | function * (next) { 444 | 445 | const { 446 | response, params, query 447 | } = this.restql 448 | 449 | const { 450 | attributes, include 451 | } = query 452 | 453 | const { 454 | body 455 | } = response 456 | 457 | const parsedQuery = queryGenerator(query, body.id) 458 | 459 | response.body = 460 | yield target.findAndCount(parsedQuery) 461 | 462 | yield* next 463 | 464 | }, 465 | middlewares.pagination(association.target), 466 | middlewares.after()) 467 | 468 | router.get(`${base}/:associationId`, 469 | middlewares.before(), 470 | middlewares.parseQuery(association.target, options), 471 | middlewares.findById(model), 472 | function * (next) { 473 | 474 | const { 475 | response, params, query 476 | } = this.restql 477 | 478 | const { 479 | body 480 | } = response 481 | 482 | query.where = query.where || {} 483 | query.where.id = +this.params.associationId 484 | 485 | const parsedQuery = queryGenerator(query, body.id) 486 | 487 | const data = 488 | yield target.findOne(parsedQuery) 489 | 490 | if (!data) 491 | this.throw(`RestQL: ${model.name} not found`, 404) 492 | 493 | response.body = data 494 | 495 | yield* next 496 | 497 | }, 498 | middlewares.after()) 499 | 500 | } 501 | 502 | /** 503 | * load POST /user/:id/tags 504 | */ 505 | loaders.model.association.plural.hasMany.post = (router, base, model, association, options) => { 506 | 507 | const { 508 | foreignKey, as 509 | } = association 510 | 511 | router.post(base, 512 | middlewares.before(), 513 | middlewares.parseRequestBody(['object', 'array']), 514 | middlewares.parseQuery(model, options), 515 | middlewares.findById(model), 516 | function * (next) { 517 | 518 | const { 519 | request, response, params 520 | } = this.restql 521 | 522 | const body = response.body 523 | 524 | common.switchByType(this.request.body, { 525 | object: (data) => { 526 | data[foreignKey] = body.id 527 | }, 528 | array: (data) => { 529 | data.forEach(row => row[foreignKey] = body.id) 530 | } 531 | }) 532 | 533 | yield* next 534 | 535 | }, 536 | middlewares.create(association.target), 537 | middlewares.bulkCreate(association.target), 538 | middlewares.after()) 539 | 540 | } 541 | 542 | /** 543 | * load POST /user/:id/characters 544 | */ 545 | loaders.model.association.plural.belongsToMany.post = (router, base, model, association, options) => { 546 | 547 | const { 548 | foreignKey, otherKey, as, through 549 | } = association 550 | 551 | const { 552 | plural 553 | } = association.options.name 554 | 555 | const get = `get${capitalizeFirstLetter(plural)}` 556 | 557 | router.post(base, 558 | middlewares.before(), 559 | middlewares.findById(model), 560 | middlewares.parseRequestBody(['object', 'array']), 561 | middlewares.parseQuery(model, options), 562 | middlewares.findOrUpsert(association.target), 563 | middlewares.bulkFindOrUpsert(association.target), 564 | function * (next) { 565 | 566 | const { 567 | request, response, params 568 | } = this.restql 569 | 570 | const data = response.body 571 | 572 | const getRequestRow = (foreignId, otherId) => { 573 | let ret = {} 574 | ret[foreignKey] = foreignId 575 | ret[otherKey] = otherId 576 | return ret 577 | } 578 | 579 | const foreignId = +this.params.id 580 | request.body = switchByType(data, { 581 | object : (data) => getRequestRow(foreignId, data.id), 582 | array : (data) => data.map(row => getRequestRow(foreignId, row.id)) 583 | }) 584 | 585 | yield* next 586 | 587 | }, 588 | middlewares.create(through.model), 589 | middlewares.bulkCreate(through.model), 590 | function * (next) { 591 | 592 | const { 593 | request, response, params 594 | } = this.restql 595 | 596 | let id = switchByType(response.body, { 597 | object : (data) => data[otherKey], 598 | array : (data) => data.map(row => row[otherKey]) 599 | }) 600 | 601 | const data = yield params.data[get]({ where: { id } }) 602 | 603 | response.body = switchByType(request.body, { 604 | object : () => data[0], 605 | array : () => data 606 | }) 607 | 608 | yield* next 609 | 610 | }, 611 | middlewares.after()) 612 | 613 | } 614 | 615 | /** 616 | * load PUT /user/:id/characters and PUT /user/:id/tags/:associationId 617 | */ 618 | loaders.model.association.plural.hasMany.put = (router, base, model, association) => { 619 | 620 | const { 621 | foreignKey 622 | } = association 623 | 624 | router.put(base, 625 | middlewares.before(), 626 | middlewares.parseRequestBody(['object', 'array']), 627 | middlewares.findById(model), 628 | function * (next) { 629 | 630 | const { 631 | request, response, params 632 | } = this.restql 633 | 634 | const id = +this.params.id 635 | 636 | request.body = switchByType(this.request.body, { 637 | object: (body) => { 638 | body[foreignKey] = id 639 | return body 640 | }, 641 | array: (body) => { 642 | return body.map(row => { 643 | row[foreignKey] = id 644 | return row 645 | }) 646 | } 647 | }) 648 | 649 | yield* next 650 | 651 | }, 652 | middlewares.upsert(association.target), 653 | middlewares.bulkUpsert(association.target), 654 | middlewares.after()) 655 | 656 | router.put(`${base}/:associationId`, 657 | middlewares.before(), 658 | middlewares.parseRequestBody(['object']), 659 | middlewares.findById(model), 660 | function * (next) { 661 | 662 | const { 663 | request, params 664 | } = this.restql 665 | 666 | const associationId = +this.params.associationId 667 | request.body.id = associationId 668 | request.body[foreignKey] = this.params.id 669 | 670 | yield* next 671 | 672 | }, 673 | middlewares.upsert(association.target), 674 | middlewares.after()) 675 | 676 | } 677 | 678 | /** 679 | * load PUT /user/:id/tags and PUT /user/:id/tags/:associationId 680 | */ 681 | loaders.model.association.plural.belongsToMany.put = (router, base, model, association, options) => { 682 | 683 | const { 684 | foreignKey, otherKey, as, through 685 | } = association 686 | 687 | const { 688 | plural 689 | } = association.options.name 690 | 691 | const get = `get${capitalizeFirstLetter(plural)}` 692 | const add = `add${capitalizeFirstLetter(plural)}` 693 | 694 | router.put(base, 695 | middlewares.before(), 696 | middlewares.findById(model), 697 | middlewares.parseRequestBody(['object', 'array']), 698 | middlewares.findOrUpsert(association.target), 699 | middlewares.bulkFindOrUpsert(association.target), 700 | function * (next) { 701 | 702 | const { 703 | request, response, params 704 | } = this.restql 705 | 706 | const data = response.body 707 | 708 | const getRequestRow = (foreignId, otherId) => { 709 | let ret = {} 710 | ret[foreignKey] = foreignId 711 | ret[otherKey] = otherId 712 | return ret 713 | } 714 | 715 | const foreignId = +this.params.id 716 | request.body = switchByType(data, { 717 | object : (data) => getRequestRow(foreignId, data.id), 718 | array : (data) => data.map(row => getRequestRow(foreignId, row.id)) 719 | }) 720 | 721 | params.status = response.status 722 | 723 | yield* next 724 | 725 | }, 726 | middlewares.upsert(through.model), 727 | middlewares.bulkUpsert(through.model), 728 | function * (next) { 729 | 730 | const { 731 | request, response, params 732 | } = this.restql 733 | 734 | let id = switchByType(response.body, { 735 | object : (data) => data[otherKey], 736 | array : (data) => data.map(row => row[otherKey]) 737 | }) 738 | 739 | const data = yield params.data[get]({ where: { id } }) 740 | 741 | response.body = switchByType(request.body, { 742 | object : () => data[0], 743 | array : () => data 744 | }) 745 | 746 | response.status = params.status 747 | yield* next 748 | 749 | }, 750 | middlewares.after()) 751 | 752 | router.put(`${base}/:associationId`, 753 | middlewares.before(), 754 | middlewares.parseRequestBody(['object']), 755 | middlewares.findById(model), 756 | function * (next) { 757 | 758 | const { 759 | request, params 760 | } = this.restql 761 | 762 | const associationId = +this.params.associationId 763 | request.body.id = associationId 764 | 765 | yield* next 766 | 767 | }, 768 | middlewares.upsert(association.target), 769 | function * (next) { 770 | 771 | const { 772 | request, response, params, query 773 | } = this.restql 774 | 775 | yield params.data[add](response.body) 776 | 777 | const data = 778 | yield params.data[get]({ 779 | where: { 780 | id: +this.params.associationId 781 | } 782 | }) 783 | 784 | response.body = data[0] 785 | 786 | yield* next 787 | 788 | }, 789 | middlewares.after()) 790 | 791 | } 792 | 793 | /** 794 | * load DELETE /user/:id/tags and DELETE /user/:id/tags/:associationId 795 | */ 796 | loaders.model.association.plural.hasMany.del = (router, base, model, association, options) => { 797 | 798 | const { 799 | foreignKey, as 800 | } = association 801 | 802 | router.del(base, 803 | middlewares.before(), 804 | middlewares.findById(model), 805 | middlewares.parseQuery(model, options), 806 | function * (next) { 807 | 808 | this.restql.query = this.restql.query || {} 809 | const where = this.restql.query.where || {} 810 | 811 | where[foreignKey] = +this.params.id 812 | this.restql.query.where = where 813 | 814 | yield* next 815 | 816 | }, 817 | middlewares.destroy(association.target), 818 | middlewares.after()) 819 | 820 | router.del(`${base}/:associationId`, 821 | middlewares.before(), 822 | middlewares.findById(model), 823 | function * (next) { 824 | 825 | this.restql.query = this.restql.query || {} 826 | const where = this.restql.query.where || {} 827 | 828 | where.id = +this.params.associationId 829 | where[foreignKey] = +this.params.id 830 | 831 | const data = 832 | yield association.target.findOne({ where }) 833 | 834 | if (!data) { 835 | this.throw(`RestQL: ${as} cannot be found`, 404) 836 | } 837 | 838 | this.restql.query.where = where 839 | 840 | yield* next 841 | 842 | }, 843 | middlewares.destroy(association.target), 844 | middlewares.after()) 845 | 846 | } 847 | 848 | /** 849 | * load DELETE /user/:id/tags and DELETE /user/:id/tags/:associationId 850 | */ 851 | loaders.model.association.plural.belongsToMany.del = (router, base, model, association, options) => { 852 | 853 | const { 854 | foreignKey, otherKey, as, through 855 | } = association 856 | 857 | const { 858 | plural 859 | } = association.options.name 860 | 861 | const get = `get${capitalizeFirstLetter(plural)}` 862 | const remove = `remove${capitalizeFirstLetter(plural)}` 863 | 864 | router.del(base, 865 | middlewares.before(), 866 | middlewares.findById(model), 867 | middlewares.parseQuery(association.target, options), 868 | function * (next) { 869 | 870 | const { 871 | request, response, params 872 | } = this.restql 873 | 874 | const query = this.restql.query 875 | const data = yield params.data[get](query) 876 | 877 | yield params.data[remove](data) 878 | 879 | response.status = 204 880 | yield* next 881 | 882 | }, 883 | middlewares.after()) 884 | 885 | router.del(`${base}/:associationId`, 886 | middlewares.before(), 887 | middlewares.findById(model), 888 | middlewares.parseQuery(association.target, options), 889 | function * (next) { 890 | 891 | const { 892 | request, response, params 893 | } = this.restql 894 | 895 | const query = this.restql.query 896 | query.where = {} 897 | query.where.id = +this.params.associationId 898 | 899 | const data = yield params.data[get](query) 900 | 901 | if (!data.length) { 902 | this.throw(`RestQL: ${as} not found`, 404) 903 | } 904 | 905 | yield params.data[remove](data) 906 | 907 | response.status = 204 908 | yield* next 909 | 910 | }, 911 | middlewares.after()) 912 | 913 | } 914 | 915 | module.exports = loaders 916 | -------------------------------------------------------------------------------- /lib/methods.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('koa-restql:methods'); 4 | 5 | module.exports = [ 6 | 'get', 'post', 'put', 'del' 7 | ]; 8 | -------------------------------------------------------------------------------- /lib/middlewares.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const co = require('co') 4 | const qs = require('qs') 5 | const _ = require('lodash') 6 | const parse = require('co-body') 7 | const debug = require('debug')('koa-restql:middlewares') 8 | 9 | const common = require('./common') 10 | 11 | const switchByType = common.switchByType 12 | 13 | function _getIndexes (model) { 14 | 15 | const { 16 | primaryKeys, options: { indexes, uniqueKeys } 17 | } = model 18 | 19 | const idxes = [] 20 | 21 | if (primaryKeys) { 22 | const keys = Object.keys(primaryKeys) 23 | if (keys.length) { 24 | idxes.push({ 25 | name : 'PRIMARY', 26 | unique : true, 27 | primary : true, 28 | fields : keys 29 | }) 30 | } 31 | } 32 | 33 | indexes.forEach(index => { 34 | idxes.push({ 35 | unique : index.unique, 36 | name : index.name, 37 | fields : index.fields 38 | }) 39 | }) 40 | 41 | Object.keys(uniqueKeys).forEach(key => { 42 | let uniqueKey = uniqueKeys[key] 43 | idxes.push({ 44 | unique : true, 45 | name : uniqueKey.name, 46 | fields : uniqueKey.fields 47 | }) 48 | }) 49 | 50 | return idxes 51 | 52 | } 53 | 54 | function _getUniqueIndexes (model) { 55 | 56 | return _getIndexes(model).filter(index => index.unique) 57 | 58 | } 59 | 60 | function _getInstanceValidIndexes (indexes, data) { 61 | 62 | if (!data) 63 | return [] 64 | 65 | return indexes.filter(index => 66 | index.fields.every(field => data[field] !== undefined)) 67 | 68 | } 69 | 70 | function _getInstanceValidIndexFields (indexes, data) { 71 | 72 | if (!data || !indexes) 73 | return 74 | 75 | const validIndexes = _getInstanceValidIndexes(indexes, data) 76 | 77 | if (!validIndexes.length) 78 | return 79 | 80 | const index = validIndexes[0] 81 | 82 | const fields = {} 83 | index.fields.forEach(field => { 84 | fields[field] = data[field] 85 | }) 86 | 87 | return fields 88 | 89 | } 90 | 91 | function * _upsert (model, data) { 92 | 93 | const uniqueIndexes = _getUniqueIndexes(model) 94 | 95 | const where = _getInstanceValidIndexFields(uniqueIndexes, data) 96 | 97 | if (!where) { 98 | this.throw('RestQL: unique index fields cannot be found', 400) 99 | } 100 | 101 | let created 102 | 103 | try { 104 | 105 | created = 106 | yield model.upsert(data) 107 | 108 | } catch (error) { 109 | 110 | if (error.name !== 'SequelizeUniqueConstraintError') { 111 | throw new Error(error) 112 | } 113 | 114 | const message = `RestQL: ${model.name} unique constraint error` 115 | this.throw(message, 409) 116 | 117 | } 118 | 119 | data = 120 | yield model.find({ 121 | where, 122 | }) 123 | 124 | if (!data) { 125 | data = 126 | yield model.find({ 127 | where, 128 | paranoid: false 129 | }) 130 | } 131 | 132 | if (isDeleted(model, data)) { 133 | yield data.restore() 134 | } 135 | 136 | return { created, data } 137 | 138 | } 139 | 140 | function * _bulkUpsert (model, data) { 141 | 142 | if (!data.length) 143 | return [] 144 | 145 | /** 146 | * updateOnDuplicate fields should be consistent 147 | */ 148 | let isValid = true 149 | 150 | if (data.length) { 151 | let match = JSON.stringify(Object.keys(data[0]).sort()) 152 | isValid = data.map(row => 153 | JSON.stringify(Object.keys(row).sort())).every(item => item === match) 154 | } 155 | 156 | if (!isValid) { 157 | this.throw('RestQL: array elements have different attributes', 400) 158 | } 159 | 160 | const $or = [] 161 | const uniqueIndexes = _getUniqueIndexes(model) 162 | 163 | data.forEach(row => { 164 | 165 | const where = _getInstanceValidIndexFields(uniqueIndexes, row) 166 | 167 | if (!where) { 168 | this.throw('RestQL: unique index fields cannot be found', 400) 169 | } 170 | 171 | $or.push(where) 172 | }) 173 | 174 | /** 175 | * ignoreDuplicates only work in mysql 176 | */ 177 | 178 | try { 179 | 180 | let updatedFields = Object.keys(data[0]).filter(key => 181 | ['id'].indexOf(key) === -1) 182 | 183 | yield model.bulkCreate(data, { 184 | updateOnDuplicate: updatedFields 185 | }) 186 | 187 | } catch (error) { 188 | 189 | if (error.name !== 'SequelizeUniqueConstraintError') { 190 | throw new Error(error) 191 | } 192 | 193 | const message = `RestQL: ${model.name} unique constraint error` 194 | this.throw(message, 409) 195 | 196 | } 197 | 198 | data = 199 | yield model.findAll({ 200 | where: { $or }, 201 | order: [['id', 'ASC']] 202 | }) 203 | 204 | return data 205 | 206 | } 207 | 208 | function _getUniqueConstraintErrorFields (model, error) { 209 | 210 | const attributes = model.attributes 211 | const fields = error.fields 212 | 213 | if (!fields) 214 | return 215 | 216 | let fieldsIsValid = Object.keys(fields).every(key => attributes[key]) 217 | 218 | if (fieldsIsValid) 219 | return fields 220 | 221 | const uniqueIndexes = _getUniqueIndexes(model) 222 | const uniqueIndex = uniqueIndexes.find(index => fields[index.name]) 223 | 224 | if (uniqueIndex && Array.isArray(uniqueIndex.fields)) { 225 | let value = fields[uniqueIndex.name] 226 | 227 | value = common.switchByType(value, { 228 | 'number' : value => [ value ], 229 | 'string' : value => value.split('-') 230 | }) 231 | 232 | if (!value || !value.length) 233 | return 234 | 235 | const ret = {} 236 | uniqueIndex.fields.forEach((field, index) => { 237 | ret[field] = value[index] 238 | }) 239 | 240 | return ret 241 | } 242 | } 243 | 244 | function isDeleted (model, row) { 245 | 246 | const attributes = model.attributes 247 | const paranoid = model.options.paranoid 248 | const deletedAtCol = model.options.deletedAt 249 | 250 | if (!paranoid || !deletedAtCol) { 251 | return false 252 | } 253 | 254 | if (!row) { 255 | return true 256 | } 257 | 258 | let defaultDeletedAt = attributes[deletedAtCol].defaultValue 259 | if (defaultDeletedAt === undefined) { 260 | defaultDeletedAt = null 261 | } 262 | 263 | const deletedAt = row[deletedAtCol] 264 | 265 | if (defaultDeletedAt instanceof Date && deletedAt instanceof Date) { 266 | return defaultDeletedAt.getTime() !== deletedAt.getTime() 267 | } else { 268 | return defaultDeletedAt !== deletedAt 269 | } 270 | 271 | } 272 | 273 | function setDefaultDeletedValue (model, data) { 274 | 275 | const attributes = model.attributes 276 | const paranoid = model.options.paranoid 277 | const deletedAtCol = model.options.deletedAt 278 | 279 | if (!paranoid || !deletedAtCol) { 280 | return 281 | } 282 | 283 | if (!data) { 284 | return 285 | } 286 | 287 | let defaultDeletedAt = attributes[deletedAtCol].defaultValue 288 | if (defaultDeletedAt === undefined) { 289 | defaultDeletedAt = null 290 | } 291 | 292 | switchByType(data, { 293 | object : (data) => data[deletedAtCol] = defaultDeletedAt, 294 | array : (data) => data.forEach(row => row[deletedAtCol] = defaultDeletedAt) 295 | }) 296 | 297 | } 298 | 299 | function * _handleUniqueConstraintError (model, error, options) { 300 | 301 | options = options || {} 302 | 303 | const message = `RestQL: ${model.name} unique constraint error` 304 | const status = 409 305 | 306 | const fields = _getUniqueConstraintErrorFields(model, error) 307 | const attributes = model.attributes 308 | const paranoid = model.options.paranoid 309 | const deletedAtCol = model.options.deletedAt 310 | const ignoreDuplicates = options.ignoreDuplicates 311 | 312 | if (!deletedAtCol || !paranoid) 313 | this.throw(message, status) 314 | 315 | let row = 316 | yield model.find({ 317 | paranoid: false, 318 | where: fields 319 | }) 320 | 321 | if (!fields || !row) { 322 | this.throw(message, status) 323 | } 324 | 325 | if (!ignoreDuplicates && !isDeleted(model, row)) { 326 | this.throw(message, status) 327 | } 328 | 329 | for (let key in attributes) { 330 | let defaultValue = attributes[key].defaultValue 331 | if (defaultValue !== undefined) { 332 | row.setDataValue(key, defaultValue) 333 | } 334 | } 335 | 336 | return { row, fields } 337 | 338 | } 339 | 340 | function * _create (model, data, options) { 341 | 342 | const id = data.id 343 | 344 | try { 345 | 346 | if (id) { 347 | delete data.id 348 | } 349 | 350 | data = 351 | yield model.create(data, options) 352 | 353 | return data 354 | 355 | } catch (error) { 356 | 357 | if (error.name !== 'SequelizeUniqueConstraintError') { 358 | throw new Error(error) 359 | } 360 | 361 | const conflict = 362 | yield* _handleUniqueConstraintError.call(this, model, error, options) 363 | 364 | const { 365 | row, fields 366 | } = conflict 367 | 368 | data = 369 | yield* _update.call(this, model, 370 | _.assign({}, row.dataValues, data), { where: fields }) 371 | 372 | data = data[0] 373 | 374 | return data 375 | 376 | } 377 | 378 | } 379 | 380 | function * _bulkCreate (model, data, options) { 381 | 382 | const $or = [] 383 | const conflicts = [] 384 | const uniqueIndexes = _getUniqueIndexes(model) 385 | 386 | data = data.slice() 387 | 388 | data.forEach(row => { 389 | 390 | const where = _getInstanceValidIndexFields(uniqueIndexes, row) 391 | 392 | if (!where) { 393 | this.throw('RestQL: unique index fields cannot be found', 400) 394 | } 395 | 396 | $or.push(where) 397 | }) 398 | 399 | while (true) { 400 | 401 | try { 402 | 403 | yield model.bulkCreate(data, options) 404 | break 405 | 406 | } catch (error) { 407 | 408 | if (error.name !== 'SequelizeUniqueConstraintError') { 409 | throw new Error(error) 410 | } 411 | 412 | const conflict = 413 | yield* _handleUniqueConstraintError.call(this, model, error) 414 | 415 | const { 416 | row, fields 417 | } = conflict 418 | 419 | const index = data.findIndex(row => 420 | Object.keys(fields).every(key => fields[key] == row[key])) 421 | 422 | if (index !== -1) { 423 | conflict.row = _.assign({}, row.dataValues, data[index]) 424 | conflicts.push(conflict) 425 | data.splice(index, 1) 426 | } else { 427 | this.throw('RestQL: bulkCreate unique index field error', 500) 428 | } 429 | 430 | } 431 | 432 | } 433 | 434 | if (conflicts.length) { 435 | const rows = conflicts.map(conflicts => conflicts.row) 436 | 437 | try { 438 | 439 | yield model.bulkCreate(rows, { 440 | updateOnDuplicate: Object.keys(model.attributes) 441 | }) 442 | 443 | } catch (error) { 444 | 445 | if (error.name !== 'SequelizeUniqueConstraintError') { 446 | throw new Error(error) 447 | } 448 | 449 | const message = `RestQL: ${model.name} unique constraint error` 450 | this.throw(message, 409) 451 | 452 | } 453 | } 454 | 455 | data = 456 | yield model.findAll({ 457 | where: { $or }, 458 | order: [['id', 'ASC']] 459 | }) 460 | 461 | return data 462 | 463 | } 464 | 465 | function * _update (model, data, options) { 466 | 467 | try { 468 | 469 | if (data.id) { 470 | delete data.id 471 | } 472 | 473 | data = 474 | yield model.update(data, options) 475 | 476 | data = 477 | yield model.findAll(options) 478 | 479 | return data 480 | 481 | } catch (error) { 482 | 483 | if (error.name !== 'SequelizeUniqueConstraintError') { 484 | throw new Error(error) 485 | } 486 | 487 | const conflict = 488 | yield* _handleUniqueConstraintError.call(this, model, error) 489 | 490 | const { row } = conflict 491 | 492 | /** 493 | * @FIXME 494 | * restql should delete the conflict with paranoid = false 495 | * and update again, now return 409 directly 496 | * for conflict happens rarely 497 | */ 498 | const message = `RestQL: ${model.name} unique constraint error` 499 | this.throw(message, 409) 500 | 501 | } 502 | 503 | } 504 | 505 | function * _findExistingRows (model, data) { 506 | 507 | const $or = [] 508 | const uniqueIndexes = _getUniqueIndexes(model) 509 | 510 | function getOr (uniqueIndexes, data) { 511 | 512 | let fields = _getInstanceValidIndexFields(uniqueIndexes, data) 513 | , row = data 514 | 515 | return { fields, row } 516 | 517 | } 518 | 519 | common.switchByType(data, { 520 | object : (data) => $or.push(getOr(uniqueIndexes, data)), 521 | array : (data) => data.forEach(row => $or.push(getOr(uniqueIndexes, row))) 522 | }) 523 | 524 | data = 525 | yield model.findAll({ 526 | where: { $or : $or.map(or => or.fields) } 527 | }) 528 | 529 | let existingRows = [] 530 | let newRows = [] 531 | 532 | if (data.length === $or.length) { 533 | 534 | existingRows = data 535 | 536 | } else { 537 | 538 | /* 539 | * find existing rows 540 | */ 541 | $or.forEach(or => { 542 | 543 | let index = data.findIndex(row => 544 | Object.keys(or.fields).every(key => row[key] === or.row[key])) 545 | 546 | if (index !== -1) { 547 | existingRows.push(data[index]) 548 | data.splice(index, 1) 549 | } else { 550 | newRows.push(or.row) 551 | } 552 | 553 | }) 554 | 555 | } 556 | 557 | return { existingRows, newRows } 558 | 559 | } 560 | 561 | function before () { 562 | return function * (next) { 563 | 564 | debug(`RestQL: ${this.request.method} ${this.url}`) 565 | 566 | this.restql = this.restql || {} 567 | this.restql.params = this.restql.params || {} 568 | this.restql.request = this.restql.request || {} 569 | this.restql.response = this.restql.response || {} 570 | 571 | yield* next 572 | 573 | } 574 | } 575 | 576 | function after () { 577 | return function * (next) { 578 | 579 | const { 580 | response 581 | } = this.restql 582 | 583 | this.response.status = response.status || 200 584 | this.response.body = response.body 585 | 586 | const headers = response.headers || {} 587 | 588 | for (let key in headers) { 589 | this.response.set(key, response.headers[key]) 590 | } 591 | 592 | debug(`RestQL: Succeed and Goodbye`) 593 | 594 | yield* next 595 | 596 | } 597 | } 598 | 599 | function parseQuery (model, options) { 600 | return function * (next) { 601 | 602 | const { 603 | method, querystring 604 | } = this.request 605 | 606 | const query = this.restql.query || qs.parse(querystring, options.qs || {}) 607 | 608 | this.restql.query = 609 | common.parseQuery(query, model, method.toLowerCase(), options) 610 | 611 | yield* next 612 | 613 | } 614 | } 615 | 616 | function findById (model, query) { 617 | return function * (next) { 618 | 619 | query = query || {} 620 | 621 | const id = this.params.id 622 | 623 | if (!id) { 624 | return yield* next 625 | } 626 | 627 | const data = 628 | yield model.findById(id, query) 629 | 630 | if (!data) { 631 | this.throw(`RestQL: ${model.name} ${id} cannot be found`, 404) 632 | } 633 | 634 | this.restql.params.data = data 635 | this.restql.response.body = data 636 | 637 | yield* next 638 | 639 | } 640 | } 641 | 642 | function pagination (model) { 643 | return function * (next) { 644 | 645 | const { 646 | response, params, query 647 | } = this.restql 648 | 649 | const { 650 | count, rows 651 | } = response.body 652 | 653 | const { 654 | offset, limit 655 | } = query 656 | 657 | let status = 200 658 | 659 | const _count = switchByType(count, { 660 | 'number' : (value) => value, 661 | 'array' : (value) => value.length 662 | }) 663 | 664 | const xRangeHeader = `objects ${offset}-${offset + rows.length}/${_count}` 665 | 666 | if (_count > limit) 667 | status = 206 668 | 669 | response.headers = response.headers || {} 670 | response.headers['X-Range'] = xRangeHeader 671 | response.body = rows 672 | response.status = status 673 | 674 | yield* next 675 | 676 | } 677 | } 678 | 679 | function upsert (model) { 680 | return function * (next) { 681 | 682 | const { 683 | request, response 684 | } = this.restql 685 | 686 | const { 687 | body 688 | } = request 689 | 690 | let status = 200 691 | 692 | if (Array.isArray(body)) { 693 | return yield* next 694 | } 695 | 696 | setDefaultDeletedValue(model, body) 697 | const uniqueIndexes = _getUniqueIndexes(model) 698 | const where = _getInstanceValidIndexFields(uniqueIndexes, body) 699 | 700 | let data = null 701 | , created = false 702 | 703 | if (where) { 704 | 705 | const result = 706 | yield* _upsert.call(this, model, body) 707 | 708 | created = result.created 709 | data = result.data 710 | 711 | } else { 712 | 713 | created = true 714 | 715 | /// don't have include 716 | data = 717 | yield* _create.call(this, model, body, { 718 | fields: Object.keys(model.attributes) 719 | }) 720 | 721 | } 722 | 723 | if (created) 724 | status = 201 725 | 726 | response.body = data 727 | response.status = status 728 | 729 | yield* next 730 | 731 | } 732 | } 733 | 734 | function findOrUpsert (model) { 735 | return function * (next) { 736 | 737 | const { 738 | request, response 739 | } = this.restql 740 | 741 | let status = 200 742 | 743 | if (Array.isArray(request.body)) { 744 | return yield* next 745 | } 746 | 747 | const { 748 | existingRows, newRows 749 | } = yield* _findExistingRows.call(this, model, [ request.body ]) 750 | 751 | let data 752 | 753 | if (newRows.length){ 754 | status = 201 755 | let ret = 756 | yield* _upsert.call(this, model, newRows[0]) 757 | 758 | if (ret.created) 759 | status = 201 760 | 761 | data = ret.data 762 | } else { 763 | data = existingRows[0] 764 | } 765 | 766 | response.body = data 767 | response.status = status 768 | 769 | yield* next 770 | 771 | } 772 | } 773 | 774 | 775 | function bulkUpsert (model) { 776 | return function * (next) { 777 | 778 | const { 779 | request, response 780 | } = this.restql 781 | 782 | const body = request.body 783 | const status = 200 784 | 785 | if (!Array.isArray(body)) { 786 | return yield* next 787 | } 788 | 789 | setDefaultDeletedValue(model, body) 790 | 791 | const data = 792 | yield* _bulkUpsert.call(this, model, body) 793 | 794 | response.body = data 795 | response.status = status 796 | 797 | yield* next 798 | 799 | } 800 | } 801 | 802 | function bulkFindOrUpsert (model) { 803 | return function * (next) { 804 | 805 | const { 806 | request, response 807 | } = this.restql 808 | 809 | const status = 200 810 | 811 | if (!Array.isArray(request.body)) { 812 | return yield* next 813 | } 814 | 815 | const { 816 | existingRows, newRows 817 | } = yield* _findExistingRows.call(this, model, request.body) 818 | 819 | let data = [] 820 | 821 | if (newRows.length){ 822 | data = 823 | yield* _bulkUpsert.call(this, model, newRows) 824 | } 825 | 826 | data.forEach(row => existingRows.push(row)) 827 | 828 | response.body = existingRows 829 | response.status = status 830 | 831 | yield* next 832 | 833 | } 834 | } 835 | 836 | function create (model) { 837 | return function * (next) { 838 | 839 | const { 840 | request, response, query 841 | } = this.restql 842 | 843 | const body = request.body 844 | const status = 201 845 | 846 | if (Array.isArray(body)) { 847 | return yield* next 848 | } 849 | 850 | const include = [] 851 | const associations = model.associations 852 | const associationList = Object.keys(associations) 853 | 854 | for (let key in body) { 855 | 856 | let value = body[key] 857 | if ('object' === typeof value) { 858 | if (associationList.indexOf(key) !== -1) { 859 | include.push(associations[key]) 860 | } 861 | } 862 | 863 | } 864 | 865 | setDefaultDeletedValue(model, body) 866 | const data = 867 | yield* _create.call(this, model, body, { 868 | ignoreDuplicates: query.ignoreDuplicates, 869 | include 870 | }) 871 | 872 | response.body = data 873 | response.status = status 874 | 875 | return yield* next 876 | 877 | } 878 | } 879 | 880 | function bulkCreate (model) { 881 | return function * (next) { 882 | 883 | const { 884 | request, response 885 | } = this.restql 886 | 887 | const body = request.body 888 | const status = 201 889 | 890 | if (!Array.isArray(body)) { 891 | return yield* next 892 | } 893 | 894 | setDefaultDeletedValue(model, body) 895 | const data = 896 | yield* _bulkCreate.call(this, model, body) 897 | 898 | response.body = data 899 | response.status = status 900 | 901 | yield* next 902 | } 903 | } 904 | 905 | function parseRequestBody (allowedTypes) { 906 | return function * (next) { 907 | 908 | const body = this.request.body 909 | || this.restql.request.body 910 | || (yield parse(this)) 911 | 912 | this.restql.request.body = this.request.body = body 913 | 914 | if (!allowedTypes) { 915 | return yield* next 916 | } 917 | 918 | const validators = {} 919 | allowedTypes.forEach(type => { 920 | validators[type] = true 921 | }) 922 | 923 | validators.defaults = () => { 924 | this.throw(`RestQL: ${allowedTypes.join()} body are supported`, 400) 925 | } 926 | 927 | common.switchByType(body, validators) 928 | 929 | yield* next 930 | 931 | } 932 | } 933 | 934 | function destroy (model) { 935 | return function * (next) { 936 | 937 | const query = this.restql.query || {} 938 | const where = query.where || {} 939 | const status = 204 940 | 941 | yield model.destroy({ 942 | where 943 | }) 944 | 945 | this.restql.response.status = status 946 | yield* next 947 | 948 | } 949 | } 950 | 951 | module.exports.before = before 952 | module.exports.after = after 953 | module.exports.pagination = pagination 954 | module.exports.parseRequestBody = parseRequestBody 955 | module.exports.parseQuery = parseQuery 956 | module.exports.upsert = upsert 957 | module.exports.bulkUpsert = bulkUpsert 958 | module.exports.findOrUpsert = findOrUpsert 959 | module.exports.bulkFindOrUpsert = bulkFindOrUpsert 960 | module.exports.create = create 961 | module.exports.bulkCreate = bulkCreate 962 | module.exports.destroy = destroy 963 | module.exports.findById = findById 964 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Router = require('koa-router'); 4 | const debug = require('debug')('koa-restql:router'); 5 | 6 | const common = require('./common'); 7 | const methods = require('./methods'); 8 | const loaders = require('./loaders'); 9 | 10 | const switchByType = common.switchByType 11 | 12 | function loadModelRoutes (router, method, model, name, options) { 13 | 14 | let base = `/${name}` 15 | , associations = model.associations 16 | , schema = model.options.schema; 17 | 18 | if (schema) { 19 | base = `/${schema}${base}`; 20 | } 21 | 22 | let loader = loaders.model[method]; 23 | if (loader) { 24 | loader(router, base, model, options) 25 | } 26 | 27 | Object.keys(associations).forEach(key => { 28 | 29 | const association = associations[key] 30 | 31 | const { 32 | isSingleAssociation, associationType 33 | } = association 34 | 35 | if (common.shouldIgnoreAssociation(method, association.options.restql)) 36 | return 37 | 38 | let loaderPath = loaders.model.association 39 | 40 | const associationTypeName = 41 | associationType.replace(/^(.)/, $1 => $1.toLowerCase()) 42 | 43 | loaderPath = isSingleAssociation ? loaderPath.singular : loaderPath.plural 44 | 45 | let loader = (loaderPath[associationTypeName] && 46 | loaderPath[associationTypeName][method]) || loaderPath[method] 47 | 48 | if (loader) { 49 | loader(router, `${base}/:id/${key}`, model, association, options) 50 | } 51 | }) 52 | 53 | } 54 | 55 | function load (models, options) { 56 | 57 | let router = new Router(); 58 | 59 | Object.keys(models).forEach(key => { 60 | 61 | let model = models[key]; 62 | 63 | methods.forEach(method => { 64 | loadModelRoutes(router, method.toLowerCase(), model, key, options); 65 | }) 66 | }) 67 | 68 | return router; 69 | } 70 | 71 | module.exports.load = load; 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-restql", 3 | "version": "1.0.8", 4 | "description": "Koa RESTful API middleware based on Sequlizejs", 5 | "main": "lib/RestQL.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "scripts": { 10 | "test": "export NODE_ENV=test; export DEBUG=; ./node_modules/.bin/mocha --reporter spec --harmony --bail --no-timeouts" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "ssh://git@git.sankuai.com/mit/koa-restql.git" 15 | }, 16 | "dependencies": { 17 | "koa-router": "^5.4.0", 18 | "co-body": "^4.0.0", 19 | "lodash": "^4.15.0", 20 | "qs": "^6.2.0", 21 | "debug": "^2.2.0" 22 | }, 23 | "devDependencies": { 24 | "koa": "^1.2.4", 25 | "mysql": "^2.10.2", 26 | "node-uuid": "^1.4.7", 27 | "sequelize": "^3.24.0", 28 | "glob": "^7.0.3", 29 | "mocha": "^2.3.4", 30 | "supertest": "^1.2.0" 31 | }, 32 | "keywords": [ 33 | "koa", 34 | "middleware", 35 | "sequlizejs", 36 | "restful" 37 | ], 38 | "engines": { 39 | "node": ">5.0.0" 40 | }, 41 | "author": "Dale ", 42 | "license": "ISC" 43 | } 44 | -------------------------------------------------------------------------------- /test/belongsTo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:associations') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const models = prepare.sequelize.models 16 | 17 | describe ('model belongsTo association routers', function () { 18 | 19 | let server 20 | 21 | before (function () { 22 | 23 | let app = koa() 24 | , restql = new RestQL(models) 25 | 26 | app.use(restql.routes()) 27 | server = request(http.createServer(app.callback())) 28 | 29 | }) 30 | 31 | beforeEach (function (done) { 32 | 33 | debug('reset db') 34 | prepare.loadMockData().then(() => { 35 | done() 36 | }).catch(done) 37 | 38 | }) 39 | 40 | const model = models.seat 41 | const association = models.house 42 | 43 | describe ('GET', function () { 44 | 45 | it ('should return 200 | get /seat/:id/house', function (done) { 46 | 47 | const id = 3 48 | 49 | model.findById(id).then(data => { 50 | 51 | server 52 | .get(`/gameofthrones/seat/${id}/house`) 53 | .expect(200) 54 | .end((err, res) => { 55 | 56 | if (err) return done(err) 57 | let body = res.body 58 | assert('object' === typeof body) 59 | assert(body.id === data.house_id) 60 | done() 61 | 62 | }) 63 | 64 | }).catch(done) 65 | 66 | }) 67 | 68 | it ('should return 404 | get /seat/:id/house', function (done) { 69 | 70 | const id = 3 71 | 72 | model.findById(id).then(seat => { 73 | 74 | return seat.getHouse().then(house => { 75 | return house.destroy().then(() => { 76 | return { seat, house } 77 | }) 78 | }) 79 | 80 | }).then(res => { 81 | 82 | server 83 | .get(`/gameofthrones/seat/${id}/house`) 84 | .expect(404) 85 | .end(done) 86 | 87 | }).catch(done) 88 | 89 | }) 90 | 91 | }) 92 | 93 | describe ('PUT', function () { 94 | 95 | it ('should return 200 | put /seat/:id/house', function (done) { 96 | 97 | const id = 3 98 | const data = { 99 | name: uuid() 100 | } 101 | 102 | model.findById(id).then(seat => { 103 | 104 | server 105 | .put(`/gameofthrones/seat/${id}/house`) 106 | .send(data) 107 | .expect(200) 108 | .end((err, res) => { 109 | 110 | if (err) return done(err) 111 | let body = res.body 112 | assert('object' === typeof body) 113 | debug(body) 114 | assert(body.id === seat.house_id) 115 | test.assertObject(body, data) 116 | test.assertModelById(association, seat.house_id, data, done) 117 | 118 | }) 119 | 120 | }).catch(done) 121 | 122 | }) 123 | 124 | it ('should return 201 | put /seat/:id/house', function (done) { 125 | 126 | const id = 2 127 | const data = { 128 | name: uuid() 129 | } 130 | 131 | model.findById(id).then(seat => { 132 | return association.destroy({ 133 | where: { 134 | id: seat.house_id 135 | } 136 | }).then((row) => { 137 | assert(row) 138 | return seat 139 | }) 140 | }).then(seat => { 141 | 142 | server 143 | .put(`/gameofthrones/seat/${id}/house`) 144 | .send(data) 145 | .expect(201) 146 | .end((err, res) => { 147 | 148 | if (err) return done(err) 149 | let body = res.body 150 | assert('object' === typeof body) 151 | debug(body) 152 | 153 | model.findById(id).then(seat => { 154 | assert(body.id === seat.house_id) 155 | test.assertObject(body, data) 156 | test.assertModelById(association, seat.house_id, data, done) 157 | }) 158 | 159 | }) 160 | 161 | }).catch(done) 162 | 163 | }) 164 | 165 | 166 | }) 167 | 168 | describe ('DELETE', function () { 169 | 170 | it ('should return 204 | delete /seat/:id/house', function (done) { 171 | 172 | const id = 2 173 | 174 | model.findById(id).then(seat => { 175 | return association.findById(seat.house_id).then(house => { 176 | assert(house) 177 | return seat 178 | }) 179 | }).then(seat => { 180 | 181 | server 182 | .del(`/gameofthrones/seat/${id}/house`) 183 | .expect(204) 184 | .end((err, res) => { 185 | 186 | association.findById(seat.house_id).then(data => { 187 | assert(!data) 188 | done() 189 | }) 190 | 191 | }) 192 | 193 | }).catch(done) 194 | 195 | }) 196 | 197 | it ('should return 404 | delete /seat/:id/house', function (done) { 198 | 199 | const id = 2 200 | 201 | model.findById(id).then(seat => { 202 | 203 | return seat.getHouse().then(house => { 204 | return house.destroy().then(() => { 205 | return { seat, house } 206 | }) 207 | }) 208 | 209 | }).then(res => { 210 | 211 | const { 212 | seat, house 213 | } = res 214 | 215 | server 216 | .del(`/gameofthrones/seat/${seat.id}/house`) 217 | .expect(404) 218 | .end(done) 219 | 220 | }).catch(done) 221 | 222 | }) 223 | 224 | }) 225 | 226 | }) 227 | -------------------------------------------------------------------------------- /test/belongsToMany.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:associations') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const models = prepare.sequelize.models 16 | 17 | describe ('model belongsToMany association routers', function () { 18 | 19 | let server 20 | 21 | before (function () { 22 | 23 | let app = koa() 24 | , restql = new RestQL(models) 25 | 26 | app.use(restql.routes()) 27 | server = request(http.createServer(app.callback())) 28 | }) 29 | 30 | beforeEach (function (done) { 31 | 32 | debug('reset db') 33 | prepare.loadMockData().then(() => { 34 | done() 35 | }).catch(done) 36 | 37 | }) 38 | 39 | const model = models.user 40 | const through = model.associations.characters.through.model 41 | const association = models.character 42 | 43 | describe ('GET', function () { 44 | 45 | it ('should return 200 | get /user/:id/characters', function (done) { 46 | 47 | const id = 1 48 | 49 | model.findById(id).then(user => { 50 | 51 | user.getCharacters().then(characters => { 52 | 53 | server 54 | .get(`/user/${id}/characters`) 55 | .expect(200) 56 | .end((err, res) => { 57 | 58 | if (err) return done(err) 59 | let body = res.body 60 | assert(Array.isArray(body)) 61 | debug(body) 62 | 63 | assert(body.length === characters.length) 64 | done() 65 | 66 | }) 67 | 68 | }) 69 | 70 | }).catch(done) 71 | 72 | }) 73 | 74 | it ('should return 200 | get /user/:id/partialities', function (done) { 75 | 76 | const id = 1 77 | 78 | model.findById(id).then(user => { 79 | 80 | user.getPartialities().then(partialities => { 81 | 82 | server 83 | .get(`/user/${id}/partialities`) 84 | .expect(200) 85 | .end((err, res) => { 86 | 87 | if (err) return done(err) 88 | let body = res.body 89 | assert(Array.isArray(body)) 90 | debug(body) 91 | 92 | assert(body.length === partialities.length) 93 | body.forEach(user => { 94 | assert(user.user_characters) 95 | assert(user.user_characters.rate > 0) 96 | }) 97 | 98 | done() 99 | 100 | }) 101 | 102 | }) 103 | 104 | }).catch(done) 105 | 106 | }) 107 | 108 | it ('should return 200 | get /user/:id/pests', function (done) { 109 | 110 | const id = 1 111 | 112 | model.findById(id).then(user => { 113 | 114 | user.getPests().then(pests => { 115 | 116 | server 117 | .get(`/user/${id}/pests`) 118 | .expect(200) 119 | .end((err, res) => { 120 | 121 | if (err) return done(err) 122 | let body = res.body 123 | assert(Array.isArray(body)) 124 | debug(body) 125 | assert(body.length === pests.length) 126 | 127 | body.forEach(user => { 128 | assert(user.user_characters) 129 | assert(user.user_characters.rate < 0) 130 | }) 131 | 132 | done() 133 | 134 | }) 135 | 136 | }) 137 | 138 | }).catch(done) 139 | 140 | }) 141 | 142 | it('should return 404 | get /user/:id/characters', function (done) { 143 | 144 | const id = 100 145 | 146 | server 147 | .get(`/user/${id}/characters`) 148 | .expect(404) 149 | .end(done) 150 | 151 | }) 152 | 153 | it ('should return 200 | get /user/:id/characters/:associationId ', function (done) { 154 | 155 | const id = 1 156 | 157 | model.findById(id).then(user => { 158 | 159 | user.getCharacters().then(characters => { 160 | 161 | let character = characters[0] 162 | character = character.dataValues 163 | 164 | test.deleteObjcetTimestamps(character) 165 | delete character.user_characters 166 | 167 | debug(character) 168 | 169 | server 170 | .get(`/user/${id}/characters/${character.id}`) 171 | .expect(200) 172 | .end((err, res) => { 173 | 174 | if (err) return done(err) 175 | let body = res.body 176 | assert('object' === typeof body) 177 | debug(body) 178 | assert(body.user_characters) 179 | test.assertObject(body, character) 180 | test.assertModelById(association, body.id, character, done) 181 | 182 | }) 183 | 184 | }) 185 | 186 | }).catch(done) 187 | 188 | }) 189 | 190 | it ('should return 404 | get /user/:id/characters/:associationId, wrong id', function (done) { 191 | 192 | const id = 100 193 | 194 | server 195 | .get(`/user/${id}/characters/1`) 196 | .expect(404) 197 | .end(done) 198 | 199 | }) 200 | 201 | it ('should return 404 | get /user/:id/characters/:associationId, wrong associationId', function (done) { 202 | 203 | const id = 1 204 | 205 | model.findById(id).then(user => { 206 | 207 | assert(user) 208 | 209 | server 210 | .get(`/user/${id}/characters/100`) 211 | .expect(404) 212 | .end(done) 213 | 214 | }).catch(done) 215 | 216 | }) 217 | 218 | }) 219 | 220 | describe ('POST', function () { 221 | 222 | it ('should return 201 | post /user/:id/characters, object body', function (done) { 223 | 224 | const id = 1 225 | const associationId = 2 226 | 227 | association.findById(associationId).then(character => { 228 | 229 | assert(character) 230 | return character 231 | 232 | }).then(character => { 233 | 234 | character = character.dataValues 235 | test.deleteObjcetTimestamps(character) 236 | 237 | server 238 | .post(`/user/${id}/characters`) 239 | .send(character) 240 | .expect(201) 241 | .end((err, res) => { 242 | 243 | if (err) return done(err) 244 | let body = res.body 245 | assert('object' === typeof body) 246 | debug(body) 247 | assert(body.id) 248 | assert(body.user_characters) 249 | test.assertObject(body, character) 250 | test.assertModelById(association, body.id, character, done) 251 | 252 | }) 253 | 254 | }).catch(done) 255 | 256 | }) 257 | 258 | it ('should return 201 | post /user/:id/characters, array body', function (done) { 259 | 260 | const id = 1 261 | 262 | model.findById(id).then(user => { 263 | 264 | assert(user) 265 | 266 | const characters = [] 267 | 268 | /* exist character */ 269 | characters.push({ 270 | name: 'Arya' 271 | }) 272 | 273 | characters.push({ 274 | name: 'Sansa' 275 | }) 276 | 277 | server 278 | .post(`/user/${id}/characters`) 279 | .send(characters) 280 | .expect(201) 281 | .end((err, res) => { 282 | 283 | if (err) return done(err) 284 | let body = res.body 285 | assert(Array.isArray(body)) 286 | debug(body) 287 | 288 | assert(body.length === characters.length) 289 | 290 | let promises = body.map((character, index) => { 291 | assert(character.id) 292 | test.assertObject(character, characters[index]) 293 | test.assertModelById(association, character.id, characters[index]) 294 | }) 295 | 296 | Promise.all(promises).then(() => done()) 297 | 298 | }) 299 | 300 | }).catch(done) 301 | 302 | }) 303 | 304 | it ('should return 404 | post /user/:id/characters', function (done) { 305 | 306 | const id = 100 307 | 308 | server 309 | .post(`/user/${id}/characters`) 310 | .expect(404) 311 | .end(done) 312 | 313 | }) 314 | 315 | describe ('unique key constraint error', function () { 316 | 317 | it ('should return 409 | post /user/:id/characters, object body', function (done) { 318 | 319 | const id = 1 320 | 321 | model.findById(id).then(user => { 322 | 323 | assert(user) 324 | 325 | user.getCharacters().then(characters => { 326 | 327 | assert(characters.length) 328 | 329 | let character = characters[0].dataValues 330 | test.deleteObjcetTimestamps(character) 331 | delete character.user_characters 332 | 333 | server 334 | .post(`/user/${id}/characters`) 335 | .send(character) 336 | .expect(409) 337 | .end(done) 338 | 339 | }) 340 | 341 | }).catch(done) 342 | 343 | }) 344 | 345 | it ('should return 409 | post /user/:id/characters, array body', function (done) { 346 | 347 | const id = 1 348 | 349 | model.findById(id).then(user => { 350 | 351 | assert(user) 352 | 353 | user.getCharacters().then(characters => { 354 | 355 | characters = characters.map(character => { 356 | character = character.dataValues 357 | test.deleteObjcetTimestamps(character) 358 | delete character.user_characters 359 | return character 360 | }) 361 | 362 | server 363 | .post(`/user/${id}/characters`) 364 | .send(characters) 365 | .expect(409) 366 | .end(done) 367 | 368 | }) 369 | 370 | }).catch(done) 371 | 372 | }) 373 | 374 | it ('should return 201 | post /user/:id/characters, object body', function (done) { 375 | 376 | const id = 1 377 | 378 | model.findById(id).then(user => { 379 | assert(user) 380 | return user.getCharacters().then(characters => { 381 | return user.setCharacters([]).then(() => { 382 | return user.getCharacters() 383 | }).then(data => { 384 | assert(!data.length) 385 | return characters 386 | }) 387 | }) 388 | }).then(characters => { 389 | 390 | const character = characters[0].dataValues 391 | test.deleteObjcetTimestamps(character) 392 | delete character.user_characters 393 | 394 | server 395 | .post(`/user/${id}/characters`) 396 | .send(character) 397 | .expect(201) 398 | .end((err, res) => { 399 | 400 | if (err) return done(err) 401 | let body = res.body 402 | assert('object' === typeof body) 403 | debug(body) 404 | assert(body.user_characters) 405 | /* test default value */ 406 | assert(body.user_characters.rate === 0) 407 | test.assertObject(body, character) 408 | test.assertModelById(association, body.id, character, done) 409 | 410 | }) 411 | 412 | }).catch(done) 413 | 414 | }) 415 | 416 | it ('should return 201 | post /user/:id/characters, array body', function (done) { 417 | 418 | const id = 1 419 | 420 | model.findById(id).then(user => { 421 | assert(user) 422 | return user.getCharacters().then(characters => { 423 | return user.setCharacters([]).then(() => { 424 | return user.getCharacters() 425 | }).then(data => { 426 | assert(!data.length) 427 | return characters 428 | }) 429 | }) 430 | }).then(characters => { 431 | 432 | characters = characters.map(character => { 433 | 434 | character = character.dataValues 435 | test.deleteObjcetTimestamps(character) 436 | delete character.user_characters 437 | 438 | return character 439 | }) 440 | 441 | characters.push({ 442 | name: 'Sansa' 443 | }) 444 | 445 | debug(characters) 446 | 447 | server 448 | .post(`/user/${id}/characters`) 449 | .send(characters) 450 | .expect(201) 451 | .end((err, res) => { 452 | 453 | if (err) return done(err) 454 | let body = res.body 455 | assert(Array.isArray(body)) 456 | debug(body) 457 | 458 | let promises = body.map((character, index) => { 459 | assert(character.id) 460 | test.assertObject(character, characters[index]) 461 | test.assertModelById(association, character.id, characters[index]) 462 | }) 463 | 464 | Promise.all(promises).then(() => done()) 465 | }) 466 | 467 | }).catch(done) 468 | 469 | }) 470 | 471 | }) 472 | 473 | }) 474 | 475 | describe ('PUT', function () { 476 | 477 | it ('should return 200 | put /user/:id/characters, object body', function (done) { 478 | 479 | const id = 1 480 | const associationId = 2 481 | 482 | association.findById(associationId).then(character => { 483 | 484 | assert(character) 485 | return character 486 | 487 | }).then(character => { 488 | 489 | character = character.dataValues 490 | test.deleteObjcetTimestamps(character) 491 | 492 | server 493 | .put(`/user/${id}/characters`) 494 | .send(character) 495 | .expect(200) 496 | .end((err, res) => { 497 | 498 | if (err) return done(err) 499 | let body = res.body 500 | assert('object' === typeof body) 501 | debug(body) 502 | assert(body.id) 503 | assert(body.user_characters) 504 | test.assertObject(body, character) 505 | test.assertModelById(association, body.id, character, done) 506 | 507 | }) 508 | 509 | }).catch(done) 510 | 511 | }) 512 | 513 | it ('should return 201 | put /user/:id/characters, object body', function (done) { 514 | 515 | const id = 1 516 | const associationId = 2 517 | 518 | model.findById(id).then(user => { 519 | 520 | assert(user) 521 | 522 | const character = { 523 | name: 'Sansa' 524 | } 525 | 526 | server 527 | .put(`/user/${id}/characters`) 528 | .send(character) 529 | .expect(201) 530 | .end((err, res) => { 531 | 532 | if (err) return done(err) 533 | let body = res.body 534 | assert('object' === typeof body) 535 | debug(body) 536 | assert(body.id) 537 | assert(body.user_characters) 538 | test.assertObject(body, character) 539 | test.assertModelById(association, body.id, character, done) 540 | 541 | }) 542 | 543 | }).catch(done) 544 | 545 | }) 546 | 547 | it ('should return 200 | put /user/:id/characters, array body', function (done) { 548 | 549 | const id = 1 550 | 551 | model.findById(id).then(user => { 552 | 553 | assert(user) 554 | 555 | const characters = [] 556 | 557 | /* exist character */ 558 | characters.push({ 559 | name: 'Arya' 560 | }) 561 | 562 | characters.push({ 563 | name: 'Sansa' 564 | }) 565 | 566 | server 567 | .put(`/user/${id}/characters`) 568 | .send(characters) 569 | .expect(200) 570 | .end((err, res) => { 571 | 572 | if (err) return done(err) 573 | let body = res.body 574 | assert(Array.isArray(body)) 575 | debug(body) 576 | 577 | assert(body.length === characters.length) 578 | 579 | let promises = body.map((character, index) => { 580 | assert(character.id) 581 | test.assertObject(character, characters[index]) 582 | test.assertModelById(association, character.id, characters[index]) 583 | }) 584 | 585 | Promise.all(promises).then(() => done()) 586 | 587 | }) 588 | 589 | }).catch(done) 590 | 591 | }) 592 | 593 | it ('should return 200 | put /user/:id/characters, object body, update', function (done) { 594 | 595 | const id = 1 596 | 597 | model.findById(id).then(user => { 598 | 599 | assert(user) 600 | 601 | user.getCharacters().then(characters => { 602 | 603 | assert(characters.length) 604 | 605 | let character = characters[0].dataValues 606 | test.deleteObjcetTimestamps(character) 607 | 608 | const userCharatersId = character.user_characters.id 609 | delete character.user_characters 610 | 611 | server 612 | .put(`/user/${id}/characters`) 613 | .send(character) 614 | .expect(200) 615 | .end((err, res) => { 616 | 617 | if (err) return done(err) 618 | let body = res.body 619 | assert('object' === typeof body) 620 | debug(body) 621 | assert(body.id) 622 | assert(body.user_characters) 623 | assert(body.user_characters.id === userCharatersId) 624 | test.assertObject(body, character) 625 | test.assertModelById(association, body.id, character, done) 626 | 627 | }) 628 | 629 | }) 630 | 631 | }).catch(done) 632 | 633 | }) 634 | 635 | it ('should return 200 | put /user/:id/characters, array body, update', function (done) { 636 | 637 | const id = 1 638 | 639 | model.findById(id).then(user => { 640 | 641 | assert(user) 642 | 643 | user.getCharacters().then(characters => { 644 | 645 | characters = characters.map(character => { 646 | character = character.dataValues 647 | character.userCharactersId = character.user_characters.id 648 | test.deleteObjcetTimestamps(character) 649 | delete character.user_characters 650 | return character 651 | }) 652 | 653 | server 654 | .put(`/user/${id}/characters`) 655 | .send(characters) 656 | .expect(200) 657 | .end((err, res) => { 658 | 659 | if (err) return done(err) 660 | let body = res.body 661 | assert(Array.isArray(body)) 662 | debug(body) 663 | 664 | assert(body.length === characters.length) 665 | 666 | let promises = body.map((character, index) => { 667 | assert(character.id) 668 | assert(character.user_characters) 669 | assert(character.user_characters.id === characters[index].userCharactersId) 670 | delete characters[index].userCharactersId 671 | test.assertObject(character, characters[index]) 672 | test.assertModelById(association, character.id, characters[index]) 673 | }) 674 | 675 | Promise.all(promises).then(() => done()) 676 | 677 | }) 678 | 679 | }) 680 | 681 | }).catch(done) 682 | 683 | }) 684 | 685 | it ('should return 404 | put /user/:id/characters', function (done) { 686 | 687 | const id = 100 688 | 689 | server 690 | .put(`/user/${id}/characters`) 691 | .expect(404) 692 | .end(done) 693 | 694 | }) 695 | 696 | it ('should return 200 | put /user/:id/characters/:associationId, update character', function (done) { 697 | 698 | const id = 1 699 | 700 | model.findById(id).then(user => { 701 | 702 | assert(user) 703 | 704 | user.getCharacters().then(characters => { 705 | 706 | assert(characters.length) 707 | 708 | let character = characters[0].dataValues 709 | test.deleteObjcetTimestamps(character) 710 | delete character.user_characters 711 | 712 | character.is_bastard = !character.is_bastard 713 | 714 | server 715 | .put(`/user/${id}/characters/${character.id}`) 716 | .send(character) 717 | .expect(200) 718 | .end((err, res) => { 719 | 720 | if (err) return done(err) 721 | let body = res.body 722 | assert('object' === typeof body) 723 | debug(body) 724 | assert(body.id) 725 | assert(body.user_characters) 726 | test.assertObject(body, character) 727 | test.assertModelById(association, body.id, character, done) 728 | 729 | }) 730 | 731 | }) 732 | 733 | }).catch(done) 734 | 735 | }) 736 | 737 | it ('should return 200 | put /user/:id/characters/:associationId, create relationship', function (done) { 738 | 739 | const id = 1 740 | const associationId = 2 741 | 742 | model.findById(id).then(user => { 743 | 744 | assert(user) 745 | 746 | association.findById(associationId).then(character => { 747 | 748 | character = character.dataValues 749 | test.deleteObjcetTimestamps(character) 750 | 751 | debug(character) 752 | 753 | server 754 | .put(`/user/${id}/characters/${associationId}`) 755 | .send(character) 756 | .end((err, res) => { 757 | 758 | // sequelize bug which upsert return wrong value 759 | debug(res.statusCode) 760 | assert([200, 201].indexOf(res.statusCode) !== -1) 761 | 762 | if (err) return done(err) 763 | let body = res.body 764 | assert('object' === typeof body) 765 | debug(body) 766 | assert(body.id) 767 | assert(body.user_characters) 768 | assert(body.user_characters.user_id === id) 769 | assert(body.user_characters.character_id === associationId) 770 | test.assertObject(body, character) 771 | test.assertModelById(association, body.id, character, done) 772 | 773 | }) 774 | 775 | }) 776 | }).catch(done) 777 | 778 | }) 779 | 780 | it ('should return 201 | put /user/:id/characters/:associationId', function (done) { 781 | 782 | const id = 1 783 | const associationId = 100 784 | 785 | model.findById(id).then(user => { 786 | 787 | assert(user) 788 | 789 | const character = { 790 | id: associationId, 791 | name: 'Sansa', 792 | house_id: 1 793 | } 794 | 795 | server 796 | .put(`/user/${id}/characters/${character.id}`) 797 | .send(character) 798 | .expect(201) 799 | .end((err, res) => { 800 | 801 | if (err) return done(err) 802 | let body = res.body 803 | assert('object' === typeof body) 804 | debug(body) 805 | assert(body.id) 806 | assert(body.user_characters) 807 | test.assertObject(body, character) 808 | test.assertModelById(association, body.id, character, done) 809 | 810 | }) 811 | 812 | }).catch(done) 813 | 814 | }) 815 | 816 | it ('should return 404 | put /user/:id/characters/:associationId, wrong id', function (done) { 817 | 818 | const id = 100 819 | 820 | server 821 | .put(`/user/${id}/characters/1`) 822 | .send({}) 823 | .expect(404) 824 | .end(done) 825 | 826 | }) 827 | 828 | }) 829 | 830 | describe ('DELETE', function () { 831 | 832 | it ('should return 204 | delete /user/:id/characters', function (done) { 833 | 834 | const id = 1 835 | 836 | model.findById(id).then(user => { 837 | 838 | assert(user) 839 | 840 | return user.getCharacters().then(characters => { 841 | return { user, characters } 842 | }) 843 | 844 | }).then(res => { 845 | 846 | const { 847 | user, characters 848 | } = res 849 | 850 | const characterIds = characters.map(character => character.id) 851 | 852 | server 853 | .del(`/user/${user.id}/characters`) 854 | .expect(204) 855 | .end((err, res) => { 856 | 857 | if (err) return done(err) 858 | 859 | user.getCharacters().then(characters => { 860 | assert(characters.length === 0) 861 | return model.findAll({ 862 | where: { id: characterIds } 863 | }) 864 | }).then(characters => { 865 | assert(characters.length) 866 | done() 867 | }) 868 | 869 | }) 870 | 871 | }).catch(done) 872 | 873 | }) 874 | 875 | it ('should return 204 | delete /user/:id/characters, with querystring', function (done) { 876 | 877 | const id = 1 878 | 879 | const where = { 880 | house_id: 5 881 | } 882 | 883 | const querystring = qs.stringify(where) 884 | 885 | model.findById(id).then(user => { 886 | 887 | assert(user) 888 | 889 | server 890 | .del(`/user/${user.id}/characters?${querystring}`) 891 | .expect(204) 892 | .end((err, res) => { 893 | 894 | if (err) return done(err) 895 | 896 | user.getCharacters({ where }).then(characters => { 897 | assert(characters.length === 0) 898 | return association.findAll({ where }) 899 | }).then(characters => { 900 | assert(characters.length) 901 | done() 902 | }) 903 | 904 | }) 905 | 906 | }).catch(done) 907 | 908 | }) 909 | 910 | it ('should return 204 | delete /user/:id/characters/:associationId', function (done) { 911 | 912 | const id = 1 913 | 914 | model.findById(id).then(user => { 915 | 916 | assert(user) 917 | 918 | return user.getCharacters().then(characters => { 919 | return { user, characters } 920 | }) 921 | 922 | }).then(res => { 923 | 924 | let { 925 | user, characters 926 | } = res 927 | 928 | assert(characters.length) 929 | 930 | const character = characters[0] 931 | const where = {} 932 | where.id = character.id 933 | 934 | server 935 | .del(`/user/${user.id}/characters/${character.id}`) 936 | .expect(204) 937 | .end((err, res) => { 938 | 939 | if (err) return done(err) 940 | 941 | user.getCharacters({ where }).then(characters => { 942 | assert(characters.length === 0) 943 | return user.getCharacters({ where }) 944 | }).then(character => { 945 | assert(character) 946 | done() 947 | }) 948 | 949 | }) 950 | 951 | }).catch(done) 952 | 953 | }) 954 | 955 | it('should return 404 | del /user/:id/characters', function (done) { 956 | 957 | const id = 100 958 | 959 | server 960 | .del(`/user/${id}/characters`) 961 | .expect(404) 962 | .end(done) 963 | 964 | }) 965 | 966 | it ('should return 404 | del /user/:id/characters/:associationId, wrong id', function (done) { 967 | 968 | const id = 100 969 | 970 | server 971 | .del(`/user/${id}/characters/1`) 972 | .expect(404) 973 | .end(done) 974 | 975 | }) 976 | 977 | it ('should return 404 | del /user/:id/characters/:associationId, wrong associationId', function (done) { 978 | 979 | const id = 1 980 | 981 | model.findById(id).then(user => { 982 | 983 | assert(user) 984 | 985 | server 986 | .del(`/user/${id}/characters/100`) 987 | .expect(404) 988 | .end(done) 989 | 990 | }).catch(done) 991 | 992 | }) 993 | 994 | 995 | }) 996 | 997 | }) 998 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('restql:test:common') 4 | const assert = require('assert') 5 | const common = require('../lib/common') 6 | const methods = require('../lib/methods') 7 | 8 | describe ('common', function () { 9 | 10 | const { 11 | switchByType, shouldIgnoreAssociation 12 | } = common; 13 | 14 | describe ('switchByType | callbacks are functions', function () { 15 | 16 | const callbacks = { 17 | object : () => 'object', 18 | array : () => 'array', 19 | string : () => 'string', 20 | bool : () => 'bool', 21 | number : () => 'number', 22 | defaults : () => 'defaults' 23 | } 24 | 25 | it ('should call object callback', function () { 26 | 27 | let res = switchByType({}, callbacks); 28 | assert(res === 'object'); 29 | 30 | }) 31 | 32 | it ('should call array callback', function () { 33 | 34 | let res = switchByType([], callbacks); 35 | assert(res === 'array'); 36 | 37 | }) 38 | 39 | it ('should call string callback', function () { 40 | 41 | let res = switchByType('', callbacks); 42 | assert(res === 'string'); 43 | 44 | }) 45 | 46 | it ('should call bool callback', function () { 47 | 48 | let res = switchByType(true, callbacks); 49 | assert(res === 'bool'); 50 | 51 | }) 52 | 53 | it ('should call number callback', function () { 54 | 55 | let res = switchByType(0, callbacks); 56 | assert(res === 'number'); 57 | 58 | }) 59 | 60 | it ('should call default callback', function () { 61 | 62 | function tester () {} 63 | 64 | let res = switchByType(tester, callbacks); 65 | assert(res === 'defaults'); 66 | 67 | }) 68 | 69 | }) 70 | 71 | describe ('switchByType | callbacks are not function', function () { 72 | 73 | function getTestCallbacks (key) { 74 | const callbacks = {}; 75 | callbacks[key] = true; 76 | return callbacks; 77 | } 78 | 79 | it ('should call object callback', function () { 80 | 81 | let cbs = getTestCallbacks('object') 82 | let res = switchByType({}, cbs) 83 | assert(res) 84 | 85 | }) 86 | 87 | it ('should call array callback', function () { 88 | 89 | let cbs = getTestCallbacks('array') 90 | let res = switchByType([], cbs) 91 | assert(res) 92 | 93 | }) 94 | 95 | it ('should call string callback', function () { 96 | 97 | let cbs = getTestCallbacks('string') 98 | let res = switchByType('', cbs); 99 | assert(res); 100 | 101 | }) 102 | 103 | it ('should call bool callback', function () { 104 | 105 | let cbs = getTestCallbacks('bool') 106 | let res = switchByType(false, cbs); 107 | assert(res); 108 | 109 | }) 110 | 111 | it ('should call number callback', function () { 112 | 113 | let cbs = getTestCallbacks('number') 114 | let res = switchByType(0, cbs); 115 | assert(res); 116 | 117 | }) 118 | 119 | it ('should call defaults callback', function () { 120 | 121 | function tester () {} 122 | 123 | let cbs = getTestCallbacks('defaults') 124 | let res = switchByType(tester, cbs); 125 | assert(res); 126 | 127 | }) 128 | 129 | }) 130 | 131 | describe ('shouldIgnoreAssociation', function () { 132 | 133 | let func = shouldIgnoreAssociation 134 | 135 | it ('should return false | ignore is a boolean', function () { 136 | 137 | methods.forEach(method => { 138 | let res = shouldIgnoreAssociation(method, { ignore: true }) 139 | assert(res); 140 | }) 141 | 142 | }) 143 | 144 | it ('should return false | ignore is an array', function () { 145 | 146 | methods.forEach(method => { 147 | 148 | let ignore = ['get', 'post'] 149 | , res = shouldIgnoreAssociation(method, { ignore }) 150 | 151 | if (ignore.indexOf(method) !== -1) { 152 | assert(res) 153 | } else { 154 | assert(!res); 155 | } 156 | }) 157 | 158 | }) 159 | 160 | }) 161 | 162 | }) 163 | 164 | -------------------------------------------------------------------------------- /test/hasMany.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:associations') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const models = prepare.sequelize.models 16 | 17 | describe ('model hasMany association routers', function () { 18 | 19 | let server 20 | 21 | before (function () { 22 | 23 | let app = koa() 24 | , restql = new RestQL(models) 25 | 26 | app.use(restql.routes()) 27 | server = request(http.createServer(app.callback())) 28 | 29 | }) 30 | 31 | beforeEach (function (done) { 32 | 33 | debug('reset db') 34 | prepare.loadMockData().then(() => { 35 | done() 36 | }).catch(done) 37 | 38 | }) 39 | 40 | const model = models.house 41 | const association = models.character 42 | 43 | describe ('GET', function () { 44 | 45 | it ('should return 200 | get /house/:id/members', function (done) { 46 | 47 | const id = 1 48 | 49 | model.findById(id).then(data => { 50 | 51 | server 52 | .get(`/gameofthrones/house/${id}/members`) 53 | .expect(200) 54 | .end((err, res) => { 55 | 56 | if (err) return done(err) 57 | let body = res.body 58 | assert(Array.isArray(body)) 59 | debug(body) 60 | assert(body.length === 2) 61 | done() 62 | 63 | }) 64 | 65 | }).catch(done) 66 | 67 | }) 68 | 69 | it ('should return 200 | get /house/:id/bastards', function (done) { 70 | 71 | const id = 1 72 | 73 | model.findById(id).then(data => { 74 | 75 | server 76 | .get(`/gameofthrones/house/${id}/bastards`) 77 | .expect(200) 78 | .end((err, res) => { 79 | 80 | if (err) return done(err) 81 | let body = res.body 82 | assert(Array.isArray(body)) 83 | debug(body) 84 | assert(body.length === 1) 85 | assert(body[0].is_bastard) 86 | done() 87 | 88 | }) 89 | 90 | }).catch(done) 91 | 92 | }) 93 | 94 | it ('should return 404 | get /house/:id/members', function (done) { 95 | 96 | const id = 100 97 | 98 | server 99 | .get(`/gameofthrones/house/${id}/members`) 100 | .expect(404) 101 | .end(done) 102 | 103 | }) 104 | 105 | it ('should return 200 | get /house/:id/members/:associationId ', function (done) { 106 | 107 | const id = 1 108 | 109 | model.findById(id).then(house => { 110 | 111 | house.getMembers().then(members => { 112 | 113 | let member = members[0] 114 | member = member.dataValues 115 | 116 | test.deleteObjcetTimestamps(member) 117 | 118 | debug(member) 119 | 120 | server 121 | .get(`/gameofthrones/house/${id}/members/${member.id}`) 122 | .expect(200) 123 | .end((err, res) => { 124 | 125 | if (err) return done(err) 126 | let body = res.body 127 | assert('object' === typeof body) 128 | debug(body) 129 | test.assertObject(body, member) 130 | test.assertModelById(association, body.id, member, done) 131 | 132 | }) 133 | 134 | }) 135 | 136 | }).catch(done) 137 | 138 | }) 139 | 140 | it ('should return 404 | get /house/:id/members/:associationId, wrong id', function (done) { 141 | 142 | const id = 100 143 | 144 | server 145 | .get(`/gameofthrones/house/${id}/members/1`) 146 | .expect(404) 147 | .end(done) 148 | 149 | }) 150 | 151 | it ('should return 404 | get /house/:id/members/:associationId, wrong associationId', function (done) { 152 | 153 | const id = 1 154 | 155 | model.findById(id).then(house => { 156 | 157 | assert(house) 158 | 159 | server 160 | .get(`/gameofthrones/house/${id}/members/100`) 161 | .expect(404) 162 | .end(done) 163 | 164 | }).catch(done) 165 | 166 | }) 167 | 168 | }) 169 | 170 | describe ('POST', function () { 171 | 172 | it ('should return 201 | post /house/:id/members, object body', function (done) { 173 | 174 | const id = 1 175 | const data = { 176 | name: 'Sansa' 177 | } 178 | 179 | model.findById(id).then(house => { 180 | 181 | server 182 | .post(`/gameofthrones/house/${id}/members`) 183 | .send(data) 184 | .expect(201) 185 | .end((err, res) => { 186 | 187 | if (err) return done(err) 188 | let body = res.body 189 | assert('object' === typeof body) 190 | debug(body) 191 | assert(body.id) 192 | assert(body.house_id === house.id) 193 | test.assertObject(body, data) 194 | test.assertModelById(association, body.id, data, done) 195 | 196 | }) 197 | 198 | }).catch(done) 199 | 200 | }) 201 | 202 | it ('should return 201 | post /house/:id/members, array body', function (done) { 203 | 204 | const id = 1 205 | const data = [{ 206 | name: 'Sansa' 207 | }, { 208 | name: 'Bran' 209 | }] 210 | 211 | model.findById(id).then(house => { 212 | 213 | server 214 | .post(`/gameofthrones/house/${id}/members`) 215 | .send(data) 216 | .expect(201) 217 | .end((err, res) => { 218 | 219 | if (err) return done(err) 220 | let body = res.body 221 | assert(Array.isArray(body)) 222 | debug(body) 223 | 224 | assert(body.length === data.length) 225 | 226 | const promises = body.map((character, index) => { 227 | assert(character.id) 228 | assert(character.house_id === house.id) 229 | test.assertObject(character, data[index]) 230 | return test.assertModelById(association, 231 | character.id, data[index]) 232 | }) 233 | 234 | Promise.all(promises).then(() => done()) 235 | 236 | }) 237 | 238 | }).catch(done) 239 | 240 | }) 241 | 242 | it ('should return 404 | post /house/:id/members', function (done) { 243 | 244 | const id = 100 245 | 246 | server 247 | .post(`/gameofthrones/house/${id}/members`) 248 | .send({}) 249 | .expect(404) 250 | .end(done) 251 | 252 | }) 253 | 254 | describe ('unique key constraint error', function () { 255 | 256 | it ('should return 409 | post /house/:id/members, object body', function (done) { 257 | 258 | const id = 1 259 | association.find({ 260 | where: { 261 | house_id: id 262 | } 263 | }).then(character => { 264 | 265 | character = character.dataValues 266 | delete character.id 267 | delete character.created_at 268 | delete character.updated_at 269 | delete character.deleted_at 270 | 271 | debug(character) 272 | 273 | server 274 | .post(`/gameofthrones/house/${id}/members`) 275 | .send(character) 276 | .expect(409) 277 | .end(done) 278 | 279 | }).catch(done) 280 | 281 | }) 282 | 283 | it ('should return 409 | post /house/:id/members, array body', function (done) { 284 | 285 | const id = 1 286 | 287 | association.findAll({ 288 | where: { 289 | house_id: id 290 | } 291 | }).then(characters => { 292 | 293 | characters = characters.map(character => { 294 | character = character.dataValues 295 | delete character.id 296 | delete character.created_at 297 | delete character.updated_at 298 | delete character.deleted_at 299 | return character 300 | }) 301 | 302 | characters.push({ 303 | name: 'Sansa' 304 | }) 305 | 306 | debug(characters) 307 | 308 | server 309 | .post(`/gameofthrones/house/${id}/members`) 310 | .send(characters) 311 | .expect(409) 312 | .end((err, res) => { 313 | 314 | if (err) return done(err) 315 | 316 | association.findAll({ 317 | where: { 318 | house_id: id 319 | } 320 | }).then(data => { 321 | assert(data.length === characters.length - 1) 322 | assert(!data.find(row => row.name === 'Sansa')) 323 | done() 324 | }) 325 | }) 326 | 327 | }).catch(done) 328 | 329 | }) 330 | 331 | it ('should return 201 | post /house/:id/members, object body', function (done) { 332 | 333 | const id = 1 334 | 335 | association.find({ 336 | where: { 337 | house_id: id 338 | } 339 | }).then(character => { 340 | return character.destroy().then(() => { 341 | return association.findById(character.id).then(data => { 342 | assert(!data) 343 | return character.dataValues 344 | }) 345 | }) 346 | }).then(character => { 347 | 348 | delete character.id 349 | delete character.created_at 350 | delete character.updated_at 351 | delete character.deleted_at 352 | 353 | server 354 | .post(`/gameofthrones/house/${id}/members`) 355 | .send(character) 356 | .expect(201) 357 | .end((err, res) => { 358 | 359 | if (err) return done(err) 360 | let body = res.body 361 | assert('object' === typeof body) 362 | debug(body) 363 | assert(body.id) 364 | assert(body.house_id === id) 365 | test.assertObject(body, character) 366 | test.assertModelById(association, body.id, character, done) 367 | 368 | }) 369 | 370 | }).catch(done) 371 | 372 | }) 373 | 374 | it ('should return 201 | post /house/:id/members, array body', function (done) { 375 | 376 | const id = 1 377 | 378 | const where = { house_id: id } 379 | 380 | association.findAll({ where }).then(characters => { 381 | return association.destroy({ where }).then(() => { 382 | return association.findAll({ where }).then(data => { 383 | assert(!data.length) 384 | return characters 385 | }) 386 | }) 387 | }).then(characters => { 388 | 389 | characters = characters.map(character => { 390 | 391 | character = character.dataValues 392 | 393 | delete character.id 394 | delete character.created_at 395 | delete character.updated_at 396 | delete character.deleted_at 397 | 398 | return character 399 | }) 400 | 401 | characters.push({ 402 | name: 'Sansa' 403 | }) 404 | 405 | server 406 | .post(`/gameofthrones/house/${id}/members`) 407 | .send(characters) 408 | .expect(201) 409 | .end((err, res) => { 410 | 411 | if (err) return done(err) 412 | let body = res.body 413 | assert(Array.isArray(body)) 414 | debug(body) 415 | 416 | let promises = body.map((character, index) => { 417 | assert(character.id) 418 | assert(character.house_id === id) 419 | test.assertObject(character, characters[index]) 420 | test.assertModelById(association, character.id, characters[index]) 421 | }) 422 | 423 | Promise.all(promises).then(() => done()) 424 | }) 425 | 426 | }).catch(done) 427 | 428 | }) 429 | 430 | }) 431 | 432 | }) 433 | 434 | describe ('PUT', function () { 435 | 436 | it ('should return 201 | put /house/:id/members, object body', function (done) { 437 | 438 | const id = 1 439 | const data = { 440 | name: 'Sansa' 441 | } 442 | 443 | model.findById(id).then(house => { 444 | 445 | server 446 | .put(`/gameofthrones/house/${id}/members`) 447 | .send(data) 448 | .expect(201) 449 | .end((err, res) => { 450 | 451 | if (err) return done(err) 452 | let body = res.body 453 | assert('object' === typeof body) 454 | debug(body) 455 | assert(body.id) 456 | assert(body.house_id === house.id) 457 | test.assertObject(body, data) 458 | test.assertModelById(association, body.id, data, done) 459 | 460 | }) 461 | 462 | }).catch(done) 463 | 464 | }) 465 | 466 | it ('should return 200 | put /house/:id/members, object body', function (done) { 467 | 468 | const id = 1 469 | const data = { 470 | name: 'Jon', 471 | is_bastard: false 472 | } 473 | 474 | model.findById(id).then(house => { 475 | 476 | server 477 | .put(`/gameofthrones/house/${id}/members`) 478 | .send(data) 479 | .expect(200) 480 | .end((err, res) => { 481 | 482 | if (err) return done(err) 483 | let body = res.body 484 | assert('object' === typeof body) 485 | debug(body) 486 | assert(body.id) 487 | assert(body.house_id === house.id) 488 | test.assertObject(body, data) 489 | test.assertModelById(association, body.id, data, done) 490 | 491 | }) 492 | 493 | }).catch(done) 494 | 495 | }) 496 | 497 | it ('should return 200 | put /house/:id/members, array body', function (done) { 498 | 499 | const id = 1 500 | const members = [{ 501 | name: 'Sansa', 502 | is_bastard: false 503 | }, { 504 | name: 'Bran', 505 | is_bastard: false 506 | }, { 507 | name: 'Jon', 508 | is_bastard: false 509 | }] 510 | 511 | model.findById(id).then(house => { 512 | 513 | server 514 | .put(`/gameofthrones/house/${id}/members`) 515 | .send(members) 516 | .expect(200) 517 | .end((err, res) => { 518 | 519 | if (err) return done(err) 520 | let body = res.body 521 | assert(Array.isArray(body)) 522 | debug(body) 523 | 524 | assert(body.length === members.length) 525 | 526 | const promises = body.map((character, index) => { 527 | assert(character.id) 528 | assert(character.house_id === house.id) 529 | test.deleteObjcetTimestamps(character) 530 | return test.assertModelById(association, character.id, character) 531 | }) 532 | 533 | Promise.all(promises).then(() => done()) 534 | 535 | }) 536 | 537 | }).catch(done) 538 | 539 | }) 540 | 541 | it ('should return 404 | put /house/:id/members', function (done) { 542 | 543 | const id = 100 544 | 545 | server 546 | .put(`/gameofthrones/house/${id}/members`) 547 | .send({}) 548 | .expect(404) 549 | .end(done) 550 | 551 | }) 552 | 553 | it ('should return 200 | put /house/:id/members/:associationId, object body', function (done) { 554 | 555 | const id = 1 556 | const data = { 557 | name: 'Jon', 558 | is_bastard: false 559 | } 560 | 561 | association.find({ 562 | where: { 563 | house_id: id 564 | } 565 | }).then(character => { 566 | 567 | server 568 | .put(`/gameofthrones/house/${id}/members/${character.id}`) 569 | .send(data) 570 | .expect(200) 571 | .end((err, res) => { 572 | 573 | if (err) return done(err) 574 | let body = res.body 575 | assert('object' === typeof body) 576 | debug(body) 577 | assert(body.id) 578 | assert(body.house_id === id) 579 | test.assertObject(body, data) 580 | test.assertModelById(association, body.id, data, done) 581 | 582 | }) 583 | 584 | }).catch(done) 585 | 586 | }) 587 | 588 | it ('should return 201 | put /house/:id/members/:associationId', function (done) { 589 | 590 | const id = 1 591 | const associationId = 100 592 | const character = { 593 | name: 'Sansa', 594 | } 595 | 596 | model.findById(id).then(house => { 597 | 598 | assert(house) 599 | 600 | return model.find({ 601 | where: character 602 | }).then(member => { 603 | assert(!member) 604 | return { house, character } 605 | }) 606 | 607 | }).then(res => { 608 | 609 | const { 610 | house, character 611 | } = res 612 | 613 | 614 | server 615 | .put(`/gameofthrones/house/${id}/members/${associationId}`) 616 | .send(character) 617 | .expect(201) 618 | .end((err, res) => { 619 | 620 | if (err) return done(err) 621 | let body = res.body 622 | assert('object' === typeof body) 623 | debug(body) 624 | assert(body.id) 625 | assert(body.id === associationId) 626 | assert(body.house_id === id) 627 | test.assertObject(body, character) 628 | test.assertModelById(association, body.id, character, done) 629 | 630 | }) 631 | 632 | }).catch(done) 633 | 634 | }) 635 | 636 | it ('should return 200 | put /house/:id/members/:associationId, create relationship', function (done) { 637 | 638 | const id = 1 639 | 640 | const character = { 641 | name: 'Sansa' 642 | } 643 | 644 | model.findById(id).then(house => { 645 | 646 | assert(house) 647 | 648 | return association.create(character).then(character => { 649 | return { house, character } 650 | }) 651 | 652 | }).then(res => { 653 | 654 | let { 655 | house, character 656 | } = res 657 | 658 | character = character.dataValues 659 | test.deleteObjcetTimestamps(character) 660 | delete character.house_id 661 | 662 | debug(character) 663 | 664 | server 665 | .put(`/gameofthrones/house/${id}/members/${character.id}`) 666 | .send(character) 667 | .end((err, res) => { 668 | 669 | // sequelize bug which upsert return wrong value 670 | debug(res.statusCode) 671 | assert([200, 201].indexOf(res.statusCode) !== -1) 672 | 673 | if (err) return done(err) 674 | let body = res.body 675 | assert('object' === typeof body) 676 | debug(body) 677 | assert(body.id) 678 | assert(body.id === character.id) 679 | assert(body.house_id === id) 680 | test.assertObject(body, character) 681 | test.assertModelById(association, body.id, character, done) 682 | 683 | }) 684 | 685 | }).catch(done) 686 | 687 | }) 688 | 689 | it ('should return 409 | put /house/:id/members/:associationId, object body', function (done) { 690 | 691 | const id = 1 692 | const data = { 693 | name: 'Arya', 694 | } 695 | 696 | association.find({ 697 | where: { 698 | house_id: id 699 | } 700 | }).then(character => { 701 | 702 | server 703 | .put(`/gameofthrones/house/${id}/members/${character.id}`) 704 | .send(data) 705 | .expect(409) 706 | .end(done) 707 | 708 | }).catch(done) 709 | 710 | }) 711 | 712 | 713 | it ('should return 404 | put /house/:id/members/:associationId, wrong id', function (done) { 714 | 715 | const id = 100 716 | 717 | server 718 | .put(`/gameofthrones/house/${id}/members/1`) 719 | .send({}) 720 | .expect(404) 721 | .end(done) 722 | 723 | }) 724 | 725 | }) 726 | 727 | describe ('DELETE', function () { 728 | 729 | it ('should return 204 | delete /house/:id/members', function (done) { 730 | 731 | const id = 1 732 | 733 | model.findById(id).then(house => { 734 | 735 | assert(house) 736 | return house.getMembers().then(members => { 737 | assert(members.length) 738 | return { house, members } 739 | }) 740 | 741 | }).then(res => { 742 | 743 | const { 744 | house, members 745 | } = res 746 | 747 | server 748 | .del(`/gameofthrones/house/${id}/members`) 749 | .expect(204) 750 | .end((err, res) => { 751 | 752 | if (err) return done(err) 753 | 754 | house.getMembers().then(members => { 755 | debug(members) 756 | assert(!members.length) 757 | done() 758 | }) 759 | 760 | }) 761 | 762 | }).catch(done) 763 | 764 | }) 765 | 766 | it ('should return 204 | delete /house/:id/members, with query', function (done) { 767 | 768 | const id = 1 769 | 770 | const where = { 771 | name: 'Jon' 772 | } 773 | 774 | const querystring = qs.stringify(where) 775 | 776 | model.findById(id).then(house => { 777 | 778 | assert(house) 779 | return house.getMembers({ where }).then(members => { 780 | assert(members.length) 781 | return { house, members } 782 | }) 783 | 784 | }).then(res => { 785 | 786 | const { 787 | house, members 788 | } = res 789 | 790 | server 791 | .del(`/gameofthrones/house/${id}/members?${querystring}`) 792 | .expect(204) 793 | .end((err, res) => { 794 | 795 | if (err) return done(err) 796 | 797 | house.getMembers({ where }).then(members => { 798 | assert(!members.length) 799 | return house.getMembers() 800 | }).then(members => { 801 | assert(members.length) 802 | done() 803 | }) 804 | 805 | }) 806 | 807 | }).catch(done) 808 | 809 | }) 810 | 811 | it ('should return 404 | delete /house/:id/members', function (done) { 812 | 813 | const id = 100 814 | 815 | server 816 | .del(`/gameofthrones/house/${id}/members`) 817 | .expect(404) 818 | .end(done) 819 | 820 | }) 821 | 822 | it ('should return 204 | delete /house/:id/members/associationId', function (done) { 823 | 824 | const id = 1 825 | 826 | model.findById(id).then(house => { 827 | 828 | assert(house) 829 | return house.getMembers().then(members => { 830 | assert(members.length) 831 | return { house, members } 832 | }) 833 | 834 | }).then(res => { 835 | 836 | const { 837 | house, members 838 | } = res 839 | 840 | let member = members[0].dataValues 841 | test.deleteObjcetTimestamps(member) 842 | 843 | server 844 | .del(`/gameofthrones/house/${id}/members/${member.id}`) 845 | .expect(204) 846 | .end((err, res) => { 847 | 848 | if (err) return done(err) 849 | 850 | association.findById(member.id).then(member => { 851 | assert(!member) 852 | done() 853 | }) 854 | 855 | }) 856 | 857 | }).catch(done) 858 | 859 | }) 860 | 861 | it ('should return 404 | delete /house/:id/members/:associationId, wrong id', function (done) { 862 | 863 | const id = 100 864 | 865 | server 866 | .del(`/gameofthrones/house/${id}/members/1`) 867 | .expect(404) 868 | .end(done) 869 | 870 | }) 871 | 872 | it ('should return 404 | delete /house/:id/members/:associationId, wrong associationId', function (done) { 873 | 874 | const id = 1 875 | 876 | model.findById(id).then(house => { 877 | 878 | assert(house) 879 | 880 | server 881 | .delete(`/gameofthrones/house/${id}/members/1000`) 882 | .expect(404) 883 | .end(done) 884 | 885 | }).catch(done) 886 | 887 | }) 888 | 889 | }) 890 | 891 | }) 892 | -------------------------------------------------------------------------------- /test/hasOne.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:associations') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const models = prepare.sequelize.models 16 | 17 | describe ('model hasOne association routers', function () { 18 | 19 | let server 20 | 21 | before (function () { 22 | 23 | let app = koa() 24 | , restql = new RestQL(models) 25 | 26 | app.use(restql.routes()) 27 | server = request(http.createServer(app.callback())) 28 | 29 | }) 30 | 31 | beforeEach (function (done) { 32 | 33 | debug('reset db') 34 | prepare.loadMockData().then(() => { 35 | done() 36 | }).catch(done) 37 | 38 | }) 39 | 40 | const model = models.house 41 | const association = models.seat 42 | 43 | describe ('GET', function () { 44 | 45 | it ('should return 200 | get /house/:id/seat', function (done) { 46 | 47 | const id = 2 48 | 49 | model.findById(id).then(data => { 50 | 51 | server 52 | .get(`/gameofthrones/house/${id}/seat`) 53 | .expect(200) 54 | .end((err, res) => { 55 | 56 | if (err) return done(err) 57 | let body = res.body 58 | assert('object' === typeof body) 59 | debug(body) 60 | assert(body.house_id === id) 61 | done() 62 | 63 | }) 64 | 65 | }).catch(done) 66 | 67 | }) 68 | 69 | it ('should return 404 | get /house/:id/seat', function (done) { 70 | 71 | const id = 100 72 | 73 | server 74 | .get(`/gameofthrones/house/${id}/seat`) 75 | .expect(404) 76 | .end(done) 77 | 78 | }) 79 | 80 | }) 81 | 82 | describe ('PUT', function () { 83 | 84 | it ('should return 200 | put /house/:id/seat', function (done) { 85 | 86 | const id = 2 87 | const data = { 88 | name: uuid() 89 | } 90 | 91 | model.findById(id).then(house => { 92 | 93 | server 94 | .put(`/gameofthrones/house/${id}/seat`) 95 | .send(data) 96 | .expect(200) 97 | .end((err, res) => { 98 | 99 | if (err) return done(err) 100 | let body = res.body 101 | assert('object' === typeof body) 102 | debug(body) 103 | assert(body.house_id === id) 104 | test.assertObject(body, data) 105 | test.assertModelById(association, body.house_id, data, done) 106 | 107 | }) 108 | 109 | }).catch(done) 110 | 111 | }) 112 | 113 | it ('should return 200 | put /house/:id/seat, restore from destroyed', function (done) { 114 | 115 | const id = 2 116 | const data = { 117 | name: uuid() 118 | } 119 | 120 | association.destroy({ 121 | where: { 122 | house_id: id 123 | } 124 | }).then((row) => { 125 | assert(row) 126 | return model.findById(id) 127 | }).then(house => { 128 | 129 | server 130 | .put(`/gameofthrones/house/${id}/seat`) 131 | .send(data) 132 | .expect(200) 133 | .end((err, res) => { 134 | 135 | if (err) return done(err) 136 | let body = res.body 137 | assert('object' === typeof body) 138 | debug(body) 139 | assert(body.house_id === id) 140 | test.assertObject(body, data) 141 | test.assertModelById(association, body.id, data, done) 142 | 143 | }) 144 | 145 | }).catch(done) 146 | 147 | }) 148 | 149 | it ('should return 201 | put /house/:id/seat', function (done) { 150 | 151 | const id = 2 152 | const data = { 153 | name: uuid() 154 | } 155 | 156 | association.destroy({ 157 | where: { 158 | house_id: id 159 | }, 160 | force: true 161 | }).then((row) => { 162 | assert(row) 163 | return model.findById(id) 164 | }).then(house => { 165 | 166 | server 167 | .put(`/gameofthrones/house/${id}/seat`) 168 | .send(data) 169 | .expect(201) 170 | .end((err, res) => { 171 | 172 | if (err) return done(err) 173 | let body = res.body 174 | assert('object' === typeof body) 175 | debug(body) 176 | assert(body.house_id === id) 177 | test.assertObject(body, data) 178 | test.assertModelById(association, body.id, data, done) 179 | 180 | }) 181 | 182 | }).catch(done) 183 | 184 | }) 185 | 186 | }) 187 | 188 | describe ('DELETE', function () { 189 | 190 | it ('should return 204 | delete /house/:id/seat', function (done) { 191 | 192 | const id = 2 193 | 194 | association.find({ 195 | where: { 196 | house_id: id 197 | } 198 | }).then(seat => { 199 | assert(seat) 200 | return model.findById(id) 201 | }).then(house => { 202 | 203 | server 204 | .del(`/gameofthrones/house/${id}/seat`) 205 | .expect(204) 206 | .end((err, res) => { 207 | 208 | association.find({ 209 | where: { 210 | house_id: id 211 | }, 212 | }).then(data => { 213 | assert(!data) 214 | done() 215 | }) 216 | 217 | }) 218 | 219 | }).catch(done) 220 | 221 | }) 222 | 223 | it ('should return 404 | delete /house/:id/seat', function (done) { 224 | 225 | const id = 2 226 | 227 | association.destroy({ 228 | where: { 229 | house_id: id 230 | } 231 | }).then(row => { 232 | assert(row) 233 | return model.findById(id) 234 | }).then(house => { 235 | 236 | server 237 | .del(`/gameofthrones/house/${id}/seat`) 238 | .expect(404) 239 | .end(done) 240 | 241 | }).catch(done) 242 | 243 | }) 244 | 245 | }) 246 | 247 | }) 248 | -------------------------------------------------------------------------------- /test/lib/assert.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const debug = require('debug')('koa-restql:test:assert') 5 | 6 | const assertObject = (data, expect) => { 7 | 8 | assert(data) 9 | assert(expect) 10 | 11 | const keys = Object.keys(expect) 12 | keys.forEach(key => { 13 | assert(data[key] !== undefined) 14 | assert(data[key] === expect[key]) 15 | }) 16 | 17 | } 18 | 19 | const assertModelById = (model, id, expect, done) => { 20 | 21 | assert(id) 22 | return model.findById(id).then(res => { 23 | assertObject(res.dataValues, expect) 24 | if (done) done() 25 | }) 26 | 27 | } 28 | 29 | module.exports.assertObject = assertObject 30 | module.exports.assertModelById = assertModelById 31 | 32 | -------------------------------------------------------------------------------- /test/lib/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | db: "mysql://koa-restql-test:test@localhost/koa-restql-test#UTF8" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /test/lib/prepare.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs') 4 | const _ = require('lodash') 5 | const util = require('util') 6 | const path = require('path') 7 | const uuid = require('node-uuid') 8 | const debug = require('debug')('sequelize') 9 | const Sequelize = require('sequelize') 10 | 11 | const config = require('./config') 12 | const mock = require('../mock/data') 13 | 14 | const database = process.env.TEST_DB || config.db 15 | 16 | const sequelize = new Sequelize(database, { 17 | 18 | logging : debug, 19 | underscored : true, 20 | underscoredAll : true, 21 | 22 | define: { 23 | paranoid : true, 24 | underscored : true, 25 | freezeTableName : true, 26 | schemaDelimiter : '_', 27 | createdAt : 'created_at', 28 | updatedAt : 'updated_at', 29 | deletedAt : 'deleted_at' 30 | }, 31 | }) 32 | 33 | const loadMockModels = (modelsPath, schema) => { 34 | 35 | fs.readdirSync(modelsPath).forEach(filename => { 36 | 37 | let modelPath = path.resolve(modelsPath, filename) 38 | , stats = fs.lstatSync(modelPath) 39 | , isDirectory = stats.isDirectory() 40 | , validNameRegex = /^(.+)\.(js|json)/ 41 | , isValidFile = validNameRegex.test(filename) || isDirectory 42 | 43 | // keep .js, .json or directory 44 | if (!isValidFile) return 45 | 46 | // load model recursively 47 | if (isDirectory) { 48 | 49 | loadMockModels(modelPath, filename) 50 | 51 | } else { 52 | let model = require(modelPath) 53 | , name = filename.match(validNameRegex)[1] 54 | 55 | let { 56 | options = {}, attributes 57 | }= model; 58 | 59 | if (schema) { 60 | options.schema = schema 61 | } 62 | 63 | if ('function' !== typeof attributes) { 64 | throw new Error(`model ${name}'s attributes is not found`); 65 | } 66 | 67 | sequelize.define(name, attributes(Sequelize), options) 68 | } 69 | }) 70 | } 71 | 72 | const reset = () => { 73 | 74 | return sequelize.sync({ 75 | 76 | logging : debug, 77 | force : true 78 | 79 | }) 80 | 81 | } 82 | 83 | const loadMockData = () => { 84 | 85 | let models = sequelize.models 86 | , promises = [] 87 | 88 | return sequelize.sync({ 89 | 90 | logging : debug, 91 | force : true 92 | 93 | }).then(() => { 94 | Object.keys(models).forEach(key => { 95 | 96 | let data = mock[key] 97 | , model = models[key] 98 | 99 | if (data && Array.isArray(data)) { 100 | promises.push(model.bulkCreate(data)) 101 | } 102 | }) 103 | 104 | return Promise.all(promises).then(() => mock) 105 | }) 106 | } 107 | 108 | loadMockModels('test/mock/models') 109 | 110 | const models = sequelize.models; 111 | Object.keys(models).forEach(key => { 112 | 113 | let model = models[key] 114 | 115 | if ('associate' in model) { 116 | model.associate(models) 117 | } 118 | 119 | debug(`model: ${model.name}, 120 | associations: [${Object.keys(model.associations).join()}]`) 121 | }) 122 | 123 | const createMockData = (model, attributes, count, defaultValues) => { 124 | 125 | const data = [] 126 | 127 | for (let i = 0; i < count; i ++) { 128 | let row = {} 129 | attributes.forEach(attribute => row[attribute] = uuid()) 130 | _.assign(row, defaultValues || {}) 131 | data.push(row) 132 | } 133 | 134 | return model.bulkCreate(data) 135 | 136 | } 137 | 138 | 139 | module.exports = { 140 | sequelize, loadMockData, reset, createMockData 141 | } 142 | -------------------------------------------------------------------------------- /test/lib/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const debug = require('debug')('koa-restql:test:assert') 5 | 6 | const assertObject = (data, expect) => { 7 | 8 | assert(data) 9 | assert(expect) 10 | 11 | const keys = Object.keys(expect) 12 | keys.forEach(key => { 13 | assert(data[key] !== undefined) 14 | assert(data[key] === expect[key]) 15 | }) 16 | 17 | } 18 | 19 | const assertModelById = (model, id, expect, done) => { 20 | 21 | assert(id) 22 | return model.findById(id).then(res => { 23 | assert(res) 24 | assertObject(res.dataValues, expect) 25 | if (done) done() 26 | }) 27 | 28 | } 29 | 30 | const deleteObjcetTimestamps = (data) => { 31 | delete data.created_at 32 | delete data.updated_at 33 | delete data.deleted_at 34 | } 35 | 36 | module.exports.assertObject = assertObject 37 | module.exports.assertModelById = assertModelById 38 | module.exports.deleteObjcetTimestamps = deleteObjcetTimestamps 39 | 40 | -------------------------------------------------------------------------------- /test/mock/data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | character: [ 5 | { id: 1, name: 'Jon', house_id: 1, is_bastard: true, }, 6 | { id: 2, name: 'Arya', house_id: 1 }, 7 | { id: 3, name: 'Daenerys', house_id: 2 }, 8 | { id: 4, name: 'Tyrion', house_id: 3 }, 9 | { id: 5, name: 'Ramsay', house_id: 5, is_bastard: true }, 10 | { id: 6, name: 'Jaqen' } 11 | ], 12 | 13 | seat: [ 14 | { id: 1, name: 'Winterfell', house_id: 1 }, 15 | { id: 2, name: 'Casterly Rock', house_id: 2 }, 16 | { id: 3, name: 'Meereen', house_id: 3 }, 17 | { id: 4, name: 'Dreadfort', house_id: 5 }, 18 | { id: 5, name: 'Dragonstone' }, 19 | ], 20 | 21 | house: [ 22 | { id: 1, name: 'Stark', words: 'Winter is Coming' }, 23 | { id: 2, name: 'Targaryen', words: 'Fire and Blood' }, 24 | { id: 3, name: 'Lannister', words: 'A Lannister Always Pays His Debts' }, 25 | { id: 4, name: 'Tully', words: 'Family, Duty, Honor' }, 26 | { id: 5, name: 'Bolton', words: 'Our Blades Are Sharp' } 27 | ], 28 | 29 | user: [ 30 | { id: 1, name: 'Dale', nickname: 'cg' }, 31 | { id: 2, name: 'Jocelyn', nickname: 'mm' } 32 | ], 33 | 34 | user_characters: [ 35 | { user_id: 1, character_id: 1, rate: 5 }, 36 | { user_id: 1, character_id: 3, rate: 4 }, 37 | { user_id: 1, character_id: 4, rate: 5 }, 38 | { user_id: 1, character_id: 5, rate: -5 }, 39 | { user_id: 2, character_id: 1, rate: 4 }, 40 | { user_id: 2, character_id: 2, rate: 5 }, 41 | { user_id: 2, character_id: 3, rate: 4 }, 42 | { user_id: 2, character_id: 5, rate: -5 } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /test/mock/models/gameofthrones/character.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.attributes = (DataTypes) => { 4 | 5 | return { 6 | 7 | id : { 8 | type: DataTypes.INTEGER, 9 | autoIncrement: true, 10 | primaryKey: true 11 | }, 12 | 13 | name : { 14 | type: DataTypes.STRING(100), 15 | allowNull: false, 16 | defaultValue: '' 17 | }, 18 | 19 | house_id: { 20 | type: DataTypes.INTEGER, 21 | allowNull: false, 22 | defaultValue: 0 23 | }, 24 | 25 | is_bastard: { 26 | type: DataTypes.BOOLEAN, 27 | allowNull: false, 28 | defaultValue: false 29 | }, 30 | 31 | deleted_at : { 32 | type: DataTypes.DATE, 33 | allowNull: false, 34 | /** 35 | * if this type is DATE, 36 | * defaultValue must be a Date, 37 | * otherwise paranoid is useless 38 | */ 39 | defaultValue: new Date(0) 40 | } 41 | }; 42 | } 43 | 44 | module.exports.options = { 45 | 46 | indexes: [{ 47 | type: 'unique', 48 | /* Name is important for unique index */ 49 | name: 'character_name_unique', 50 | fields: ['name'] 51 | }, { 52 | fields: ['house_id'] 53 | }, ], 54 | 55 | classMethods: { 56 | associate: (models) => { 57 | 58 | models.character.belongsTo(models.house, { 59 | as: 'house', 60 | constraints: false 61 | }) 62 | 63 | models.character.belongsToMany(models.user, { 64 | as: 'reviewers', 65 | constraints: false, 66 | through: { 67 | model: models.user_characters, 68 | }, 69 | foreignKey: 'character_id', 70 | otherKey: 'user_id' 71 | }) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/mock/models/gameofthrones/house.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.attributes = (DataTypes) => { 4 | 5 | return { 6 | 7 | id : { 8 | type: DataTypes.INTEGER, 9 | autoIncrement: true, 10 | primaryKey: true 11 | }, 12 | 13 | name : { 14 | type: DataTypes.STRING(100), 15 | allowNull: false, 16 | defaultValue: '' 17 | }, 18 | 19 | words : { 20 | type: DataTypes.STRING(100), 21 | allowNull: false, 22 | defaultValue: '' 23 | }, 24 | 25 | deleted_at : { 26 | type: DataTypes.DATE, 27 | allowNull: false, 28 | /** 29 | * if this type is DATE, 30 | * defaultValue must be a Date, 31 | * otherwise paranoid is useless 32 | */ 33 | defaultValue: new Date(0) 34 | } 35 | } 36 | } 37 | 38 | module.exports.options = { 39 | 40 | indexes: [{ 41 | type: 'unique', 42 | /* Name is important for unique index */ 43 | name: 'house_name_unique', 44 | fields: ['name'] 45 | }], 46 | 47 | classMethods: { 48 | associate: (models) => { 49 | 50 | models.house.hasOne(models.seat, { 51 | as: 'seat', 52 | constraints: false 53 | }) 54 | 55 | models.house.hasMany(models.character, { 56 | as: 'members', 57 | constraints: false 58 | }) 59 | 60 | models.house.hasMany(models.character, { 61 | as: 'bastards', 62 | constraints: false, 63 | scope: { 64 | is_bastard: true 65 | } 66 | }) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/mock/models/gameofthrones/seat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.attributes = (DataTypes) => { 4 | 5 | return { 6 | 7 | id : { 8 | type: DataTypes.INTEGER, 9 | autoIncrement: true, 10 | primaryKey: true 11 | }, 12 | 13 | name : { 14 | type: DataTypes.STRING(100), 15 | allowNull: false, 16 | defaultValue: '' 17 | }, 18 | 19 | house_id: { 20 | type: DataTypes.INTEGER, 21 | allowNull: false, 22 | defaultValue: 0 23 | }, 24 | 25 | deleted_at : { 26 | type: DataTypes.DATE, 27 | allowNull: false, 28 | /** 29 | * if this type is DATE, 30 | * defaultValue must be a Date, 31 | * otherwise paranoid is useless 32 | */ 33 | defaultValue: new Date(0) 34 | } 35 | }; 36 | } 37 | 38 | module.exports.options = { 39 | 40 | indexes: [{ 41 | type: 'unique', 42 | /* Name is important for unique index */ 43 | name: 'seat_name_unique', 44 | fields: ['name'] 45 | }, { 46 | type: 'unique', 47 | name: 'seat_house_id_unique', 48 | fields: ['house_id'] 49 | }], 50 | 51 | classMethods: { 52 | associate: (models) => { 53 | 54 | models.seat.belongsTo(models.house, { 55 | as: 'house', 56 | constraints: false 57 | }) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/mock/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.attributes = (DataTypes) => { 4 | 5 | return { 6 | id: { 7 | type: DataTypes.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true 10 | }, 11 | 12 | name: { 13 | type: DataTypes.STRING(100), 14 | allowNull: false, 15 | defaultValue: '' 16 | }, 17 | 18 | nickname: { 19 | type: DataTypes.STRING(100), 20 | allowNull: false, 21 | defaultValue: '' 22 | }, 23 | 24 | deleted_at: { 25 | type: DataTypes.DATE, 26 | allowNull: false, 27 | /** 28 | * if this type is DATE, 29 | * defaultValue must be a Date, 30 | * otherwise paranoid is useless 31 | */ 32 | defaultValue: new Date(0) 33 | } 34 | } 35 | } 36 | 37 | module.exports.options = { 38 | 39 | indexes: [{ 40 | type: 'unique', 41 | /* Name is important for unique index */ 42 | name: 'user_name_unique', 43 | fields: ['name'] 44 | }], 45 | 46 | classMethods: { 47 | associate: (models) => { 48 | 49 | models.user.belongsToMany(models.character, { 50 | as: 'characters', 51 | constraints: false, 52 | through: { 53 | model: models.user_characters, 54 | }, 55 | foreignKey: 'user_id', 56 | otherKey: 'character_id' 57 | }) 58 | 59 | models.user.belongsToMany(models.character, { 60 | as: 'partialities', 61 | constraints: false, 62 | through: { 63 | model: models.user_characters, 64 | scope: { 65 | rate: { 66 | $gt: 0 67 | } 68 | } 69 | }, 70 | foreignKey: 'user_id', 71 | otherKey: 'character_id' 72 | }) 73 | 74 | models.user.belongsToMany(models.character, { 75 | as: 'pests', 76 | constraints: false, 77 | through: { 78 | model: models.user_characters, 79 | scope: { 80 | rate: { 81 | $lte: 0 82 | } 83 | } 84 | }, 85 | foreignKey: 'user_id', 86 | otherKey: 'character_id' 87 | }) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/mock/models/user_characters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.attributes = (DataTypes) => { 4 | return { 5 | id : { 6 | type: DataTypes.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | 11 | user_id : { 12 | type: DataTypes.INTEGER, 13 | allowNull: false, 14 | defaultValue: 0 15 | }, 16 | 17 | character_id : { 18 | type: DataTypes.INTEGER, 19 | allowNull: false, 20 | defaultValue: 0 21 | }, 22 | 23 | rate : { 24 | type: DataTypes.INTEGER, 25 | allowNull: false, 26 | defaultValue: 0 27 | }, 28 | 29 | deleted_at: { 30 | type: DataTypes.DATE, 31 | allowNull: false, 32 | /** 33 | * if this type is DATE, 34 | * defaultValue must be a Date, 35 | * otherwise paranoid is useless 36 | */ 37 | defaultValue: new Date(0) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/models.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:models') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const models = prepare.sequelize.models 16 | 17 | describe ('model routers', function () { 18 | 19 | let server 20 | 21 | before (function () { 22 | 23 | let app = koa() 24 | , restql = new RestQL(models) 25 | 26 | app.use(restql.routes()) 27 | server = request(http.createServer(app.callback())) 28 | 29 | }) 30 | 31 | beforeEach (function (done) { 32 | 33 | debug('reset db') 34 | prepare.loadMockData().then(() => { 35 | done() 36 | }).catch(done) 37 | 38 | }) 39 | 40 | describe ('user | with single field unique index', function () { 41 | 42 | const model = models.user 43 | 44 | it ('should return 200 | get /user', function (done) { 45 | 46 | server 47 | .get('/user') 48 | .expect(200) 49 | .end((err, res) => { 50 | 51 | if (err) return done(err) 52 | let body = res.body 53 | assert(Array.isArray(body)) 54 | debug(body) 55 | assert(body.length === 2) 56 | done() 57 | 58 | }) 59 | 60 | }) 61 | 62 | it ('should return 201 | post /user, object body', function (done) { 63 | 64 | const data = { 65 | name : 'Li Xin', 66 | nickname : 'xt' 67 | } 68 | 69 | server 70 | .post(`/user`) 71 | .send(data) 72 | .expect(201) 73 | .end((err, res) => { 74 | if (err) return done(err) 75 | let body = res.body 76 | assert(typeof body === 'object') 77 | debug(body) 78 | 79 | test.assertObject(body, data) 80 | test.assertModelById(model, body.id, data, done).catch(done) 81 | }) 82 | }) 83 | 84 | it ('should return 201 | post /user, object body, with object include create new character', function (done) { 85 | 86 | const data = { 87 | name : 'Li Xin', 88 | nickname : 'xt', 89 | characters: { name: uuid() } 90 | } 91 | 92 | server 93 | .post(`/user`) 94 | .send(data) 95 | .expect(201) 96 | .end((err, res) => { 97 | if (err) return done(err) 98 | let body = res.body 99 | assert(typeof body === 'object') 100 | debug(body) 101 | assert(body.id) 102 | assert(Array.isArray(body.characters)) 103 | assert(body.characters) 104 | 105 | delete data.characters 106 | 107 | test.assertObject(body, data) 108 | test.assertModelById(model, body.id, data, done).catch(done) 109 | }) 110 | 111 | }) 112 | 113 | it ('should return 409 | post /user, object body, with object include', function (done) { 114 | 115 | models.character.findAll().then(res => ({ 116 | name : 'Li Xin', 117 | nickname : 'xt', 118 | characters: res[0] 119 | })).then(data => { 120 | 121 | server 122 | .post(`/user`) 123 | .send(data) 124 | .expect(409) 125 | .end(done) 126 | 127 | }) 128 | 129 | }) 130 | 131 | it ('should return 201 | post /user, object body, with array include', function (done) { 132 | 133 | const data = { 134 | name : 'Li Xin', 135 | nickname : 'xt', 136 | characters: [ 137 | { name: uuid() } 138 | ] 139 | } 140 | 141 | server 142 | .post(`/user`) 143 | .send(data) 144 | .expect(201) 145 | .end((err, res) => { 146 | if (err) return done(err) 147 | let body = res.body 148 | assert(typeof body === 'object') 149 | debug(body) 150 | assert(body.id) 151 | assert(Array.isArray(body.characters)) 152 | assert(body.characters) 153 | 154 | delete data.characters 155 | 156 | test.assertObject(body, data) 157 | test.assertModelById(model, body.id, data, done).catch(done) 158 | }) 159 | 160 | }) 161 | 162 | it ('should return 201 | post /user, array body', function (done) { 163 | 164 | const data = [{ 165 | name: 'Li Xin' 166 | }, { 167 | name: 'yadan' 168 | }] 169 | 170 | server 171 | .post(`/user`) 172 | .send(data) 173 | .expect(201) 174 | .end((err, res) => { 175 | if (err) return done(err) 176 | let body = res.body 177 | assert(Array.isArray(body)) 178 | debug(body) 179 | 180 | let promises = data.map((row, index) => { 181 | test.assertObject(body[index], row) 182 | return test.assertModelById(model, body[index].id, row) 183 | }) 184 | 185 | Promise.all(promises) 186 | .then(() => done()) 187 | .catch(done) 188 | }) 189 | }) 190 | 191 | it ('should return 201 | put /user, object body', function (done) { 192 | 193 | const data = { 194 | name : 'Li Xin', 195 | nickname : 'xt' 196 | } 197 | 198 | server 199 | .put(`/user`) 200 | .send(data) 201 | .expect(201) 202 | .end((err, res) => { 203 | if (err) return done(err) 204 | let body = res.body 205 | assert(typeof body === 'object') 206 | debug(body) 207 | 208 | test.assertObject(body, data) 209 | test.assertModelById(model, body.id, data, done).catch(done) 210 | }) 211 | }) 212 | 213 | it ('should return 201 | put /user, object body, without name', function (done) { 214 | 215 | const data = { 216 | nickname : 'xt', 217 | characters: [] 218 | } 219 | 220 | server 221 | .put(`/user`) 222 | .send(data) 223 | .expect(201) 224 | .end((err, res) => { 225 | if (err) return done(err) 226 | let body = res.body 227 | assert(typeof body === 'object') 228 | debug(body) 229 | 230 | delete data.characters 231 | test.assertObject(body, data) 232 | test.assertModelById(model, body.id, data, done).catch(done) 233 | }) 234 | }) 235 | 236 | it ('should return 200 | put /user, object body', function (done) { 237 | 238 | const id = 2 239 | 240 | models.user.findById(id).then(data => { 241 | 242 | data = data.dataValues 243 | delete data.created_at 244 | delete data.updated_at 245 | delete data.deleted_at 246 | 247 | data.nickname = uuid() 248 | 249 | server 250 | .put(`/user`) 251 | .send(data) 252 | .expect(200) 253 | .end((err, res) => { 254 | if (err) return done(err) 255 | let body = res.body 256 | assert(typeof body === 'object') 257 | debug(body) 258 | 259 | test.assertObject(body, data) 260 | test.assertModelById(model, body.id, data, done).catch(done) 261 | }) 262 | 263 | }) 264 | 265 | }) 266 | 267 | it ('should return 201 | put /user, array body', function (done) { 268 | 269 | const data = [{ 270 | name: 'Li Xin' 271 | }, { 272 | name: 'yadan' 273 | }] 274 | 275 | server 276 | .put(`/user`) 277 | .send(data) 278 | .expect(200) 279 | .end((err, res) => { 280 | if (err) return done(err) 281 | let body = res.body 282 | assert(Array.isArray(body)) 283 | debug(body) 284 | 285 | let promises = data.map((row, index) => { 286 | test.assertObject(body[index], row) 287 | return test.assertModelById(model, body[index].id, row) 288 | }) 289 | 290 | Promise.all(promises) 291 | .then(() => done()) 292 | .catch(done) 293 | }) 294 | }) 295 | 296 | it ('should return 204 | delete /user', function (done) { 297 | 298 | const where = { 299 | $or: [{ id: 1 }, { id: 2 }] 300 | } 301 | 302 | const querystring = qs.stringify(where) 303 | 304 | server 305 | .del(`/user?${querystring}`) 306 | .expect(204) 307 | .end((err, res) => { 308 | 309 | if (err) return done(err) 310 | models.user.findAll({ 311 | where 312 | }).then(data => { 313 | assert(!data.length) 314 | done() 315 | }).catch(done) 316 | 317 | }) 318 | 319 | }) 320 | 321 | describe ('unique key constraint error', function () { 322 | 323 | it ('should return 409 | post /user, object body', function (done) { 324 | 325 | const id = 1 326 | 327 | models.user.findById(id).then(data => { 328 | 329 | data = data.dataValues 330 | delete data.id 331 | delete data.created_at 332 | delete data.updated_at 333 | delete data.deleted_at 334 | 335 | server 336 | .post(`/user`) 337 | .send(data) 338 | .expect(409) 339 | .end(done) 340 | }) 341 | 342 | }) 343 | 344 | it ('should return 409 | post /user, array body', function (done) { 345 | 346 | const ids = [1, 2] 347 | 348 | models.user.findAll({ 349 | where: { 350 | id: ids 351 | } 352 | }).then(data => { 353 | 354 | data = data.map(row => { 355 | row = row.dataValues 356 | delete row.id 357 | delete row.created_at 358 | delete row.updated_at 359 | delete row.deleted_at 360 | return row 361 | }) 362 | 363 | server 364 | .post(`/user`) 365 | .send(data) 366 | .expect(409) 367 | .end(done) 368 | }) 369 | 370 | }) 371 | 372 | it ('should return 201 | post /user, object body', function (done) { 373 | 374 | const id = 2 375 | 376 | models.user.findById(id).then(data => { 377 | return models.user.destroy({ 378 | where: { 379 | id: data.id 380 | } 381 | }).then(() => { 382 | return models.user.findById(id) 383 | }).then(res => { 384 | assert(!res) 385 | return data 386 | }) 387 | }).then(data => { 388 | 389 | data = data.dataValues 390 | delete data.created_at 391 | delete data.updated_at 392 | delete data.deleted_at 393 | 394 | data.nickname = uuid() 395 | debug(data) 396 | 397 | server 398 | .post(`/user`) 399 | .send(data) 400 | .expect(201) 401 | .end((err, res) => { 402 | if (err) return done(err) 403 | let body = res.body 404 | assert(typeof body === 'object') 405 | debug(body) 406 | 407 | test.assertObject(body, data) 408 | test.assertModelById(model, body.id, data, done).catch(done) 409 | }) 410 | }) 411 | 412 | }) 413 | 414 | it ('should return 201 | post /user, array body', function (done) { 415 | 416 | const ids = [2] 417 | 418 | models.user.findAll({ 419 | where: { 420 | id: ids 421 | } 422 | }).then(data => { 423 | return models.user.destroy({ 424 | where: { 425 | id: ids 426 | } 427 | }).then(() => { 428 | return models.user.findAll({ 429 | where: { 430 | id: ids 431 | } 432 | }) 433 | }).then(res => { 434 | assert(!res.length) 435 | return data 436 | }) 437 | }).then(data => { 438 | 439 | data = data.map(row => { 440 | row = row.dataValues 441 | row.nickname = uuid() 442 | delete row.id 443 | delete row.created_at 444 | delete row.updated_at 445 | delete row.deleted_at 446 | return row 447 | }) 448 | 449 | server 450 | .post(`/user`) 451 | .send(data) 452 | .expect(201) 453 | .end((err, res) => { 454 | if (err) return done(err) 455 | let body = res.body 456 | assert(Array.isArray(body)) 457 | debug(body) 458 | 459 | let promises = data.map((row, index) => { 460 | test.assertObject(body[index], row) 461 | return test.assertModelById(model, body[index].id, row) 462 | }) 463 | 464 | Promise.all(promises) 465 | .then(() => done()) 466 | .catch(done) 467 | }) 468 | }) 469 | 470 | }) 471 | 472 | }) 473 | 474 | it ('should return 200 | get /user/:id', function (done) { 475 | 476 | const id = 1 477 | 478 | server 479 | .get(`/user/${id}`) 480 | .expect(200) 481 | .end((err, res) => { 482 | if (err) return done(err) 483 | let body = res.body 484 | assert(typeof body === 'object') 485 | debug(body) 486 | assert(body.id === id) 487 | done() 488 | }) 489 | }) 490 | 491 | it ('should return 404 | get /user/:id', function (done) { 492 | 493 | const id = 100 494 | 495 | server 496 | .get(`/user/${id}`) 497 | .expect(404) 498 | .end(done) 499 | 500 | }) 501 | 502 | it ('should return 200 | put /user/:id', function (done) { 503 | 504 | const id = 1 505 | 506 | const data = { 507 | id: id, 508 | nickname: uuid() 509 | } 510 | 511 | server 512 | .put(`/user/${id}`) 513 | .send(data) 514 | .expect(200) 515 | .end((err, res) => { 516 | if (err) return done(err) 517 | let body = res.body 518 | assert(typeof body === 'object') 519 | debug(body) 520 | 521 | test.assertObject(body, data) 522 | test.assertModelById(model, body.id, data, done).catch(done) 523 | }) 524 | }) 525 | 526 | it ('should return 204 | delete /user/:id', function (done) { 527 | 528 | const id = 2 529 | 530 | server 531 | .del(`/user/${id}`) 532 | .expect(204) 533 | .end((err, res) => { 534 | 535 | if (err) return done(err) 536 | 537 | models.user.findById(id).then(data => { 538 | assert(!data) 539 | done() 540 | }).catch(done) 541 | 542 | }) 543 | 544 | }) 545 | 546 | }) 547 | 548 | describe ('user_characters | with multi-field unique index', function () { 549 | 550 | const model = models.user_characters 551 | 552 | describe ('unique key constraint error', function () { 553 | 554 | it ('should return 409 | post /user_characters, object body', function (done) { 555 | 556 | const id = 1 557 | 558 | model.findById(id).then(data => { 559 | 560 | data = data.dataValues 561 | delete data.id 562 | delete data.created_at 563 | delete data.updated_at 564 | delete data.deleted_at 565 | 566 | server 567 | .post(`/user_characters`) 568 | .send(data) 569 | .expect(409) 570 | .end(done) 571 | }) 572 | 573 | }) 574 | 575 | it ('should return 409 | post /user_characters, array body', function (done) { 576 | 577 | const ids = [1, 2] 578 | 579 | model.findAll({ 580 | where: { 581 | id: ids 582 | } 583 | }).then(data => { 584 | 585 | data = data.map(row => { 586 | row = row.dataValues 587 | delete row.id 588 | delete row.created_at 589 | delete row.updated_at 590 | delete row.deleted_at 591 | return row 592 | }) 593 | 594 | server 595 | .post(`/user_characters`) 596 | .send(data) 597 | .expect(409) 598 | .end(done) 599 | }) 600 | 601 | }) 602 | 603 | it ('should return 201 | post /user_characters, object body', function (done) { 604 | 605 | const id = 2 606 | 607 | model.findById(id).then(data => { 608 | return model.destroy({ 609 | where: { 610 | id: data.id 611 | } 612 | }).then(() => { 613 | return model.findById(id) 614 | }).then(res => { 615 | assert(!res) 616 | return data 617 | }) 618 | }).then(data => { 619 | 620 | data = data.dataValues 621 | delete data.created_at 622 | delete data.updated_at 623 | delete data.deleted_at 624 | 625 | data.rate = 0 626 | debug(data) 627 | 628 | server 629 | .post(`/user_characters`) 630 | .send(data) 631 | .expect(201) 632 | .end((err, res) => { 633 | if (err) return done(err) 634 | let body = res.body 635 | assert(typeof body === 'object') 636 | debug(body) 637 | 638 | test.assertObject(body, data) 639 | test.assertModelById(model, id, data, done) 640 | .catch(done) 641 | }) 642 | }) 643 | 644 | }) 645 | 646 | it ('should return 201 | post /user_characters, array body', function (done) { 647 | 648 | const ids = [2] 649 | 650 | model.findAll({ 651 | where: { 652 | id: ids 653 | } 654 | }).then(data => { 655 | return model.destroy({ 656 | where: { 657 | id: ids 658 | } 659 | }).then(() => { 660 | return model.findAll({ 661 | where: { 662 | id: ids 663 | } 664 | }) 665 | }).then(res => { 666 | assert(!res.length) 667 | return data 668 | }) 669 | }).then(data => { 670 | 671 | data = data.map(row => { 672 | row = row.dataValues 673 | row.rate = 0 674 | delete row.id 675 | delete row.created_at 676 | delete row.updated_at 677 | delete row.deleted_at 678 | return row 679 | }) 680 | 681 | debug(data) 682 | 683 | server 684 | .post(`/user_characters`) 685 | .send(data) 686 | .expect(201) 687 | .end((err, res) => { 688 | if (err) return done(err) 689 | let body = res.body 690 | assert(Array.isArray(body)) 691 | debug(body) 692 | 693 | let promises = data.map((row, index) => { 694 | test.assertObject(body[index], row) 695 | return test.assertModelById(model, body[index].id, row) 696 | }) 697 | 698 | Promise.all(promises) 699 | .then(() => done()) 700 | .catch(done) 701 | }) 702 | }) 703 | 704 | }) 705 | 706 | }) 707 | 708 | }) 709 | 710 | describe ('characters | with schema', function () { 711 | 712 | const model = models.character 713 | 714 | it ('should return 200 | get /gameofthrones/character', function (done) { 715 | 716 | model.count().then(count => { 717 | 718 | server 719 | .get('/gameofthrones/character') 720 | .expect(200) 721 | .end((err, res) => { 722 | 723 | if (err) return done(err) 724 | let body = res.body 725 | assert(Array.isArray(body)) 726 | debug(body) 727 | assert(body.length === count) 728 | done() 729 | 730 | }) 731 | }).catch(done) 732 | 733 | }) 734 | 735 | }) 736 | 737 | }) 738 | -------------------------------------------------------------------------------- /test/query-attributes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:query') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const models = prepare.sequelize.models 16 | 17 | describe ('attributes', function () { 18 | 19 | let server 20 | 21 | before (function () { 22 | 23 | let app = koa() 24 | , restql = new RestQL(models) 25 | 26 | app.use(restql.routes()) 27 | server = request(http.createServer(app.callback())) 28 | 29 | }) 30 | 31 | beforeEach (function (done) { 32 | 33 | debug('reset db') 34 | prepare.loadMockData().then(() => { 35 | done() 36 | }).catch(done) 37 | 38 | }) 39 | 40 | it ('should return 200 | get /user, with attributes', function (done) { 41 | 42 | const querystring = qs.stringify({ 43 | _attributes: [ 'id', 'name' ] 44 | }) 45 | 46 | const id = 1 47 | 48 | server 49 | .get(`/user?${querystring}`) 50 | .expect(200) 51 | .end((err, res) => { 52 | 53 | if (err) return done(err) 54 | let body = res.body 55 | assert(Array.isArray(body)) 56 | debug(body) 57 | 58 | body.forEach(user => { 59 | debug(user) 60 | assert(user.id) 61 | assert(user.name) 62 | }) 63 | 64 | done() 65 | 66 | }) 67 | 68 | 69 | }) 70 | 71 | it ('should return 200 | get /user/:id/characters, with attributes', function (done) { 72 | 73 | const querystring = qs.stringify({ 74 | _attributes: [ 'id', 'name' ] 75 | }) 76 | 77 | const id = 1 78 | 79 | models.user.findById(id).then(user => { 80 | 81 | server 82 | .get(`/user/${user.id}/characters?${querystring}`) 83 | .expect(200) 84 | .end((err, res) => { 85 | 86 | if (err) return done(err) 87 | let body = res.body 88 | assert(Array.isArray(body)) 89 | debug(body) 90 | 91 | body.forEach(character=> { 92 | debug(character) 93 | assert(character.id) 94 | assert(character.name) 95 | assert(!character.house_id) 96 | assert(character.is_bastard === undefined) 97 | }) 98 | 99 | done() 100 | 101 | }) 102 | 103 | }).catch(done) 104 | 105 | 106 | }) 107 | 108 | }) 109 | -------------------------------------------------------------------------------- /test/query-group.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:query') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const models = prepare.sequelize.models 16 | 17 | describe ('group', function () { 18 | 19 | let server 20 | 21 | before (function () { 22 | 23 | let app = koa() 24 | , restql = new RestQL(models) 25 | 26 | app.use(restql.routes()) 27 | server = request(http.createServer(app.callback())) 28 | 29 | }) 30 | 31 | beforeEach (function (done) { 32 | 33 | debug('reset db') 34 | prepare.loadMockData().then(() => { 35 | done() 36 | }).catch(done) 37 | 38 | }) 39 | 40 | it ('should return 200 | get /character, group = house_id', function (done) { 41 | 42 | const querystring = qs.stringify({ 43 | _group: ['house_id'] 44 | }) 45 | 46 | server 47 | .get(`/gameofthrones/character?${querystring}`) 48 | .expect(200) 49 | .expect('X-Range', `objects 0-5/5`) 50 | .end((err, res) => { 51 | 52 | debug(res.headers) 53 | if (err) return done(err) 54 | let body = res.body 55 | assert(Array.isArray(body)) 56 | debug(body) 57 | assert(body.length === 5) 58 | 59 | done() 60 | 61 | }) 62 | 63 | }) 64 | 65 | it ('should return 200 | get /character, group = house_id, with count', function (done) { 66 | 67 | const querystring = qs.stringify({ 68 | _attributes: [ 69 | [ 70 | 'count(`character`.`id`)', 71 | 'count' 72 | ] 73 | ], 74 | _group: ['house_id'] 75 | }) 76 | 77 | server 78 | .get(`/gameofthrones/character?${querystring}`) 79 | .expect(200) 80 | .expect('X-Range', `objects 0-5/5`) 81 | .end((err, res) => { 82 | 83 | debug(res.headers) 84 | if (err) return done(err) 85 | let body = res.body 86 | assert(Array.isArray(body)) 87 | debug(body) 88 | assert(body.length === 5) 89 | body.forEach(row => assert(row.count)) 90 | 91 | done() 92 | 93 | }) 94 | 95 | }) 96 | 97 | it ('should return 200 | get /seat, include house, group = house_id having = 1', function (done) { 98 | 99 | const querystring = qs.stringify({ 100 | _group: ['house_id'], 101 | _having: { 102 | house_id: 1 103 | } 104 | }) 105 | 106 | server 107 | .get(`/gameofthrones/character?${querystring}`) 108 | .expect(200) 109 | .expect('X-Range', `objects 0-1/1`) 110 | .end((err, res) => { 111 | 112 | debug(res.headers) 113 | if (err) return done(err) 114 | let body = res.body 115 | assert(Array.isArray(body)) 116 | debug(body) 117 | 118 | assert(body.length === 1) 119 | 120 | done() 121 | 122 | }) 123 | 124 | }) 125 | 126 | it ('should return 200 | get /user/1/characters, group = house_id', function (done) { 127 | 128 | const id = 1 129 | 130 | const querystring = qs.stringify({ 131 | _group: ['house_id'], 132 | _limit: 3 133 | }) 134 | 135 | server 136 | .get(`/user/${id}/characters?${querystring}`) 137 | .expect(206) 138 | .expect('X-Range', `objects 0-3/4`) 139 | .end((err, res) => { 140 | 141 | debug(res.headers) 142 | if (err) return done(err) 143 | let body = res.body 144 | assert(Array.isArray(body)) 145 | debug(body) 146 | 147 | assert(body.length === 3) 148 | 149 | done() 150 | 151 | }) 152 | 153 | }) 154 | 155 | 156 | }) 157 | -------------------------------------------------------------------------------- /test/query-include.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:query') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const models = prepare.sequelize.models 16 | 17 | describe ('include', function () { 18 | 19 | let server 20 | 21 | before (function () { 22 | 23 | let app = koa() 24 | , restql = new RestQL(models) 25 | 26 | app.use(restql.routes()) 27 | server = request(http.createServer(app.callback())) 28 | 29 | }) 30 | 31 | beforeEach (function (done) { 32 | 33 | debug('reset db') 34 | prepare.loadMockData().then(() => { 35 | done() 36 | }).catch(done) 37 | 38 | }) 39 | 40 | it ('should return 200 | get /user, include characters, string', function (done) { 41 | 42 | const querystring = qs.stringify({ 43 | _include: 'characters' 44 | }) 45 | 46 | server 47 | .get(`/user?${querystring}`) 48 | .expect(200) 49 | .end((err, res) => { 50 | 51 | if (err) return done(err) 52 | let body = res.body 53 | assert(Array.isArray(body)) 54 | debug(body) 55 | 56 | body.forEach(row => { 57 | assert(row.id) 58 | assert(Array.isArray(row.characters)) 59 | }) 60 | 61 | done() 62 | 63 | }) 64 | 65 | }) 66 | 67 | it ('should return 200 | get /user, include characters, object', function (done) { 68 | 69 | const querystring = qs.stringify({ 70 | _include: { 71 | association: 'characters' 72 | } 73 | }) 74 | 75 | server 76 | .get(`/user?${querystring}`) 77 | .expect(200) 78 | .end((err, res) => { 79 | 80 | if (err) return done(err) 81 | let body = res.body 82 | assert(Array.isArray(body)) 83 | debug(body) 84 | 85 | body.forEach(row => { 86 | assert(row.id) 87 | assert(Array.isArray(row.characters)) 88 | row.characters.forEach(character => { 89 | assert(character.user_characters) 90 | }) 91 | }) 92 | 93 | done() 94 | 95 | }) 96 | 97 | }) 98 | 99 | it ('should return 200 | get /user, include characters, array', function (done) { 100 | 101 | const querystring = qs.stringify({ 102 | _include: [{ 103 | association: 'characters' 104 | }] 105 | }) 106 | 107 | server 108 | .get(`/user?${querystring}`) 109 | .expect(200) 110 | .end((err, res) => { 111 | 112 | if (err) return done(err) 113 | let body = res.body 114 | assert(Array.isArray(body)) 115 | debug(body) 116 | 117 | body.forEach(row => { 118 | assert(row.id) 119 | assert(Array.isArray(row.characters)) 120 | }) 121 | 122 | done() 123 | 124 | }) 125 | 126 | }) 127 | 128 | it ('should return 200 | get /user, include characters, with attributes', function (done) { 129 | 130 | const querystring = qs.stringify({ 131 | _include: [{ 132 | association: 'characters', 133 | attributes: ['id', 'name'] 134 | }] 135 | }) 136 | 137 | server 138 | .get(`/user?${querystring}`) 139 | .expect(200) 140 | .end((err, res) => { 141 | 142 | if (err) return done(err) 143 | let body = res.body 144 | assert(Array.isArray(body)) 145 | debug(body) 146 | 147 | body.forEach(row => { 148 | assert(row.id) 149 | assert(Array.isArray(row.characters)) 150 | row.characters.forEach(character => { 151 | debug(character) 152 | assert(character.id) 153 | assert(character.name) 154 | assert(!character.house_id) 155 | }) 156 | }) 157 | 158 | done() 159 | 160 | }) 161 | 162 | }) 163 | 164 | it ('should return 200 | get /user, include characters, with through', function (done) { 165 | 166 | const querystring = qs.stringify({ 167 | _include: [{ 168 | association: 'characters', 169 | through: { 170 | where: { 171 | rate: { 172 | $gt: 0 173 | } 174 | } 175 | } 176 | }] 177 | }) 178 | 179 | server 180 | .get(`/user?${querystring}`) 181 | .expect(200) 182 | .end((err, res) => { 183 | 184 | if (err) return done(err) 185 | let body = res.body 186 | assert(Array.isArray(body)) 187 | debug(body) 188 | 189 | body.forEach(row => { 190 | assert(row.id) 191 | assert(Array.isArray(row.characters)) 192 | row.characters.forEach(character => { 193 | debug(character) 194 | assert(character.id) 195 | assert(character.name) 196 | assert(character.user_characters) 197 | assert(character.user_characters.rate > 0) 198 | }) 199 | }) 200 | 201 | done() 202 | 203 | }) 204 | 205 | }) 206 | 207 | it ('should return 200 | get /user, include characters, nest include house', function (done) { 208 | 209 | const querystring = qs.stringify({ 210 | _include: [{ 211 | association: 'characters', 212 | include: 'house' 213 | }] 214 | }) 215 | 216 | server 217 | .get(`/user?${querystring}`) 218 | .expect(200) 219 | .end((err, res) => { 220 | 221 | if (err) return done(err) 222 | let body = res.body 223 | assert(Array.isArray(body)) 224 | debug(body) 225 | 226 | body.forEach(row => { 227 | assert(row.id) 228 | assert(Array.isArray(row.characters)) 229 | row.characters.forEach(character => { 230 | debug(character) 231 | assert(character.house) 232 | }) 233 | }) 234 | 235 | done() 236 | 237 | }) 238 | 239 | }) 240 | 241 | it ('should return 200 | get /house, include members, with require = true', function (done) { 242 | 243 | const user = { 244 | name: uuid() 245 | } 246 | 247 | const querystring = qs.stringify({ 248 | _include: [{ 249 | association: 'members', 250 | required: 1 251 | }], 252 | // it is important 253 | _distinct: 1 254 | }) 255 | 256 | models.user.create(user).then(user => { 257 | 258 | server 259 | .get(`/gameofthrones/house?${querystring}`) 260 | .expect(200) 261 | .expect('X-Range', 'objects 0-4/4') 262 | .end((err, res) => { 263 | 264 | if (err) return done(err) 265 | let body = res.body 266 | debug(res.headers) 267 | assert(Array.isArray(body)) 268 | debug(body) 269 | 270 | body.forEach(row => { 271 | assert(row.id) 272 | assert(Array.isArray(row.members)) 273 | assert(row.members.length) 274 | }) 275 | 276 | done() 277 | 278 | }) 279 | 280 | }).catch(done) 281 | 282 | }) 283 | 284 | it ('should return 200 | get /house, include members, with require = false', function (done) { 285 | 286 | const user = { 287 | name: uuid() 288 | } 289 | 290 | const querystring = qs.stringify({ 291 | _include: [{ 292 | association: 'members', 293 | }], 294 | // it is important 295 | _distinct: 1 296 | }) 297 | 298 | models.user.create(user).then(user => { 299 | 300 | server 301 | .get(`/gameofthrones/house?${querystring}`) 302 | .expect(200) 303 | .expect('X-Range', 'objects 0-5/5') 304 | .end((err, res) => { 305 | 306 | if (err) return done(err) 307 | let body = res.body 308 | debug(res.headers) 309 | assert(Array.isArray(body)) 310 | debug(body) 311 | 312 | done() 313 | 314 | }) 315 | 316 | }).catch(done) 317 | 318 | }) 319 | 320 | it ('should return 200 | get /user, include characters, with where', function (done) { 321 | 322 | const user = { 323 | name: uuid() 324 | } 325 | 326 | const querystring = qs.stringify({ 327 | _include: [{ 328 | association: 'characters', 329 | where: { 330 | house_id: 1 331 | } 332 | }] 333 | }) 334 | 335 | models.user.create(user).then(user => { 336 | 337 | server 338 | .get(`/user?${querystring}`) 339 | .expect(200) 340 | .end((err, res) => { 341 | 342 | if (err) return done(err) 343 | let body = res.body 344 | assert(Array.isArray(body)) 345 | debug(body) 346 | 347 | body.forEach(row => { 348 | assert(row.id) 349 | assert(Array.isArray(row.characters)) 350 | row.characters.forEach(character => { 351 | debug(character) 352 | assert(character.house_id === 1) 353 | }) 354 | }) 355 | 356 | done() 357 | 358 | }) 359 | 360 | }).catch(done) 361 | 362 | }) 363 | 364 | 365 | it ('should return 200 | get /house, include seat and characters', function (done) { 366 | 367 | const querystring = qs.stringify({ 368 | _include: [ 369 | 'members', 'seat' 370 | ] 371 | }) 372 | 373 | server 374 | .get(`/gameofthrones/house?${querystring}`) 375 | .expect(200) 376 | .end((err, res) => { 377 | 378 | if (err) return done(err) 379 | let body = res.body 380 | assert(Array.isArray(body)) 381 | debug(body) 382 | 383 | assert(body.some(row => { 384 | return row.id && Array.isArray(row.members) && row.seat 385 | })) 386 | 387 | done() 388 | 389 | }) 390 | 391 | }) 392 | 393 | it ('should return 200 | get /user/:id/characters, include house', function (done) { 394 | 395 | const id = 1 396 | 397 | const querystring = qs.stringify({ 398 | _include: [ 399 | 'house' 400 | ] 401 | }) 402 | 403 | server 404 | .get(`/user/${id}/characters?${querystring}`) 405 | .expect(200) 406 | .end((err, res) => { 407 | 408 | if (err) return done(err) 409 | let body = res.body 410 | assert(Array.isArray(body)) 411 | debug(body) 412 | 413 | assert(body.every(row => row.id && row.house)) 414 | 415 | done() 416 | 417 | }) 418 | 419 | }) 420 | 421 | it ('should return 200 | get /seat/:id/house, include members', function (done) { 422 | 423 | const id = 1 424 | 425 | const querystring = qs.stringify({ 426 | _include: [ 427 | 'members' 428 | ] 429 | }) 430 | 431 | server 432 | .get(`/gameofthrones/seat/${id}/house?${querystring}`) 433 | .expect(200) 434 | .end((err, res) => { 435 | 436 | if (err) return done(err) 437 | let body = res.body 438 | debug(body) 439 | 440 | assert(body.id) 441 | assert(body.members) 442 | assert(Array.isArray(body.members)) 443 | 444 | done() 445 | 446 | }) 447 | 448 | }) 449 | 450 | }) 451 | -------------------------------------------------------------------------------- /test/query-order.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:query') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const models = prepare.sequelize.models 16 | 17 | describe ('order', function () { 18 | 19 | let server 20 | 21 | before (function () { 22 | 23 | let app = koa() 24 | , restql = new RestQL(models) 25 | 26 | app.use(restql.routes()) 27 | server = request(http.createServer(app.callback())) 28 | 29 | }) 30 | 31 | beforeEach (function (done) { 32 | 33 | debug('reset db') 34 | prepare.loadMockData().then(() => { 35 | done() 36 | }).catch(done) 37 | 38 | }) 39 | 40 | it ('should return 200 | get /user, with order', function (done) { 41 | 42 | const order = [['id', 'DESC']] 43 | 44 | const querystring = qs.stringify({ 45 | _order: order 46 | }) 47 | 48 | server 49 | .get(`/user?${querystring}`) 50 | .expect(200) 51 | .end((err, res) => { 52 | 53 | if (err) return done(err) 54 | let body = res.body 55 | assert(Array.isArray(body)) 56 | debug(body) 57 | 58 | models.user.findAll({ order }).then(users => { 59 | 60 | assert(body.length === users.length) 61 | 62 | users.forEach((user, index) => { 63 | assert(user.id === body[index].id) 64 | assert(user.name === body[index].name) 65 | }) 66 | 67 | done() 68 | 69 | }) 70 | 71 | }) 72 | 73 | }) 74 | 75 | it ('should return 200 | get /user, with order', function (done) { 76 | 77 | const order = 'id DESC' 78 | 79 | const querystring = qs.stringify({ 80 | _order: order 81 | }) 82 | 83 | server 84 | .get(`/user?${querystring}`) 85 | .expect(200) 86 | .end((err, res) => { 87 | 88 | if (err) return done(err) 89 | let body = res.body 90 | assert(Array.isArray(body)) 91 | debug(body) 92 | 93 | models.user.findAll({ order }).then(users => { 94 | 95 | assert(body.length === users.length) 96 | 97 | users.forEach((user, index) => { 98 | assert(user.id === body[index].id) 99 | assert(user.name === body[index].name) 100 | }) 101 | 102 | done() 103 | 104 | }) 105 | 106 | }) 107 | 108 | }) 109 | 110 | it ('should return 200 | get /user/:id/characters, with order', function (done) { 111 | 112 | const order = [['id', 'DESC']] 113 | 114 | const querystring = qs.stringify({ 115 | _order: order 116 | }) 117 | 118 | const id = 1 119 | 120 | models.user.findById(id).then(user => { 121 | 122 | server 123 | .get(`/user/${user.id}/characters?${querystring}`) 124 | .expect(200) 125 | .end((err, res) => { 126 | 127 | if (err) return done(err) 128 | let body = res.body 129 | assert(Array.isArray(body)) 130 | debug(body) 131 | 132 | user.getCharacters({ order }).then(characters => { 133 | 134 | assert(body.length === characters.length) 135 | 136 | characters.forEach((character, index) => { 137 | assert(character.id === body[index].id) 138 | assert(character.name === body[index].name) 139 | }) 140 | 141 | done() 142 | 143 | }) 144 | 145 | }) 146 | 147 | }).catch(done) 148 | 149 | 150 | }) 151 | 152 | }) 153 | -------------------------------------------------------------------------------- /test/query-pagination.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:pagination') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const { 16 | sequelize, createMockData 17 | } = prepare 18 | 19 | const models = sequelize.models 20 | 21 | describe ('pagination', function () { 22 | 23 | let server 24 | 25 | before (function () { 26 | 27 | let app = koa() 28 | , restql = new RestQL(models) 29 | 30 | app.use(restql.routes()) 31 | server = request(http.createServer(app.callback())) 32 | 33 | }) 34 | 35 | describe ('model', function () { 36 | 37 | const model = models.user 38 | const count = 100 39 | 40 | before (function () { 41 | 42 | return prepare.reset().then(() => 43 | createMockData(model, ['name'], count)) 44 | 45 | }) 46 | 47 | it ('should return 206 | get /user', function (done) { 48 | 49 | server 50 | .get('/user') 51 | .expect(206) 52 | .expect('X-Range', 'objects 0-20/100') 53 | .end((err, res) => { 54 | 55 | if (err) return done(err) 56 | let body = res.body 57 | assert(Array.isArray(body)) 58 | debug(body) 59 | assert(body.length === 20) 60 | done() 61 | 62 | }) 63 | 64 | }) 65 | 66 | it ('should return 206 | get /user, with offset', function (done) { 67 | 68 | const querystring = qs.stringify({ 69 | _offset: 50 70 | }) 71 | 72 | server 73 | .get(`/user?${querystring}`) 74 | .expect(206) 75 | .expect('X-Range', 'objects 50-70/100') 76 | .end((err, res) => { 77 | 78 | if (err) return done(err) 79 | let body = res.body 80 | assert(Array.isArray(body)) 81 | debug(body) 82 | assert(body.length === 20) 83 | done() 84 | 85 | }) 86 | 87 | }) 88 | 89 | it ('should return 206 | get /user, with offset + limit > count', function (done) { 90 | 91 | const querystring = qs.stringify({ 92 | _offset: 90 93 | }) 94 | 95 | server 96 | .get(`/user?${querystring}`) 97 | .expect(206) 98 | .expect('X-Range', 'objects 90-100/100') 99 | .end((err, res) => { 100 | 101 | if (err) return done(err) 102 | let body = res.body 103 | assert(Array.isArray(body)) 104 | debug(body) 105 | assert(body.length === 10) 106 | done() 107 | 108 | }) 109 | 110 | }) 111 | 112 | it ('should return 200 | get /user, limit > count', function (done) { 113 | 114 | const querystring = qs.stringify({ 115 | _limit: 200 116 | }) 117 | 118 | server 119 | .get(`/user?${querystring}`) 120 | .expect(200) 121 | .end((err, res) => { 122 | 123 | if (err) return done(err) 124 | let body = res.body 125 | assert(Array.isArray(body)) 126 | debug(body) 127 | assert(body.length === count) 128 | done() 129 | 130 | }) 131 | 132 | }) 133 | 134 | it ('should return 200 | get /user, _limit = null', function (done) { 135 | 136 | const querystring = qs.stringify({ 137 | _limit: null 138 | }, { 139 | strictNullHandling : true 140 | }) 141 | 142 | server 143 | .get(`/user?${querystring}`) 144 | .expect(200) 145 | .end((err, res) => { 146 | 147 | if (err) return done(err) 148 | let body = res.body 149 | assert(Array.isArray(body)) 150 | debug(body) 151 | assert(body.length === count) 152 | done() 153 | 154 | }) 155 | 156 | }) 157 | 158 | }) 159 | 160 | describe ('model with plural association, with include', function () { 161 | 162 | beforeEach (function (done) { 163 | 164 | debug('reset db') 165 | prepare.loadMockData().then(() => { 166 | done() 167 | }).catch(done) 168 | 169 | }) 170 | 171 | it ('should return 206 | get /house include members', function (done) { 172 | 173 | const model = models.house 174 | const association = models.character 175 | 176 | const querystring = qs.stringify({ 177 | _include: [{ 178 | association: 'members', 179 | }] 180 | }) 181 | 182 | server 183 | .get(`/gameofthrones/house?${querystring}`) 184 | .expect(200) 185 | .expect('X-Range', 'objects 0-5/5') 186 | .end((err, res) => { 187 | 188 | if (err) return done(err) 189 | let body = res.body 190 | assert(Array.isArray(body)) 191 | debug(body) 192 | done() 193 | 194 | }) 195 | 196 | }) 197 | 198 | it ('should return 206 | get /house/:id/members include reviewers', function (done) { 199 | 200 | const id = 1 201 | 202 | const querystring = qs.stringify({ 203 | _include: [{ 204 | association: 'reviewers', 205 | }] 206 | }) 207 | 208 | server 209 | .get(`/gameofthrones/house/1/members?${querystring}`) 210 | .expect(200) 211 | .expect('X-Range', 'objects 0-2/2') 212 | .end((err, res) => { 213 | 214 | if (err) return done(err) 215 | let body = res.body 216 | assert(Array.isArray(body)) 217 | debug(body) 218 | done() 219 | 220 | }) 221 | 222 | }) 223 | 224 | it ('should return 206 | get /user/:id/character include reviewers', function (done) { 225 | 226 | const id = 1 227 | 228 | const querystring = qs.stringify({ 229 | _attributes: ['id'], 230 | _include: [{ 231 | association: 'reviewers', 232 | }] 233 | }) 234 | 235 | server 236 | .get(`/user/1/characters?${querystring}`) 237 | .expect(200) 238 | .expect('X-Range', 'objects 0-4/4') 239 | .end((err, res) => { 240 | 241 | if (err) return done(err) 242 | let body = res.body 243 | assert(Array.isArray(body)) 244 | assert(body.length === 4) 245 | debug(body) 246 | done() 247 | 248 | }) 249 | 250 | }) 251 | 252 | }) 253 | 254 | describe ('model with association', function () { 255 | 256 | const model = models.house 257 | const association = models.character 258 | 259 | const id = 1 260 | const count = 100 261 | 262 | before (function () { 263 | 264 | return prepare.reset().then(() => { 265 | return model.create({ 266 | name: uuid() 267 | }).then(house => { 268 | return createMockData(association, ['name'], count, { 269 | house_id: house.id 270 | }) 271 | }) 272 | }) 273 | 274 | }) 275 | 276 | it ('should return 206 | get /house/:id/members', function (done) { 277 | 278 | server 279 | .get(`/gameofthrones/house/${id}/members`) 280 | .expect(206) 281 | .expect('X-Range', 'objects 0-20/100') 282 | .end((err, res) => { 283 | 284 | if (err) return done(err) 285 | let body = res.body 286 | assert(Array.isArray(body)) 287 | debug(body) 288 | assert(body.length === 20) 289 | done() 290 | 291 | }) 292 | 293 | }) 294 | 295 | it ('should return 206 | get /house/:id/members, with offset', function (done) { 296 | 297 | const querystring = qs.stringify({ 298 | _offset: 20 299 | }) 300 | 301 | server 302 | .get(`/gameofthrones/house/${id}/members?${querystring}`) 303 | .expect(206) 304 | .expect('X-Range', 'objects 20-40/100') 305 | .end((err, res) => { 306 | 307 | if (err) return done(err) 308 | let body = res.body 309 | assert(Array.isArray(body)) 310 | debug(body) 311 | assert(body.length === 20) 312 | done() 313 | 314 | }) 315 | 316 | }) 317 | 318 | it ('should return 206 | get /house/:id/members, with offset + limit > count', function (done) { 319 | 320 | const querystring = qs.stringify({ 321 | _offset: 90 322 | }) 323 | 324 | server 325 | .get(`/gameofthrones/house/${id}/members?${querystring}`) 326 | .expect(206) 327 | .expect('X-Range', 'objects 90-100/100') 328 | .end((err, res) => { 329 | 330 | if (err) return done(err) 331 | let body = res.body 332 | assert(Array.isArray(body)) 333 | debug(body) 334 | assert(body.length === 10) 335 | done() 336 | 337 | }) 338 | 339 | }) 340 | 341 | it ('should return 200 | get /house/:id/members, limit > count', function (done) { 342 | 343 | const querystring = qs.stringify({ 344 | _limit: 200 345 | }) 346 | 347 | server 348 | .get(`/gameofthrones/house/${id}/members?${querystring}`) 349 | .expect(200) 350 | .end((err, res) => { 351 | 352 | if (err) return done(err) 353 | let body = res.body 354 | assert(Array.isArray(body)) 355 | debug(body) 356 | assert(body.length === count) 357 | done() 358 | 359 | }) 360 | 361 | }) 362 | 363 | }) 364 | 365 | }) 366 | -------------------------------------------------------------------------------- /test/query-subQuery.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:query') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const models = prepare.sequelize.models 16 | 17 | describe ('subQuery', function () { 18 | 19 | let server 20 | 21 | before (function () { 22 | 23 | let app = koa() 24 | , restql = new RestQL(models) 25 | 26 | app.use(restql.routes()) 27 | server = request(http.createServer(app.callback())) 28 | 29 | }) 30 | 31 | beforeEach (function (done) { 32 | 33 | debug('reset db') 34 | prepare.loadMockData().then(() => { 35 | done() 36 | }).catch(done) 37 | 38 | }) 39 | 40 | it ('should return 200 | get /seat, include house, subQuery = 0', function (done) { 41 | 42 | const querystring = qs.stringify({ 43 | _attributes: ['id'], 44 | _include: [{ 45 | attributes: ['id', 'house_id'], 46 | association: 'members', 47 | include: [{ 48 | attributes: ['id'], 49 | association: 'reviewers' 50 | }] 51 | }], 52 | _distinct: 1, 53 | _subQuery: 0 54 | }) 55 | 56 | server 57 | .get(`/gameofthrones/house?${querystring}`) 58 | .expect(200) 59 | .expect('X-Range', `objects 0-5/5`) 60 | .end((err, res) => { 61 | 62 | debug(res.headers) 63 | if (err) return done(err) 64 | let body = res.body 65 | assert(Array.isArray(body)) 66 | debug(body) 67 | 68 | assert(body.length === 5) 69 | 70 | done() 71 | 72 | }) 73 | 74 | }) 75 | 76 | }) 77 | -------------------------------------------------------------------------------- /test/query-through.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:query') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const models = prepare.sequelize.models 16 | 17 | describe ('through', function () { 18 | 19 | let server 20 | 21 | before (function () { 22 | 23 | let app = koa() 24 | , restql = new RestQL(models) 25 | 26 | app.use(restql.routes()) 27 | server = request(http.createServer(app.callback())) 28 | 29 | }) 30 | 31 | beforeEach (function (done) { 32 | 33 | debug('reset db') 34 | prepare.loadMockData().then(() => { 35 | done() 36 | }).catch(done) 37 | 38 | }) 39 | 40 | it ('should return 200 | get /user/:id/characters, with through', function (done) { 41 | 42 | const querystring = qs.stringify({ 43 | _through: { 44 | where: { 45 | rate: { 46 | $gt: 0 47 | } 48 | } 49 | } 50 | }) 51 | 52 | const id = 1 53 | 54 | models.user.findById(id).then(user => { 55 | 56 | server 57 | .get(`/user/${user.id}/characters?${querystring}`) 58 | .expect(200) 59 | .end((err, res) => { 60 | 61 | if (err) return done(err) 62 | let body = res.body 63 | assert(Array.isArray(body)) 64 | debug(body) 65 | 66 | body.forEach(character=> { 67 | debug(character) 68 | assert(character.id) 69 | assert(character.name) 70 | assert(character.user_characters) 71 | assert(character.user_characters.rate > 0) 72 | }) 73 | 74 | done() 75 | 76 | }) 77 | 78 | }).catch(done) 79 | 80 | 81 | }) 82 | 83 | }) 84 | -------------------------------------------------------------------------------- /test/query-where.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('qs') 4 | const koa = require('koa') 5 | const http = require('http') 6 | const uuid = require('node-uuid') 7 | const assert = require('assert') 8 | const request = require('supertest') 9 | const debug = require('debug')('koa-restql:test:query') 10 | 11 | const test = require('./lib/test') 12 | const prepare = require('./lib/prepare') 13 | const RestQL = require('../lib/RestQL') 14 | 15 | const { 16 | sequelize, createMockData 17 | } = prepare 18 | 19 | const models = sequelize.models 20 | 21 | describe ('where', function () { 22 | 23 | let server 24 | 25 | before (function () { 26 | 27 | let app = koa() 28 | , restql = new RestQL(models) 29 | 30 | app.use(restql.routes()) 31 | server = request(http.createServer(app.callback())) 32 | 33 | }) 34 | 35 | describe('qs parse array', function() { 36 | 37 | const model = models.user 38 | const count = 100 39 | 40 | before (function () { 41 | 42 | return prepare.reset().then(() => 43 | createMockData(model, ['name'], count)) 44 | 45 | }) 46 | 47 | it ('should return 200 | get /user, with id = 1', function (done) { 48 | 49 | const id = 1 50 | const querystring = qs.stringify({ id }) 51 | 52 | server 53 | .get(`/user?${querystring}`) 54 | .expect(200) 55 | .end((err, res) => { 56 | 57 | if (err) return done(err) 58 | let body = res.body 59 | assert(Array.isArray(body)) 60 | debug(body) 61 | 62 | body.forEach(user => { 63 | assert(user.id === id) 64 | assert(user.name) 65 | }) 66 | 67 | done() 68 | 69 | }) 70 | 71 | }) 72 | 73 | 74 | it('should return 206 | get /user with id = [1, 2, ...]', function(done) { 75 | 76 | const iterator = new Array(25).fill(0) 77 | const ids = iterator.map((value, index) => index) 78 | 79 | const querystring = qs.stringify({ id: ids }) 80 | 81 | server 82 | .get(`/user?${querystring}`) 83 | .expect(206) 84 | .end((err, res) => { 85 | 86 | if (err) return done(err) 87 | let body = res.body 88 | assert(Array.isArray(body)) 89 | assert(body.length === 20) 90 | debug(body) 91 | 92 | done() 93 | 94 | }) 95 | 96 | }) 97 | 98 | }) 99 | 100 | }) 101 | -------------------------------------------------------------------------------- /test/restql.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert'); 4 | const debug = require('debug')('koa-restql:test:restql') 5 | 6 | const RestQL = require('../lib/RestQL') 7 | const prepare = require('./lib/prepare') 8 | 9 | const models = prepare.sequelize.models; 10 | 11 | describe ('RestQL', function () { 12 | 13 | it ('should create a Restql instance | new RestQL(models)', function () { 14 | 15 | const restql = new RestQL(models) 16 | assert(restql instanceof RestQL) 17 | 18 | }) 19 | 20 | 21 | it ('should create a Restql instance | RestQL(models)', function () { 22 | 23 | const restql = RestQL(models) 24 | assert(restql instanceof RestQL) 25 | 26 | }) 27 | 28 | }) 29 | 30 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const prepare = require('./lib/prepare') 4 | const debug = require('debug')('koa-restql:test:setup') 5 | 6 | before ('database setup', function (done) { 7 | 8 | let sequelize = prepare.sequelize 9 | 10 | prepare.loadMockData().then(res => { 11 | debug(res); 12 | done() 13 | }).catch(done) 14 | 15 | }) 16 | --------------------------------------------------------------------------------