├── circle.yml ├── scripts ├── watch.sh ├── env.sh └── build.sh ├── src ├── associations │ ├── index.js │ ├── associate.js │ ├── one-to-one.js │ └── one-to-many.js ├── utils.js ├── error.js ├── get-config-for-method.js ├── crud.test.js ├── crud.js └── get-config-for-method.test.js ├── .eslintrc ├── CONTRIBUTING ├── .gitignore ├── package.json └── README.md /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.5.0 4 | 5 | dependencies: 6 | pre: 7 | - npm prune 8 | post: 9 | - mkdir -p $CIRCLE_TEST_REPORTS/ava 10 | -------------------------------------------------------------------------------- /scripts/watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | ./scripts/build.sh --watch 7 | -------------------------------------------------------------------------------- /scripts/env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | export SRC_DIR="./src" 7 | export OUT_DIR="./build" 8 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | 7 | source "scripts/env.sh" 8 | 9 | babel="./node_modules/.bin/babel" 10 | 11 | build () { 12 | $babel "$SRC_DIR" --out-dir "$OUT_DIR" $@ 13 | } 14 | 15 | build $@ 16 | -------------------------------------------------------------------------------- /src/associations/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const one_to_one_1 = require('./one-to-one'); 3 | exports.oneToOne = one_to_one_1.default; 4 | const one_to_many_1 = require('./one-to-many'); 5 | exports.oneToMany = one_to_many_1.default; 6 | const associate_1 = require('./associate'); 7 | exports.associate = associate_1.default; 8 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true 7 | } 8 | }, 9 | "env": { 10 | "browser": false, 11 | "node": true 12 | }, 13 | "plugins": [ 14 | ], 15 | "extends": [ 16 | "airbnb" 17 | ], 18 | "rules": { 19 | "indent": [ 20 | "error", 21 | 4 22 | ], 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Commit Message 2 | =============== 3 | Please follow [this convention](http://karma-runner.github.io/1.0/dev/git-commit-msg.html) for git commit message. 4 | 5 | Lint 6 | ==== 7 | Please lint your code using `npm run lint` (also `npm run lint -- --fix` to auto-fix). 8 | 9 | Pull request 10 | === 11 | 1. Fork it! 12 | 2. Create your feature branch: `git checkout -b my-new-feature` 13 | 3. Commit your changes: `git commit -m 'Add some feature'` 14 | 4. Push to the branch: `git push origin my-new-feature` 15 | 5. Submit the pull request. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src 2 | #### joe made this: https://goel.io/joe 3 | 4 | #####=== Node ===##### 5 | 6 | # Logs 7 | logs 8 | *.log 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | src/Release 29 | 30 | # Dependency directory 31 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 32 | node_modules 33 | 34 | # Debug log from npm 35 | npm-debug.log 36 | 37 | # IDEIA directory 38 | .idea -------------------------------------------------------------------------------- /src/associations/associate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const utils_1 = require('../utils'); 3 | let prefix; 4 | let defaultConfig; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.default = (server, a, b, names, options) => { 7 | prefix = options.prefix; 8 | defaultConfig = options.defaultConfig; 9 | server.route({ 10 | method: 'GET', 11 | path: `${prefix}/associate/${names.a.singular}/{aid}/${names.b.singular}/{bid}`, }); 12 | handler(request, reply); 13 | { 14 | const instanceb = yield b.findOne({ 15 | where: { 16 | [b.primaryKeyField]: request.params.bid, 17 | }, 18 | }); 19 | const instancea = yield a.findOne({ 20 | where: { 21 | [a.primaryKeyField]: request.params.aid, 22 | }, 23 | }); 24 | const fn = utils_1.getMethod(instancea, names.b, false, 'add') || 25 | utils_1.getMethod(instancea, names.b, false, 'set'); 26 | yield fn(instanceb); 27 | reply([instancea, instanceb]); 28 | } 29 | config: defaultConfig, 30 | ; 31 | }; 32 | ; 33 | ; 34 | //# sourceMappingURL=associate.js.map -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-sequelize-restfull", 3 | "version": "2.6.2", 4 | "description": "Hapi plugin that automatically generates RESTful API for CRUD", 5 | "main": "src/index.js", 6 | "config": { 7 | "ghooks": { 8 | "__pre-commit": "npm run lint && npm run build" 9 | } 10 | }, 11 | "scripts": { 12 | "lint": "eslint src", 13 | "test": "ava --require babel-register --source='src/**/*.js' --source='!build/**/*' --tap=${CI-false} src/**/*.test.js | $(if [ -z ${CI:-} ]; then echo 'tail'; else tap-xunit > $CIRCLE_TEST_REPORTS/ava/ava.xml; fi;)", 14 | "tdd": "ava --require babel-register --source='src/**/*.js' --source='!build/**/*' --watch src/**/*.test.js", 15 | "build": "scripty", 16 | "watch": "scripty" 17 | }, 18 | "repository": { 19 | "git": "https://github.com/ephillipe/hapi-sequelize-restfull" 20 | }, 21 | "files": [ 22 | "src" 23 | ], 24 | "author": "Erick Almeida (https://twitter.com/ephillipe)", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "ava": "^0.16.0", 28 | "eslint": "^3.3.1", 29 | "eslint-config-airbnb": "^10.0.1", 30 | "eslint-plugin-import": "^1.14.0", 31 | "eslint-plugin-jsx-a11y": "^2.1.0", 32 | "eslint-plugin-prefer-object-spread": "^1.1.0", 33 | "eslint-plugin-react": "^6.1.2", 34 | "ghooks": "^1.3.2", 35 | "scripty": "^1.6.0", 36 | "sinon": "^1.17.5", 37 | "sinon-bluebird": "^3.0.2", 38 | "tap-xunit": "^1.4.0" 39 | }, 40 | "dependencies": { 41 | "boom": "^4.0.0", 42 | "joi": "^9.0.4", 43 | "lodash": "^4.15.0", 44 | "qs": "^6.2.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const lodash_1 = require('lodash'); 3 | const boom_1 = require('boom'); 4 | exports.parseInclude = request => { 5 | const include = Array.isArray(request.query.include) ? request.query.include 6 | : [request.query.include]; 7 | const noGetDb = typeof request.getDb !== 'function'; 8 | const noRequestModels = !request.models; 9 | if (noGetDb && noRequestModels) { 10 | return boom_1.notImplemented('`request.getDb` or `request.models` are not defined.' 11 | + 'Be sure to load hapi-sequelize before hapi-sequelize-restfull.'); 12 | } 13 | const { models } = noGetDb ? request : request.getDb(); 14 | return include.map(a => { 15 | if (typeof a === 'string') 16 | return models[a]; 17 | if (a && typeof a.model === 'string' && a.model.length) { 18 | a.model = models[a.model]; 19 | } 20 | return a; 21 | }).filter(lodash_1.identity); 22 | }; 23 | exports.parseWhere = request => { 24 | const where = lodash_1.omit(request.query, 'include'); 25 | for (const key of Object.keys(where)) { 26 | try { 27 | where[key] = JSON.parse(where[key]); 28 | } 29 | catch (e) { 30 | } 31 | } 32 | return where; 33 | }; 34 | exports.getMethod = (model, association, plural = true, method = 'get') => { 35 | const a = plural ? association.original.plural : association.original.singular; 36 | const b = plural ? association.original.singular : association.original.plural; // alternative 37 | const fn = model[`${method}${a}`] || model[`${method}${b}`]; 38 | if (fn) 39 | return fn.bind(model); 40 | return false; 41 | }; 42 | //# sourceMappingURL=utils.js.map -------------------------------------------------------------------------------- /src/error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator.throw(value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments)).next()); 8 | }); 9 | }; 10 | const boom_1 = require('boom'); 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.default = (target, key, descriptor) => { 13 | const fn = descriptor.value; 14 | descriptor.value = (request, reply) => __awaiter(this, void 0, void 0, function* () { 15 | try { 16 | yield fn(request, reply); 17 | } 18 | catch (e) { 19 | if (e.original) { 20 | const { code, detail, hint } = e.original; 21 | let error; 22 | // pg error codes https://www.postgresql.org/docs/9.5/static/errcodes-appendix.html 23 | if (code && (code.startsWith('22') || code.startsWith('23'))) { 24 | error = boom_1.default.wrap(e, 406); 25 | } 26 | else if (code && (code.startsWith('42'))) { 27 | error = boom_1.default.wrap(e, 422); 28 | } 29 | else { 30 | // use a 502 error code since the issue is upstream with postgres, not 31 | // this server 32 | error = boom_1.default.wrap(e, 502); 33 | } 34 | // detail tends to be more specific information. So, if we have it, use. 35 | if (detail) { 36 | error.message += `: ${detail}`; 37 | error.reformat(); 38 | } 39 | // hint might provide useful information about how to fix the problem 40 | if (hint) { 41 | error.message += ` Hint: ${hint}`; 42 | error.reformat(); 43 | } 44 | reply(error); 45 | } 46 | else if (!e.isBoom) { 47 | const { message } = e; 48 | let err; 49 | if (e.name === 'SequelizeValidationError') 50 | err = boom_1.default.badData(message); 51 | else if (e.name === 'SequelizeConnectionTimedOutError') 52 | err = boom_1.default.gatewayTimeout(message); 53 | else if (e.name === 'SequelizeHostNotReachableError') 54 | err = boom_1.default.serverUnavailable(message); 55 | else if (e.name === 'SequelizeUniqueConstraintError') 56 | err = boom_1.default.conflict(message); 57 | else if (e.name === 'SequelizeForeignKeyConstraintError') 58 | err = boom_1.default.expectationFailed(message); 59 | else if (e.name === 'SequelizeExclusionConstraintError') 60 | err = boom_1.default.expectationFailed(message); 61 | else if (e.name === 'SequelizeConnectionError') 62 | err = boom_1.default.badGateway(message); 63 | else 64 | err = boom_1.default.badImplementation(message); 65 | reply(err); 66 | } 67 | else { 68 | reply(e); 69 | } 70 | } 71 | }); 72 | return descriptor; 73 | }; 74 | //# sourceMappingURL=error.js.map -------------------------------------------------------------------------------- /src/associations/one-to-one.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const utils_1 = require('../utils'); 4 | let prefix; 5 | let defaultConfig; 6 | Object.defineProperty(exports, "__esModule", {value: true}); 7 | exports.default = (server, a, b, names, options) => { 8 | prefix = options.prefix; 9 | defaultConfig = options.defaultConfig; 10 | exports.get(server, a, b, names); 11 | exports.create(server, a, b, names); 12 | exports.destroy(server, a, b, names); 13 | exports.update(server, a, b, names); 14 | }; 15 | exports.get = (server, a, b, names) => { 16 | server.route({ 17 | method: 'GET', 18 | path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}`, 19 | }); 20 | handler(request, reply); 21 | { 22 | const include = utils_1.parseInclude(request); 23 | const where = utils_1.parseWhere(request); 24 | a.findOne({ 25 | where: { 26 | [a.primaryKeyField]: request.params.aid, 27 | }, 28 | }).then(base){ 29 | const method = utils_1.getMethod(base, names.b, false); 30 | const list = yield method({where: where, include: include, limit: 1}); 31 | if (Array.isArray(list)) { 32 | reply(list[0]); 33 | } 34 | else { 35 | reply(list); 36 | } 37 | }; 38 | } 39 | config: defaultConfig, 40 | ; 41 | }; 42 | ; 43 | exports.create = (server, a, b, names) => { 44 | server.route({ 45 | method: 'POST', 46 | path: `${prefix}/${names.a.singular}/{id}/${names.b.singular}`, 47 | }); 48 | handler(request, reply); 49 | { 50 | const base = yield a.findOne({ 51 | where: { 52 | [a.primaryKeyField]: request.params.id, 53 | }, 54 | }); 55 | const method = utils_1.getMethod(base, names.b, false, 'create'); 56 | const instance = yield method(request.payload); 57 | reply(instance); 58 | } 59 | config: defaultConfig, 60 | ; 61 | }; 62 | ; 63 | exports.destroy = (server, a, b, names) => { 64 | server.route({ 65 | method: 'DELETE', 66 | path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`, 67 | }); 68 | handler(request, reply); 69 | { 70 | const include = utils_1.parseInclude(request); 71 | const where = utils_1.parseWhere(request); 72 | const base = yield a.findOne({ 73 | where: { 74 | [a.primaryKeyField]: request.params.aid, 75 | }, 76 | }); 77 | where[b.primaryKeyField] = request.params.bid; 78 | const method = utils_1.getMethod(base, names.b, false, 'get'); 79 | const instance = yield method({where: where, include: include}); 80 | yield instance.destroy(); 81 | reply(instance); 82 | } 83 | config: defaultConfig, 84 | ; 85 | }; 86 | ; 87 | exports.update = (server, a, b, names) => { 88 | server.route({ 89 | method: 'PUT', 90 | path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`, 91 | }); 92 | handler(request, reply); 93 | { 94 | const include = utils_1.parseInclude(request); 95 | const where = utils_1.parseWhere(request); 96 | const base = yield a.findOne({ 97 | where: { 98 | id: request.params.aid, 99 | }, 100 | }); 101 | where[b.primaryKeyField] = request.params.bid; 102 | const method = utils_1.getMethod(base, names.b, false); 103 | const instance = yield method({where: where, include: include}); 104 | yield instance.update(request.payload); 105 | reply(instance); 106 | } 107 | config: defaultConfig, 108 | ; 109 | }; 110 | ; 111 | //# sourceMappingURL=one-to-one.js.map -------------------------------------------------------------------------------- /src/get-config-for-method.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const lodash_1 = require('lodash'); 3 | const joi_1 = require('joi'); 4 | // if the custom validation is a joi object we need to concat 5 | // else, assume it's an plain object and we can just add it in with .keys 6 | const concatToJoiObject = (joi, candidate) => { 7 | if (!candidate) 8 | return joi; 9 | else if (candidate.isJoi) 10 | return joi.concat(candidate); 11 | else 12 | return joi.keys(candidate); 13 | }; 14 | exports.sequelizeOperators = { 15 | $and: joi_1.default.any(), 16 | $or: joi_1.default.any(), 17 | $gt: joi_1.default.any(), 18 | $gte: joi_1.default.any(), 19 | $lt: joi_1.default.any(), 20 | $lte: joi_1.default.any(), 21 | $ne: joi_1.default.any(), 22 | $eq: joi_1.default.any(), 23 | $not: joi_1.default.any(), 24 | $between: joi_1.default.any(), 25 | $notBetween: joi_1.default.any(), 26 | $in: joi_1.default.any(), 27 | $notIn: joi_1.default.any(), 28 | $like: joi_1.default.any(), 29 | $notLike: joi_1.default.any(), 30 | $iLike: joi_1.default.any(), 31 | $notILike: joi_1.default.any(), 32 | $overlap: joi_1.default.any(), 33 | $contains: joi_1.default.any(), 34 | $contained: joi_1.default.any(), 35 | $any: joi_1.default.any(), 36 | $col: joi_1.default.any(), 37 | }; 38 | exports.whereMethods = [ 39 | 'list', 40 | 'get', 41 | 'scope', 42 | 'destroy', 43 | 'destoryScope', 44 | 'destroyAll', 45 | ]; 46 | exports.includeMethods = [ 47 | 'list', 48 | 'get', 49 | 'scope', 50 | 'destoryScope', 51 | ]; 52 | exports.payloadMethods = [ 53 | 'create', 54 | 'update', 55 | ]; 56 | exports.scopeParamsMethods = [ 57 | 'destroyScope', 58 | 'scope', 59 | ]; 60 | exports.idParamsMethods = [ 61 | 'get', 62 | 'update', 63 | ]; 64 | Object.defineProperty(exports, "__esModule", { value: true }); 65 | exports.default = ({ method, attributeValidation, associationValidation, scopes = [], config = {}, }) => { 66 | const hasWhere = exports.whereMethods.includes(method); 67 | const hasInclude = exports.includeMethods.includes(method); 68 | const hasPayload = exports.payloadMethods.includes(method); 69 | const hasScopeParams = exports.scopeParamsMethods.includes(method); 70 | const hasIdParams = exports.idParamsMethods.includes(method); 71 | // clone the config so we don't modify it on multiple passes. 72 | let methodConfig = { config: config, validate: { config: .validate } }; 73 | if (hasWhere) { 74 | const query = concatToJoiObject(joi_1.default.object() 75 | .keys({}, ...attributeValidation, ...exports.sequelizeOperators)); 76 | } 77 | lodash_1.get(methodConfig, 'validate.query'); 78 | ; 79 | methodConfig = lodash_1.set(methodConfig, 'validate.query', query); 80 | }; 81 | if (hasInclude) { 82 | const query = concatToJoiObject(joi_1.default.object() 83 | .keys({}, ...associationValidation)); 84 | } 85 | lodash_1.get(methodConfig, 'validate.query'); 86 | ; 87 | methodConfig = lodash_1.set(methodConfig, 'validate.query', query); 88 | if (hasPayload) { 89 | const payload = concatToJoiObject(joi_1.default.object() 90 | .keys({}, ...attributeValidation)); 91 | } 92 | lodash_1.get(methodConfig, 'validate.payload'); 93 | ; 94 | methodConfig = lodash_1.set(methodConfig, 'validate.payload', payload); 95 | if (hasScopeParams) { 96 | const params = concatToJoiObject(joi_1.default.object() 97 | .keys({ 98 | scope: joi_1.default.string().valid(...scopes), 99 | }), lodash_1.get(methodConfig, 'validate.params')); 100 | methodConfig = lodash_1.set(methodConfig, 'validate.params', params); 101 | } 102 | if (hasIdParams) { 103 | const params = concatToJoiObject(joi_1.default.object() 104 | .keys({ 105 | id: joi_1.default.any(), 106 | }), lodash_1.get(methodConfig, 'validate.params')); 107 | methodConfig = lodash_1.set(methodConfig, 'validate.params', params); 108 | } 109 | return methodConfig; 110 | ; 111 | //# sourceMappingURL=get-config-for-method.js.map -------------------------------------------------------------------------------- /src/crud.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator.throw(value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments)).next()); 8 | }); 9 | }; 10 | const ava_1 = require('ava'); 11 | const crud_1 = require('./crud'); 12 | const sinon_1 = require('sinon'); 13 | const uniqueId = require('lodash/uniqueId'); 14 | require('sinon-bluebird'); 15 | const METHODS = { 16 | GET: 'GET', 17 | }; 18 | ava_1.default.beforeEach('setup server', (t) => { 19 | t.context.server = { 20 | route: sinon_1.stub(), 21 | }; 22 | }); 23 | const makeModel = () => { 24 | const id = uniqueId(); 25 | return { 26 | findAll: sinon_1.stub(), 27 | _plural: 'models', 28 | _singular: 'model', 29 | toJSON: () => ({ id: id }), 30 | id: id, 31 | }; 32 | }; 33 | ava_1.default.beforeEach('setup model', (t) => { 34 | t.context.model = makeModel(); 35 | }); 36 | ava_1.default.beforeEach('setup models', (t) => { 37 | t.context.models = [t.context.model, makeModel()]; 38 | }); 39 | ava_1.default.beforeEach('setup request stub', (t) => { 40 | t.context.request = { 41 | query: {}, 42 | payload: {}, 43 | models: [t.context.model], 44 | }; 45 | }); 46 | ava_1.default.beforeEach('setup reply stub', (t) => { 47 | t.context.reply = sinon_1.stub(); 48 | }); 49 | ava_1.default('crud#list without prefix', (t) => { 50 | let { server, model } = t.context; 51 | new crud_1.default().list({ server: server, model: model }); 52 | let { path } = server.route.args[0][0]; 53 | t.falsy(path.includes('undefined'), 'correctly sets the path without a prefix defined'); 54 | t.is(path, `/${model._plural}`, 'the path sets to the plural model'); 55 | }); 56 | ava_1.default('crud#list with prefix', (t) => { 57 | const { server, model } = t.context; 58 | const prefix = '/v1'; 59 | new crud_1.default().list({ server: server, model: model, prefix: prefix }); 60 | const { path } = server.route.args[0][0]; 61 | t.is(path, `${prefix}/${model._plural}`, 'the path sets to the plural model with the prefix'); 62 | }); 63 | ava_1.default('crud#list method', (t) => { 64 | const { server, model } = t.context; 65 | new crud_1.default().list({ server: server, model: model }); 66 | const { method } = server.route.args[0][0]; 67 | t.is(method, METHODS.GET, `sets the method to ${METHODS.GET}`); 68 | }); 69 | ava_1.default('crud#list config', (t) => { 70 | const { server, model } = t.context; 71 | const userConfig = {}; 72 | new crud_1.default().list({ server: server, model: model, config: userConfig }); 73 | const { config } = server.route.args[0][0]; 74 | t.is(config, userConfig, 'sets the user config'); 75 | }); 76 | ava_1.default('crud#list handler', (t) => __awaiter(this, void 0, void 0, function* () { 77 | const { server, model, request, reply, models } = t.context; 78 | new crud_1.default().list({ server: server, model: model }); 79 | const { handler } = server.route.args[0][0]; 80 | model.findAll.resolves(models); 81 | try { 82 | yield handler(request, reply); 83 | } 84 | catch (e) { 85 | t.ifError(e, 'does not error while handling'); 86 | } 87 | finally { 88 | t.pass('does not error while handling'); 89 | } 90 | t.truthy(reply.calledOnce, 'calls reply only once'); 91 | const response = reply.args[0][0]; 92 | t.deepEqual(response, models.map(({ id }) => ({ id: id })), 'responds with the list of models'); 93 | })); 94 | ava_1.default('crud#list handler if parseInclude errors', (t) => __awaiter(this, void 0, void 0, function* () { 95 | const { server, model, request, reply } = t.context; 96 | // we _want_ the error 97 | delete request.models; 98 | new crud_1.default().list({ server: server, model: model }); 99 | const { handler } = server.route.args[0][0]; 100 | yield handler(request, reply); 101 | t.truthy(reply.calledOnce, 'calls reply only once'); 102 | const response = reply.args[0][0]; 103 | t.truthy(response.isBoom, 'responds with a Boom error'); 104 | })); 105 | //# sourceMappingURL=crud.test.js.map -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hapi-sequelize-restfull [![CircleCI](https://circleci.com/gh/ephillipe/hapi-sequelize-restfull.svg?style=svg)](https://circleci.com/gh/mdibaiee/hapi-sequelize-restfull) 2 | ========================================================================================================================================================================== 3 | 4 | Automatically generate a RESTful API for your models and associations 5 | 6 | This plugin depends on [`hapi-sequelize`](https://github.com/danecando/hapi-sequelize). 7 | This plugin was forked from [`hapi-sequelize-restfull`](https://github.com/mdibaiee/hapi-sequelize-crud). 8 | 9 | ``` 10 | npm install -S hapi-sequelize-restfull 11 | ``` 12 | 13 | ## Configure 14 | 15 | Please note that you should register `hapi-sequelize-restfull` after defining your 16 | associations. 17 | 18 | ```javascript 19 | // First, register hapi-sequelize 20 | await register({ 21 | register: require('hapi-sequelize'), 22 | options: { ... } 23 | }); 24 | 25 | // Then, define your associations 26 | let db = server.plugins['hapi-sequelize'].db; 27 | let models = db.sequelize.models; 28 | associations(models); // pretend this function defines our associations 29 | 30 | // Now, register hapi-sequelize-restfull 31 | await register({ 32 | register: require('hapi-sequelize-restfull'), 33 | options: { 34 | prefix: '/v1', 35 | name: 'db', // the same name you used for configuring `hapi-sequelize` (options.name) 36 | defaultConfig: { ... }, // passed as `config` to all routes created 37 | 38 | // You can specify which models must have routes defined for using the 39 | // `models` property. If you omit this property, all models will have 40 | // models defined for them. e.g. 41 | models: ['cat', 'dog'] // only the cat and dog models will have routes created 42 | 43 | // or 44 | models: [ 45 | // possible methods: list, get, scope, create, destroy, destroyAll, destroyScope, update 46 | // the cat model only has get and list methods enabled 47 | {model: 'cat', methods: ['get', 'list']}, 48 | // the dog model has all methods enabled 49 | {model: 'dog'}, 50 | // the cow model also has all methods enabled 51 | 'cow', 52 | // the bat model as a custom config for the list method, but uses the default config for create. 53 | // `config` if provided, overrides the default config 54 | {model: 'bat', methods: ['list'], config: { ... }}, 55 | {model: 'bat', methods: ['create']} 56 | {model: 'fly', config: { 57 | // interact with the request before hapi-sequelize-restfull does 58 | , ext: { 59 | onPreHandler: (request, reply) => { 60 | if (request.auth.hasAccessToFly) reply.continue() 61 | else reply(Boom.unauthorized()) 62 | } 63 | } 64 | // change the response data 65 | response: { 66 | schema: {id: joi.string()}, 67 | modify: true 68 | } 69 | }} 70 | ] 71 | } 72 | }); 73 | ``` 74 | 75 | ### Methods 76 | * **list**: get all rows in a table 77 | * **get**: get a single row 78 | * **scope**: reference a [sequelize scope](http://docs.sequelizejs.com/en/latest/api/model/#scopeoptions-model) 79 | * **create**: create a new row 80 | * **destroy**: delete a row 81 | * **destroyAll**: delete all models in the table 82 | * **destroyScope**: use a [sequelize scope](http://docs.sequelizejs.com/en/latest/api/model/#scopeoptions-model) to find rows, then delete them 83 | * **update**: update a row 84 | 85 | ## `where` queries 86 | It's easy to restrict your requests using Sequelize's `where` query option. Just pass a query parameter. 87 | 88 | ```js 89 | // returns only teams that have a `city` property of "windsor" 90 | // GET /team?city=windsor 91 | 92 | // results in the Sequelize query: 93 | Team.findOne({ where: { city: 'windsor' }}) 94 | ``` 95 | 96 | You can also do more complex queries by setting the value of a key to JSON. 97 | 98 | ```js 99 | // returns only teams that have a `address.city` property of "windsor" 100 | // GET /team?city={"address": "windsor"} 101 | // or 102 | // GET /team?city[address]=windsor 103 | 104 | // results in the Sequelize query: 105 | Team.findOne({ where: { address: { city: 'windsor' }}}) 106 | ``` 107 | 108 | ## `include` queries 109 | Getting related models is easy, just use a query parameter `include`. 110 | 111 | ```js 112 | // returns all teams with their related City model 113 | // GET /teams?include=City 114 | 115 | // results in a Sequelize query: 116 | Team.findAll({include: City}) 117 | ``` 118 | 119 | If you want to get multiple related models, just pass multiple `include` parameters. 120 | ```js 121 | // returns all teams with their related City and Uniform models 122 | // GET /teams?include=City&include=Uniform 123 | 124 | // results in a Sequelize query: 125 | Team.findAll({include: [City, Uniform]}) 126 | ``` 127 | 128 | ## Authorization and other hooks 129 | You can use Hapi's [`ext` option](http://hapijs.com/api#route-options) to interact with the request both before and after this module does. This is useful if you want to enforce authorization, or modify the request before or after this module does. Hapi [has a full list of hooks](http://hapijs.com/api#request-lifecycle) you can use. 130 | 131 | ## Modify the response format 132 | By default, `hapi-sequelize-restfull` routes will respond with the full model. You can modify this using the built-in [hapi settings](http://hapijs.com/tutorials/validation#output). 133 | 134 | ```js 135 | await register({ 136 | register: require('hapi-sequelize-restfull'), 137 | options: { 138 | … 139 | {model: 'fly', config: { 140 | response: { 141 | // setting this schema will restrict the response to only the id 142 | schema: { id: joi.string() }, 143 | // This tells Hapi to restrict the response to the keys specified in `schema` 144 | modify: true 145 | } 146 | }} 147 | } 148 | 149 | }) 150 | ``` 151 | 152 | ## Full list of methods 153 | 154 | Let's say you have a `many-to-many` association like this: 155 | 156 | ```javascript 157 | Team.belongsToMany(Role, { through: 'TeamRoles' }); 158 | Role.belongsToMany(Team, { through: 'TeamRoles' }); 159 | ``` 160 | 161 | You get these: 162 | 163 | ``` 164 | # get an array of records 165 | GET /team/{id}/roles 166 | GET /role/{id}/teams 167 | # might also append `where` query parameters to search for 168 | GET /role/{id}/teams?members=5 169 | GET /role/{id}/teams?city=healdsburg 170 | 171 | # you might also use scopes 172 | GET /teams/{scope}/roles/{scope} 173 | GET /team/{id}/roles/{scope} 174 | GET /roles/{scope}/teams/{scope} 175 | GET /roles/{id}/teams/{scope} 176 | 177 | # get a single record 178 | GET /team/{id}/role/{id} 179 | GET /role/{id}/team/{id} 180 | 181 | # create 182 | POST /team/{id}/role 183 | POST /role/{id}/team 184 | 185 | # update 186 | PUT /team/{id}/role/{id} 187 | PUT /role/{id}/team/{id} 188 | 189 | # delete 190 | DELETE /team/{id}/roles #search and destroy 191 | DELETE /role/{id}/teams?members=5 192 | 193 | DELETE /team/{id}/role/{id} 194 | DELETE /role/{id}/team/{id} 195 | 196 | # include 197 | # include nested associations (you can specify an array if includes) 198 | GET /team/{id}/role/{id}?include=SomeRoleAssociation 199 | 200 | # you also get routes to associate objects with each other 201 | GET /associate/role/{id}/employee/{id} # associates role {id} with employee {id} 202 | 203 | # you can specify a prefix to change the URLs like this: 204 | GET /v1/team/{id}/roles 205 | ``` 206 | -------------------------------------------------------------------------------- /src/associations/one-to-many.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const joi_1 = require('joi'); 3 | const lodash_1 = require('lodash'); 4 | const utils_1 = require('../utils'); 5 | let prefix; 6 | let defaultConfig; 7 | Object.defineProperty(exports, "__esModule", { value: true }); 8 | exports.default = (server, a, b, names, options) => { 9 | prefix = options.prefix; 10 | defaultConfig = options.defaultConfig; 11 | exports.get(server, a, b, names); 12 | exports.list(server, a, b, names); 13 | exports.scope(server, a, b, names); 14 | exports.scopeScope(server, a, b, names); 15 | exports.destroy(server, a, b, names); 16 | exports.destroyScope(server, a, b, names); 17 | exports.update(server, a, b, names); 18 | }; 19 | exports.get = (server, a, b, names) => { 20 | server.route({ 21 | method: 'GET', 22 | path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`, }); 23 | handler(request, reply); 24 | { 25 | const include = utils_1.parseInclude(request); 26 | const base = yield a.findOne({ 27 | where: { 28 | [a.primaryKeyField]: request.params.aid, 29 | }, 30 | }); 31 | const method = utils_1.getMethod(base, names.b); 32 | const list = yield method({ where: { 33 | [b.primaryKeyField]: request.params.bid, 34 | }, include: include }); 35 | if (Array.isArray(list)) { 36 | reply(list[0]); 37 | } 38 | else { 39 | reply(list); 40 | } 41 | } 42 | config: defaultConfig, 43 | ; 44 | }; 45 | ; 46 | exports.list = (server, a, b, names) => { 47 | server.route({ 48 | method: 'GET', 49 | path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`, }); 50 | handler(request, reply); 51 | { 52 | const include = utils_1.parseInclude(request); 53 | const where = utils_1.parseWhere(request); 54 | const base = yield a.findOne({ 55 | where: { 56 | [a.primaryKeyField]: request.params.aid, 57 | }, 58 | }); 59 | const method = utils_1.getMethod(base, names.b); 60 | const list = yield method({ where: where, include: include }); 61 | reply(list); 62 | } 63 | config: defaultConfig, 64 | ; 65 | }; 66 | ; 67 | exports.scope = (server, a, b, names) => { 68 | const scopes = Object.keys(b.options.scopes); 69 | server.route({ 70 | method: 'GET', 71 | path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`, }); 72 | handler(request, reply); 73 | { 74 | const include = utils_1.parseInclude(request); 75 | const where = utils_1.parseWhere(request); 76 | const base = yield a.findOne({ 77 | where: { 78 | [a.primaryKeyField]: request.params.aid, 79 | }, 80 | }); 81 | const method = utils_1.getMethod(base, names.b); 82 | const list = yield method({ 83 | scope: request.params.scope, 84 | where: where, 85 | include: include, 86 | }); 87 | reply(list); 88 | } 89 | config: lodash_1.default.defaultsDeep({ 90 | validate: { 91 | params: joi_1.default.object().keys({ 92 | scope: joi_1.default.string().valid(...scopes), 93 | aid: joi_1.default.number().integer().required(), 94 | }), 95 | }, 96 | }, defaultConfig), 97 | ; 98 | }; 99 | ; 100 | exports.scopeScope = (server, a, b, names) => { 101 | const scopes = { 102 | a: Object.keys(a.options.scopes), 103 | b: Object.keys(b.options.scopes), 104 | }; 105 | server.route({ 106 | method: 'GET', 107 | path: `${prefix}/${names.a.plural}/{scopea}/${names.b.plural}/{scopeb}`, }); 108 | handler(request, reply); 109 | { 110 | const include = utils_1.parseInclude(request); 111 | const where = utils_1.parseWhere(request); 112 | const list = yield b.scope(request.params.scopeb).findAll({ 113 | where: where, 114 | include: include.concat({ 115 | model: a.scope(request.params.scopea), 116 | }), 117 | }); 118 | reply(list); 119 | } 120 | config: lodash_1.default.defaultsDeep({ 121 | validate: { 122 | params: joi_1.default.object().keys({ 123 | scopea: joi_1.default.string().valid(...scopes.a), 124 | scopeb: joi_1.default.string().valid(...scopes.b), 125 | }), 126 | }, 127 | }, defaultConfig), 128 | ; 129 | }; 130 | ; 131 | exports.destroy = (server, a, b, names) => { 132 | server.route({ 133 | method: 'DELETE', 134 | path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`, }); 135 | handler(request, reply); 136 | { 137 | const include = utils_1.parseInclude(request); 138 | const where = utils_1.parseWhere(request); 139 | const base = yield a.findOne({ 140 | where: { 141 | [a.primaryKeyField]: request.params.aid, 142 | }, 143 | }); 144 | const method = utils_1.getMethod(base, names.b, true, 'get'); 145 | const list = yield method({ where: where, include: include }); 146 | yield Promise.all(list.map(item => item.destroy())); 147 | reply(list); 148 | } 149 | }; 150 | ; 151 | exports.destroyScope = (server, a, b, names) => { 152 | const scopes = Object.keys(b.options.scopes); 153 | server.route({ 154 | method: 'DELETE', 155 | path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`, }); 156 | handler(request, reply); 157 | { 158 | const include = utils_1.parseInclude(request); 159 | const where = utils_1.parseWhere(request); 160 | const base = yield a.findOne({ 161 | where: { 162 | [a.primarykeyField]: request.params.aid, 163 | }, 164 | }); 165 | const method = utils_1.getMethod(base, names.b, true, 'get'); 166 | const list = yield method({ 167 | scope: request.params.scope, 168 | where: where, 169 | include: include, 170 | }); 171 | yield Promise.all(list.map(instance => instance.destroy())); 172 | reply(list); 173 | } 174 | config: lodash_1.default.defaultsDeep({ 175 | validate: { 176 | params: joi_1.default.object().keys({ 177 | scope: joi_1.default.string().valid(...scopes), 178 | aid: joi_1.default.number().integer().required(), 179 | }), 180 | }, 181 | }, defaultConfig), 182 | ; 183 | }; 184 | ; 185 | exports.update = (server, a, b, names) => { 186 | server.route({ 187 | method: 'PUT', 188 | path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`, }); 189 | handler(request, reply); 190 | { 191 | const include = utils_1.parseInclude(request); 192 | const where = utils_1.parseWhere(request); 193 | const base = yield a.findOne({ 194 | where: { 195 | [a.primaryKeyField]: request.params.aid, 196 | }, 197 | }); 198 | const method = utils_1.getMethod(base, names.b); 199 | const list = yield method({ where: where, include: include }); 200 | yield Promise.all(list.map(instance => instance.update(request.payload))); 201 | reply(list); 202 | } 203 | config: defaultConfig, 204 | ; 205 | }; 206 | ; 207 | //# sourceMappingURL=one-to-many.js.map -------------------------------------------------------------------------------- /src/crud.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const joi = require('joi'); 3 | const path = require('path'); 4 | const lodash_1 = require('lodash'); 5 | const utils_1 = require('./utils'); 6 | const boom_1 = require('boom'); 7 | const associations = require('./associations/index'); 8 | exports.associations = associations; 9 | const get_config_for_method_ts_1 = require('./get-config-for-method.ts'); 10 | 11 | class Crud { 12 | constructor() { 13 | this.methods = { 14 | list: List.config(), 15 | get: get, 16 | scope: scope, 17 | create: create, 18 | destroy: destroy, 19 | destroyAll: destroyAll, 20 | destroyScope: destroyScope, 21 | update: update, 22 | }; 23 | } 24 | 25 | createAll({server, model, prefix, config, attributeValidation, associationValidation, scopes}) { 26 | Object.keys(this.methods).forEach((method) => { 27 | this[this.methods[method]]({ 28 | server: server, 29 | model: model, 30 | prefix: prefix, 31 | config: get_config_for_method_ts_1.default({ 32 | method: method, 33 | attributeValidation: attributeValidation, 34 | associationValidation: associationValidation, 35 | config: config, 36 | scopes: scopes, 37 | }), 38 | }); 39 | }); 40 | } 41 | 42 | register(server, model, {prefix, defaultConfig: config, models: permissions}) { 43 | const modelName = model._singular; 44 | const modelAttributes = Object.keys(model.attributes); 45 | const modelAssociations = Object.keys(model.associations); 46 | const attributeValidation = modelAttributes.reduce((params, attribute) => { 47 | params[attribute] = joi.any(); 48 | return params; 49 | }, {}); 50 | const associationValidation = { 51 | include: joi.array().items(joi.string().valid(...modelAssociations)), 52 | }; 53 | const scopes = Object.keys(model.options.scopes); 54 | // if we don't have any permissions set, just create all the methods 55 | if (!permissions) { 56 | this.createAll({ 57 | server: server, 58 | model: model, 59 | prefix: prefix, 60 | config: config, 61 | attributeValidation: attributeValidation, 62 | associationValidation: associationValidation, 63 | scopes: scopes, 64 | }); 65 | } 66 | else if (!Array.isArray(permissions)) { 67 | throw new Error('hapi-sequelize-restfull: `models` property must be an array'); 68 | } 69 | else if (permissions.includes(modelName)) { 70 | this.createAll({ 71 | server: server, 72 | model: model, 73 | prefix: prefix, 74 | config: config, 75 | attributeValidation: attributeValidation, 76 | associationValidation: associationValidation, 77 | scopes: scopes, 78 | }); 79 | } 80 | else { 81 | const permissionOptions = permissions.filter((permission) => { 82 | return permission.model === modelName; 83 | }); 84 | permissionOptions.forEach((permissionOption) => { 85 | if (lodash_1.default.isPlainObject(permissionOption)) { 86 | const permissionConfig = permissionOption.config || config; 87 | if (permissionOption.methods) { 88 | permissionOption.methods.forEach((method) => { 89 | this[this.methods[method]]({ 90 | server: server, 91 | model: model, 92 | prefix: prefix, 93 | config: get_config_for_method_ts_1.default({ 94 | method: method, 95 | attributeValidation: attributeValidation, 96 | associationValidation: associationValidation, 97 | scopes: scopes, 98 | config: permissionConfig, 99 | }), 100 | }); 101 | }); 102 | } 103 | else { 104 | this.createAll({ 105 | server: server, 106 | model: model, 107 | prefix: prefix, 108 | attributeValidation: attributeValidation, 109 | associationValidation: associationValidation, 110 | scopes: scopes, 111 | config: permissionConfig, 112 | }); 113 | } 114 | } 115 | }); 116 | } 117 | } 118 | 119 | list({server, model, prefix = '/', config}) { 120 | server.route({ 121 | method: 'GET', 122 | path: path.join(prefix, model._plural), 123 | handler: (request, reply) => { 124 | const include = utils_1.parseInclude(request); 125 | const where = utils_1.parseWhere(request); 126 | if (include instanceof Error) { 127 | return void reply(include); 128 | } 129 | const list = model.findAll({ 130 | where: where, include: include, 131 | }); 132 | reply(list.map((item) => item.toJSON())); 133 | }, 134 | config: config 135 | }); 136 | } 137 | 138 | get({server, model, prefix = '/', config}) { 139 | server.route({ 140 | method: 'GET', 141 | path: path.join(prefix, model._singular, '{id?}'), 142 | handler: (request, reply) => { 143 | const include = utils_1.parseInclude(request); 144 | const where = utils_1.parseWhere(request); 145 | const {id} = request.params; 146 | if (id) { 147 | where[model.primaryKeyField] = id; 148 | } 149 | if (include instanceof Error) { 150 | return void reply(include); 151 | } 152 | const instance = model.findOne({where: where, include: include}); 153 | if (!instance) { 154 | return void reply(boom_1.notFound(`${id} not found.`)); 155 | } 156 | reply(instance.toJSON()); 157 | }, 158 | config: config, 159 | }); 160 | } 161 | 162 | scope({server, model, prefix = '/', config}) { 163 | server.route({ 164 | method: 'GET', 165 | path: path.join(prefix, model._plural, '{scope}'), 166 | handler: (request, reply) => { 167 | const include = utils_1.parseInclude(request); 168 | const where = utils_1.parseWhere(request); 169 | if (include instanceof Error) { 170 | return void reply(include); 171 | } 172 | const list = model.scope(request.params.scope).findAll({include: include, where: where}); 173 | reply(list.map((item) => item.toJSON())); 174 | }, 175 | config: config 176 | }); 177 | } 178 | 179 | create({server, model, prefix = '/', config}) { 180 | server.route({ 181 | method: 'POST', 182 | path: path.join(prefix, model._singular), 183 | handler: (request, reply) => { 184 | const instance = model.create(request.payload); 185 | reply(instance.toJSON()); 186 | }, 187 | config: config 188 | }); 189 | } 190 | 191 | destroy({server, model, prefix = '/', config}) { 192 | server.route({ 193 | method: 'DELETE', 194 | path: path.join(prefix, model._singular, '{id?}'), 195 | handler: (request, reply) => { 196 | const where = utils_1.parseWhere(request); 197 | if (request.params.id) { 198 | where[model.primaryKeyField] = request.params.id; 199 | } 200 | const list = model.findAll({where: where}); 201 | Promise.all(list.map(instance => instance.destroy())); 202 | const listAsJSON = list.map((item) => item.toJSON()); 203 | reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON); 204 | }, 205 | config: config 206 | }); 207 | } 208 | 209 | destroyAll({server, model, prefix = '/', config}) { 210 | server.route({ 211 | method: 'DELETE', 212 | path: path.join(prefix, model._plural), 213 | handler: (request, reply) => { 214 | const where = utils_1.parseWhere(request); 215 | const list = model.findAll({where: where}); 216 | Promise.all(list.map(instance => instance.destroy())); 217 | const listAsJSON = list.map((item) => item.toJSON()); 218 | reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON); 219 | }, 220 | config: config 221 | }); 222 | } 223 | 224 | destroyScope({server, model, prefix = '/', config}) { 225 | server.route({ 226 | method: 'DELETE', 227 | path: path.join(prefix, model._plural, '{scope}'), 228 | handler: (request, reply) => { 229 | const include = utils_1.parseInclude(request); 230 | const where = utils_1.parseWhere(request); 231 | if (include instanceof Error) { 232 | return void reply(include); 233 | } 234 | const list = model.scope(request.params.scope).findAll({include: include, where: where}); 235 | Promise.all(list.map(instance => instance.destroy())); 236 | const listAsJSON = list.map((item) => item.toJSON()); 237 | reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON); 238 | }, 239 | config: config 240 | }); 241 | } 242 | 243 | update({server, model, prefix = '/', config}) { 244 | server.route({ 245 | method: 'PUT', 246 | path: path.join(prefix, model._singular, '{id}'), 247 | handler: (request, reply) => { 248 | const {id} = request.params; 249 | const instance = model.findById(id); 250 | if (!instance) { 251 | return void reply(boom_1.notFound(`${id} not found.`)); 252 | } 253 | instance.update(request.payload); 254 | reply(instance.toJSON()); 255 | }, 256 | config: config 257 | }); 258 | } 259 | } 260 | Object.defineProperty(exports, "__esModule", {value: true}); 261 | exports.default = Crud; 262 | /* 263 | The `models` option, becomes `permissions`, and can look like: 264 | 265 | ``` 266 | models: ['cat', 'dog'] 267 | ``` 268 | 269 | or 270 | 271 | ``` 272 | models: { 273 | cat: ['list', 'get'] 274 | , dog: true // all 275 | } 276 | ``` 277 | 278 | */ 279 | //# sourceMappingURL=crud.js.map -------------------------------------------------------------------------------- /src/get-config-for-method.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const ava_1 = require('ava'); 3 | const joi_1 = require('joi'); 4 | const get_config_for_method_ts_1 = require('./get-config-for-method.ts'); 5 | ava_1.default.beforeEach((t) => { 6 | t.context.models = ['MyModel']; 7 | t.context.scopes = ['aScope']; 8 | t.context.attributeValidation = { 9 | myKey: joi_1.default.any(), 10 | }; 11 | t.context.associationValidation = { 12 | include: joi_1.default.array().items(joi_1.default.string().valid(t.context.models)), 13 | }; 14 | t.context.config = { 15 | cors: {}, 16 | }; 17 | }); 18 | ava_1.default('validate.query seqeulizeOperators', (t) => { 19 | get_config_for_method_ts_1.whereMethods.forEach((method) => { 20 | const configForMethod = get_config_for_method_ts_1.default({ method: method }); 21 | const { query } = configForMethod.validate; 22 | t.truthy(query, `applies query validation for ${method}`); 23 | Object.keys(get_config_for_method_ts_1.sequelizeOperators).forEach((operator) => { 24 | t.ifError(query.validate({ [operator]: true }).error, `applies sequelize operator "${operator}" in validate.where for ${method}`); 25 | }); 26 | t.truthy(query.validate({ notAThing: true }).error, 'errors on a non-valid key'); 27 | }); 28 | }); 29 | ava_1.default('validate.query attributeValidation', (t) => { 30 | const { attributeValidation } = t.context; 31 | get_config_for_method_ts_1.whereMethods.forEach((method) => { 32 | const configForMethod = get_config_for_method_ts_1.default({ method: method, attributeValidation: attributeValidation }); 33 | const { query } = configForMethod.validate; 34 | Object.keys(attributeValidation).forEach((key) => { 35 | t.ifError(query.validate({ [key]: true }).error, `applies attributeValidation (${key}) to validate.query`); 36 | }); 37 | t.truthy(query.validate({ notAThing: true }).error, 'errors on a non-valid key'); 38 | }); 39 | }); 40 | ava_1.default('query attributeValidation w/ config as plain object', (t) => { 41 | const { attributeValidation } = t.context; 42 | const config = { 43 | validate: { 44 | query: { 45 | aKey: joi_1.default.boolean(), 46 | }, 47 | }, 48 | }; 49 | get_config_for_method_ts_1.whereMethods.forEach((method) => { 50 | const configForMethod = get_config_for_method_ts_1.default({ 51 | method: method, 52 | attributeValidation: attributeValidation, 53 | config: config, 54 | }); 55 | const { query } = configForMethod.validate; 56 | const keys = [ 57 | ...Object.keys(attributeValidation), 58 | ...Object.keys(config.validate.query), 59 | ]; 60 | keys.forEach((key) => { 61 | t.ifError(query.validate({ [key]: true }).error, `applies ${key} to validate.query`); 62 | }); 63 | t.truthy(query.validate({ notAThing: true }).error, 'errors on a non-valid key'); 64 | }); 65 | }); 66 | ava_1.default('query attributeValidation w/ config as joi object', (t) => { 67 | const { attributeValidation } = t.context; 68 | const queryKeys = { 69 | aKey: joi_1.default.boolean(), 70 | }; 71 | const config = { 72 | validate: { 73 | query: joi_1.default.object().keys(queryKeys), 74 | }, 75 | }; 76 | get_config_for_method_ts_1.whereMethods.forEach((method) => { 77 | const configForMethod = get_config_for_method_ts_1.default({ 78 | method: method, 79 | attributeValidation: attributeValidation, 80 | config: config, 81 | }); 82 | const { query } = configForMethod.validate; 83 | const keys = [ 84 | ...Object.keys(attributeValidation), 85 | ...Object.keys(queryKeys), 86 | ]; 87 | keys.forEach((key) => { 88 | t.ifError(query.validate({ [key]: true }).error, `applies ${key} to validate.query`); 89 | }); 90 | t.truthy(query.validate({ notAThing: true }).error, 'errors on a non-valid key'); 91 | }); 92 | }); 93 | ava_1.default('validate.query associationValidation', (t) => { 94 | const { attributeValidation, associationValidation, models } = t.context; 95 | get_config_for_method_ts_1.includeMethods.forEach((method) => { 96 | const configForMethod = get_config_for_method_ts_1.default({ 97 | method: method, 98 | attributeValidation: attributeValidation, 99 | associationValidation: associationValidation, 100 | }); 101 | const { query } = configForMethod.validate; 102 | Object.keys(attributeValidation).forEach((key) => { 103 | t.ifError(query.validate({ [key]: true }).error, `applies attributeValidation (${key}) to validate.query when include should be applied`); 104 | }); 105 | Object.keys(associationValidation).forEach((key) => { 106 | t.ifError(query.validate({ [key]: models }).error, `applies associationValidation (${key}) to validate.query when include should be applied`); 107 | }); 108 | t.truthy(query.validate({ notAThing: true }).error, 'errors on a non-valid key'); 109 | }); 110 | }); 111 | ava_1.default('query associationValidation w/ config as plain object', (t) => { 112 | const { associationValidation, models } = t.context; 113 | const config = { 114 | validate: { 115 | query: { 116 | aKey: joi_1.default.boolean(), 117 | }, 118 | }, 119 | }; 120 | get_config_for_method_ts_1.includeMethods.forEach((method) => { 121 | const configForMethod = get_config_for_method_ts_1.default({ 122 | method: method, 123 | associationValidation: associationValidation, 124 | config: config, 125 | }); 126 | const { query } = configForMethod.validate; 127 | Object.keys(associationValidation).forEach((key) => { 128 | t.ifError(query.validate({ [key]: models }).error, `applies ${key} to validate.query`); 129 | }); 130 | Object.keys(config.validate.query).forEach((key) => { 131 | t.ifError(query.validate({ [key]: true }).error, `applies ${key} to validate.query`); 132 | }); 133 | t.truthy(query.validate({ notAThing: true }).error, 'errors on a non-valid key'); 134 | }); 135 | }); 136 | ava_1.default('query associationValidation w/ config as joi object', (t) => { 137 | const { associationValidation, models } = t.context; 138 | const queryKeys = { 139 | aKey: joi_1.default.boolean(), 140 | }; 141 | const config = { 142 | validate: { 143 | query: joi_1.default.object().keys(queryKeys), 144 | }, 145 | }; 146 | get_config_for_method_ts_1.includeMethods.forEach((method) => { 147 | const configForMethod = get_config_for_method_ts_1.default({ 148 | method: method, 149 | associationValidation: associationValidation, 150 | config: config, 151 | }); 152 | const { query } = configForMethod.validate; 153 | Object.keys(associationValidation).forEach((key) => { 154 | t.ifError(query.validate({ [key]: models }).error, `applies ${key} to validate.query`); 155 | }); 156 | Object.keys(queryKeys).forEach((key) => { 157 | t.ifError(query.validate({ [key]: true }).error, `applies ${key} to validate.query`); 158 | }); 159 | t.truthy(query.validate({ notAThing: true }).error, 'errors on a non-valid key'); 160 | }); 161 | }); 162 | ava_1.default('validate.payload associationValidation', (t) => { 163 | const { attributeValidation } = t.context; 164 | get_config_for_method_ts_1.payloadMethods.forEach((method) => { 165 | const configForMethod = get_config_for_method_ts_1.default({ method: method, attributeValidation: attributeValidation }); 166 | const { payload } = configForMethod.validate; 167 | Object.keys(attributeValidation).forEach((key) => { 168 | t.ifError(payload.validate({ [key]: true }).error, `applies attributeValidation (${key}) to validate.payload`); 169 | }); 170 | t.truthy(payload.validate({ notAThing: true }).error, 'errors on a non-valid key'); 171 | }); 172 | }); 173 | ava_1.default('payload attributeValidation w/ config as plain object', (t) => { 174 | const { attributeValidation } = t.context; 175 | const config = { 176 | validate: { 177 | payload: { 178 | aKey: joi_1.default.boolean(), 179 | }, 180 | }, 181 | }; 182 | get_config_for_method_ts_1.payloadMethods.forEach((method) => { 183 | const configForMethod = get_config_for_method_ts_1.default({ 184 | method: method, 185 | attributeValidation: attributeValidation, 186 | config: config, 187 | }); 188 | const { payload } = configForMethod.validate; 189 | const keys = [ 190 | ...Object.keys(attributeValidation), 191 | ...Object.keys(config.validate.payload), 192 | ]; 193 | keys.forEach((key) => { 194 | t.ifError(payload.validate({ [key]: true }).error, `applies ${key} to validate.payload`); 195 | }); 196 | t.truthy(payload.validate({ notAThing: true }).error, 'errors on a non-valid key'); 197 | }); 198 | }); 199 | ava_1.default('payload attributeValidation w/ config as joi object', (t) => { 200 | const { attributeValidation } = t.context; 201 | const payloadKeys = { 202 | aKey: joi_1.default.boolean(), 203 | }; 204 | const config = { 205 | validate: { 206 | payload: joi_1.default.object().keys(payloadKeys), 207 | }, 208 | }; 209 | get_config_for_method_ts_1.payloadMethods.forEach((method) => { 210 | const configForMethod = get_config_for_method_ts_1.default({ 211 | method: method, 212 | attributeValidation: attributeValidation, 213 | config: config, 214 | }); 215 | const { payload } = configForMethod.validate; 216 | const keys = [ 217 | ...Object.keys(attributeValidation), 218 | ...Object.keys(payloadKeys), 219 | ]; 220 | keys.forEach((key) => { 221 | t.ifError(payload.validate({ [key]: true }).error, `applies ${key} to validate.payload`); 222 | }); 223 | t.truthy(payload.validate({ notAThing: true }).error, 'errors on a non-valid key'); 224 | }); 225 | }); 226 | ava_1.default('validate.params scopeParamsMethods', (t) => { 227 | const { scopes } = t.context; 228 | get_config_for_method_ts_1.scopeParamsMethods.forEach((method) => { 229 | const configForMethod = get_config_for_method_ts_1.default({ method: method, scopes: scopes }); 230 | const { params } = configForMethod.validate; 231 | scopes.forEach((key) => { 232 | t.ifError(params.validate({ scope: key }).error, `applies "scope: ${key}" to validate.params`); 233 | }); 234 | t.truthy(params.validate({ scope: 'notAthing' }).error, 'errors on a non-valid key'); 235 | }); 236 | }); 237 | ava_1.default('params scopeParamsMethods w/ config as plain object', (t) => { 238 | const { scopes } = t.context; 239 | const config = { 240 | validate: { 241 | params: { 242 | aKey: joi_1.default.boolean(), 243 | }, 244 | }, 245 | }; 246 | get_config_for_method_ts_1.scopeParamsMethods.forEach((method) => { 247 | const configForMethod = get_config_for_method_ts_1.default({ 248 | method: method, 249 | scopes: scopes, 250 | config: config, 251 | }); 252 | const { params } = configForMethod.validate; 253 | scopes.forEach((key) => { 254 | t.ifError(params.validate({ scope: key }).error, `applies "scope: ${key}" to validate.params`); 255 | }); 256 | Object.keys(config.validate.params).forEach((key) => { 257 | t.ifError(params.validate({ [key]: true }).error, `applies ${key} to validate.params`); 258 | }); 259 | t.truthy(params.validate({ notAThing: true }).error, 'errors on a non-valid key'); 260 | }); 261 | }); 262 | ava_1.default('params scopeParamsMethods w/ config as joi object', (t) => { 263 | const { scopes } = t.context; 264 | const paramsKeys = { 265 | aKey: joi_1.default.boolean(), 266 | }; 267 | const config = { 268 | validate: { 269 | params: joi_1.default.object().keys(paramsKeys), 270 | }, 271 | }; 272 | get_config_for_method_ts_1.scopeParamsMethods.forEach((method) => { 273 | const configForMethod = get_config_for_method_ts_1.default({ 274 | method: method, 275 | scopes: scopes, 276 | config: config, 277 | }); 278 | const { params } = configForMethod.validate; 279 | scopes.forEach((key) => { 280 | t.ifError(params.validate({ scope: key }).error, `applies "scope: ${key}" to validate.params`); 281 | }); 282 | Object.keys(paramsKeys).forEach((key) => { 283 | t.ifError(params.validate({ [key]: true }).error, `applies ${key} to validate.params`); 284 | }); 285 | t.truthy(params.validate({ notAThing: true }).error, 'errors on a non-valid key'); 286 | }); 287 | }); 288 | ava_1.default('validate.payload idParamsMethods', (t) => { 289 | get_config_for_method_ts_1.idParamsMethods.forEach((method) => { 290 | const configForMethod = get_config_for_method_ts_1.default({ method: method }); 291 | const { params } = configForMethod.validate; 292 | t.ifError(params.validate({ id: 'aThing' }).error, 'applies id to validate.params'); 293 | }); 294 | }); 295 | ava_1.default('does not modify initial config on multiple passes', (t) => { 296 | const { config } = t.context; 297 | const originalConfig = {}; 298 | }, ...config); 299 | get_config_for_method_ts_1.whereMethods.forEach((method) => { 300 | get_config_for_method_ts_1.default({ method: method, }, ...t.context); 301 | }); 302 | ; 303 | t.deepEqual(config, originalConfig, 'does not modify the original config object'); 304 | ; 305 | //# sourceMappingURL=get-config-for-method.test.js.map --------------------------------------------------------------------------------