├── test ├── .gitkeep ├── mocha.opts ├── config │ └── config.js ├── models │ ├── index.js │ ├── requestlogs.js │ └── trash.js ├── controllers │ └── initialize.js ├── services │ ├── logger.js │ ├── database.js │ ├── validation.js │ ├── encryption.js │ ├── response.js │ └── queue.js └── routes │ ├── initialize.js │ └── index.js ├── config ├── .gitkeep ├── index.js ├── development.js └── production.js ├── services ├── .gitkeep ├── database │ ├── index.js │ ├── redis.js │ ├── mongo.js │ ├── logMongo.js │ └── sql.js ├── response │ ├── index.js │ ├── notFound.js │ ├── forbidden.js │ ├── badRequest.js │ ├── unauthorized.js │ ├── unprocessable.js │ ├── serverError.js │ └── ok.js ├── queue │ ├── Model.js │ ├── workers.js │ ├── index.js │ ├── clock.js │ └── jobs.js ├── validator │ └── index.js ├── request │ ├── Model.js │ └── index.js ├── logger │ └── index.js └── encryption │ └── index.js ├── controllers ├── .gitkeep └── Initialize.js ├── _config.yml ├── .jshintignore ├── .eslintignore ├── mkdocs.yml ├── clock └── Dockerfile ├── workers └── Dockerfile ├── Dockerfile ├── routes ├── initialize.js └── index.js ├── template ├── model_api.tmpl ├── route.tmpl ├── model_sql.tmpl ├── model.tmpl ├── model_sql_test.tmpl ├── model_test.tmpl └── route_test.tmpl ├── .editorconfig ├── .gitignore ├── .travis.yml ├── models ├── index.js ├── Trash.js └── RequestLogs.js ├── .eslintrc ├── TODO.md ├── .eslintrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── app.js ├── package.json ├── docker-compose.yml ├── .jshintrc ├── docs └── index.md ├── README.md └── gulpfile.js /test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage 3 | template/ 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --timeout 20000 3 | --exit -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage 3 | template/ 4 | views/ 5 | docs/ -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Express REST API Generator Documentation 2 | theme: readthedocs 3 | -------------------------------------------------------------------------------- /clock/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY ./package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | CMD [ "npm", "run", "clock" ] 12 | -------------------------------------------------------------------------------- /workers/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY ./package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | CMD [ "npm", "run", "workers" ] 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 8080 12 | 13 | CMD [ "npm", "start" ] 14 | -------------------------------------------------------------------------------- /routes/initialize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var express = require('express'); 3 | var router = express.Router(); 4 | var initializeController = require('../controllers/Initialize'); 5 | 6 | // set tag 7 | router.get('/initialize', initializeController.init); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /services/database/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var databases = { 4 | logMongo: require('./logMongo'), 5 | mongo: require('./mongo'), 6 | redis: require('./redis'), 7 | api: require('./api'), 8 | sql: require('./sql') 9 | }; 10 | 11 | module.exports = databases; 12 | -------------------------------------------------------------------------------- /template/model_api.tmpl: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Db = require('../services/database').api; 4 | 5 | var Model = new Db('<%= baseurl %>','<%= endpoint %>'/*, { 6 | authorization: '72624642hfbsbsyrwywr' 7 | }*/); 8 | 9 | Model._mongoose = Db._mongoose; 10 | module.exports = Model; 11 | -------------------------------------------------------------------------------- /test/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai'); 3 | chai.should(); 4 | 5 | var config = require('../../config'); 6 | 7 | describe('#Config test', function(){ 8 | it('should be an object', function(done){ 9 | config.should.be.an('object'); 10 | done(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | chai.should(); 5 | 6 | var models = require('../../models'); 7 | describe('#Models test', function(){ 8 | it('should return an object', function(done){ 9 | models.should.be.an('object'); 10 | done(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /services/database/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var redis = require('redis'); 3 | var config = require('../../config'); 4 | var log = require('../../services/logger'); 5 | 6 | var client = redis.createClient(config.redisURL); 7 | 8 | client.on('error', log.error); 9 | 10 | client.on('connect', function() { 11 | log.info('Redis database connection successful'); 12 | }); 13 | 14 | module.exports = client; 15 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var development = require('./development'); 3 | var production = require('./production'); 4 | 5 | if (process.env.NODE_ENV === 'development') { 6 | module.exports = development; 7 | } 8 | else if (process.env.NODE_ENV === 'production') { 9 | module.exports = production; 10 | } 11 | else { 12 | module.exports = development; 13 | } 14 | // ToDo: Test for production and development senarios 15 | -------------------------------------------------------------------------------- /controllers/Initialize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var encryption = require('../services/encryption'); 3 | var config = require('../config'); 4 | var debug = require('debug')('initialize'); 5 | 6 | module.exports = { 7 | init: function(req, res, next){ 8 | encryption.generateKey() 9 | .then(function(resp){ 10 | res.ok({'x-tag': resp}); 11 | }) 12 | .catch(function(err){ 13 | next(err); 14 | }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /services/response/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | 4 | module.exports = function(req, res, next){ 5 | var responseTypes = { 6 | ok: require('./ok'), 7 | badRequest: require('./badRequest'), 8 | forbidden: require('./forbidden'), 9 | notFound: require('./notFound'), 10 | serverError: require('./serverError'), 11 | unauthorized: require('./unauthorized'), 12 | unprocessable: require('./unprocessable') 13 | }; 14 | 15 | res = _.extend(res, responseTypes); 16 | next(); 17 | }; 18 | -------------------------------------------------------------------------------- /services/response/notFound.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var log = require('../logger'); 3 | var _ = require('lodash'); 4 | var queue = require('../queue'); 5 | 6 | module.exports = function(){ 7 | log.warn('Sending 404 response: '+'not found'); 8 | var req = this.req; 9 | var res = this; 10 | 11 | // Dump it in the queue 12 | var response = {response: {status: 'error', message: 'not found'}}; 13 | response.requestId = req.requestId; 14 | 15 | queue.create('logResponse', response) 16 | .save(); 17 | 18 | this.status(404).json({status: 'error', message: 'not found'}); 19 | }; 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js,py}] 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 4 17 | 18 | # Tab indentation (no size specified) 19 | [Makefile] 20 | indent_style = tab 21 | 22 | # Matches the exact files either package.json or .travis.yml 23 | [{package.json,.travis.yml}] 24 | indent_style = space 25 | indent_size = 2 -------------------------------------------------------------------------------- /services/database/mongo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var mongoose = require('mongoose'); 3 | var config = require('../../config'); 4 | var log = require('../../services/logger'); 5 | 6 | mongoose.Promise = require('q').Promise; 7 | // Connect to DB 8 | var mongooseConfig = {}; 9 | 10 | if(config.env === 'production'){ 11 | mongooseConfig.autoIndex = false; 12 | } 13 | 14 | var db = mongoose.createConnection(config.mongoURL, mongooseConfig); 15 | 16 | db.on('error', log.error); 17 | db.once('open', function() { 18 | log.info('MongoDB database connection successful'); 19 | }); 20 | 21 | module.exports = db; 22 | module.exports._mongoose = mongoose; 23 | -------------------------------------------------------------------------------- /services/database/logMongo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var mongoose = require('mongoose'); 3 | var config = require('../../config'); 4 | var log = require('../../services/logger'); 5 | 6 | mongoose.Promise = require('q').Promise; 7 | // Connect to DB 8 | var mongooseConfig = {}; 9 | 10 | if(config.env === 'production'){ 11 | mongooseConfig.autoIndex = false; 12 | } 13 | 14 | var db = mongoose.createConnection(config.logMongoURL, mongooseConfig); 15 | 16 | db.on('error', log.error); 17 | db.once('open', function() { 18 | log.info('Log mongoDB database connection successful'); 19 | }); 20 | 21 | module.exports = db; 22 | module.exports._mongoose = mongoose; 23 | -------------------------------------------------------------------------------- /.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 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | #Mac 40 | .DS_Store 41 | -------------------------------------------------------------------------------- /test/controllers/initialize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | chai.should(); 5 | var chaiAsPromised = require('chai-as-promised'); 6 | chai.use(chaiAsPromised); 7 | var initialize = require('../../controllers/Initialize.js'); 8 | 9 | describe('Initialize controller', function(){ 10 | it('should return a string', function(done){ 11 | var next = function(err){ 12 | done(err); 13 | }; 14 | var res = {}; 15 | res.ok = function(data){ 16 | data.should.be.an.object; /* jslint ignore:line */ 17 | data.should.have.property('x-tag'); 18 | data['x-tag'].should.be.a.string; /* jslint ignore:line */ 19 | done(); 20 | }; 21 | var req; 22 | initialize.init(req, res, next); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | services: 4 | - mongodb 5 | - redis-server 6 | - mysql 7 | env: 8 | - MONGOLAB_URL=mongodb://127.0.0.1/snipe LOG_MONGOLAB_URL=mongodb://127.0.0.1/snipelogs REDIS_URL=redis://127.0.0.1/1 SECURE_MODE=true NO_CACHE=no SQL_HOST=127.0.0.1 9 | addons: 10 | apt: 11 | sources: 12 | - mongodb-3.0-precise 13 | packages: 14 | - mongodb-org-server 15 | node_js: 16 | - '12' 17 | before_script: 18 | - sleep 15 19 | - npm install -g gulp mocha gulp-cli codecov istanbul 20 | before_install: 21 | - mysql -e 'CREATE DATABASE snipe;' 22 | install: 23 | - npm install 24 | - gulp service -n api 25 | - gulp service -n test -b http://localhost:8080 -e apis 26 | - gulp service --sql -n apisql 27 | - npm start >> femi.log . & 28 | script: 29 | - sleep 15 30 | - >- 31 | istanbul cover ./node_modules/mocha/bin/_mocha --exit --report lcovonly -- -R spec 32 | && codecov 33 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | 4 | var models = {}; 5 | var normalizedPath = require('path').join(__dirname, './'); 6 | 7 | var files = require('fs').readdirSync(normalizedPath); 8 | var filesCount = files.length * 1; 9 | var count = 0; 10 | var associate = function(models){ 11 | _.forOwn(models, function(value, key){ 12 | if(value.associate){ 13 | value.associate(models); 14 | } 15 | }); 16 | }; 17 | 18 | files.forEach(function(file) { 19 | count = count + 1; 20 | var splitFileName = file.split('.'); 21 | if (splitFileName[0] !== 'index') { 22 | models[splitFileName[0]] = require('./' + splitFileName[0]); 23 | } 24 | if (count === filesCount) { 25 | associate(models); 26 | } 27 | }); 28 | 29 | module.exports = models; 30 | // Todo: Automatically generate tests with the schema structure 31 | // Add the option of elasticsearch for API search 32 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly", 11 | "it": "readonly", 12 | "process": "readonly", 13 | "describe": "readonly", 14 | "before": "readonly", 15 | "afterEach": "readonly", 16 | "after": "readonly", 17 | "Buffer": "readonly", 18 | "__dirname": "readonly" 19 | }, 20 | "parserOptions": { 21 | "ecmaVersion": 2018 22 | }, 23 | "rules": { 24 | "semi": ["error", "always"], 25 | "camelcase": "off", 26 | "comma-dangle": "off", 27 | "indent": 2, 28 | "prefer-const": ["error", { 29 | "destructuring": "any", 30 | "ignoreReadBeforeAssign": false 31 | }], 32 | "no-unused-vars": "off" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ### TODOs 2 | | Filename | line # | TODO 3 | |:------|:------:|:------ 4 | | app.js | 63 | Write a complete Documentation for this project 5 | | config/index.js | 14 | Test for production and development senarios 6 | | models/index.js | 30 | Automatically generate tests with the schema structure 7 | | routes/index.js | 257 | Test API versioning 8 | | routes/index.js | 258 | Test rate limiting 9 | | routes/index.js | 259 | Test complete route Loader test 10 | | routes/index.js | 260 | Test _sanitizeRequestUrl middleware function 11 | | routes/index.js | 261 | Test _allRequestData middleware function for default value scenario 12 | | routes/index.js | 262 | Make Log requests testable and write unit tests for it 13 | | routes/index.js | 263 | Develop the route loader into a separate node module to be publish on npm 14 | | routes/index.js | 264 | Develop all services onto separate node module to be publish on npm 15 | | services/logger/index.js | 56 | Test Error Handler 16 | | services/request/index.js | 90 | Test request module -------------------------------------------------------------------------------- /services/queue/Model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var db = require('../database').logMongo; 4 | 5 | var collection = 'Clock'; 6 | 7 | var debug = require('debug')(collection); 8 | 9 | var schemaObject = { 10 | job: { 11 | type: 'String' 12 | }, 13 | crontab: { 14 | type: 'String' 15 | }, 16 | name: { 17 | type: 'String', 18 | unique: true 19 | }, 20 | enabled: { 21 | type: 'Boolean', 22 | default: true 23 | }, 24 | arguments: { 25 | type: db._mongoose.Schema.Types.Mixed 26 | }, 27 | lastRunAt: { 28 | type: db._mongoose.Schema.Types.Mixed 29 | } 30 | }; 31 | 32 | schemaObject.createdAt = { 33 | type: 'Date', 34 | default: Date.now 35 | }; 36 | 37 | schemaObject.updatedAt = { 38 | type: 'Date' 39 | // default: Date.now 40 | }; 41 | 42 | // Let us define our schema 43 | var Schema = new db._mongoose.Schema(schemaObject); 44 | 45 | var Model = db.model(collection, Schema); 46 | 47 | module.exports = Model; 48 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly", 11 | "it": "readonly", 12 | "process": "readonly", 13 | "describe": "readonly", 14 | "before": "readonly", 15 | "afterEach": "readonly", 16 | "after": "readonly", 17 | "Buffer": "readonly", 18 | "__dirname": "readonly" 19 | }, 20 | "parserOptions": { 21 | "ecmaVersion": 2018 22 | }, 23 | "rules": { 24 | "indent": [ 25 | "error", 26 | 4 27 | ], 28 | "linebreak-style": [ 29 | "error", 30 | "unix" 31 | ], 32 | "quotes": [ 33 | "error", 34 | "single" 35 | ], 36 | "semi": [ 37 | "error", 38 | "always" 39 | ], 40 | "no-unused-vars": "off" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /services/response/forbidden.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var log = require('../logger'); 3 | var _ = require('lodash'); 4 | var queue = require('../queue'); 5 | 6 | module.exports = function(data, message){ 7 | log.warn('Sending forbidden response: ', data, message || 'forbidden'); 8 | var req = this.req; 9 | var res = this; 10 | 11 | // Dump it in the queue 12 | var response = {response: {status: 'error', data: data, message: message ? message : 'forbidden'}}; 13 | response.requestId = req.requestId; 14 | 15 | queue.create('logResponse', response) 16 | .save(); 17 | 18 | if (data !== undefined && data !== null) { 19 | if(Object.keys(data).length === 0 && JSON.stringify(data) === JSON.stringify({})){ 20 | data = data.toString(); 21 | } 22 | } 23 | 24 | if(data){ 25 | this.status(403).json({status: 'error', data: data, message: message ? message : 'forbidden'}); 26 | }else{ 27 | this.status(403).json({status: 'error', message: message ? message : 'forbidden'}); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /services/response/badRequest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var log = require('../logger'); 3 | var _ = require('lodash'); 4 | var queue = require('../queue'); 5 | 6 | module.exports = function(data, message){ 7 | log.warn('Sending bad request response: ', data, message || 'bad request'); 8 | var req = this.req; 9 | var res = this; 10 | 11 | // Dump it in the queue 12 | var response = {response: {status: 'error', data: data, message: message ? message : 'bad request'}}; 13 | response.requestId = req.requestId; 14 | 15 | queue.create('logResponse', response) 16 | .save(); 17 | 18 | if (data !== undefined && data !== null) { 19 | if(Object.keys(data).length === 0 && JSON.stringify(data) === JSON.stringify({})){ 20 | data = data.toString(); 21 | } 22 | } 23 | 24 | if(data){ 25 | this.status(400).json({status: 'error', data: data, message: message ? message : 'bad request'}); 26 | }else{ 27 | this.status(400).json({status: 'error', message: message ? message : 'bad request'}); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /services/response/unauthorized.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var log = require('../logger'); 3 | var _ = require('lodash'); 4 | var queue = require('../queue'); 5 | 6 | module.exports = function(data, message){ 7 | log.warn('sending unauthorized response: ', data, message || 'unauthorized'); 8 | var req = this.req; 9 | var res = this; 10 | 11 | // Dump it in the queue 12 | var response = {response: {status: 'error', data: data, message: message ? message : 'unauthorized'}}; 13 | response.requestId = req.requestId; 14 | 15 | queue.create('logResponse', response) 16 | .save(); 17 | 18 | if (data !== undefined && data !== null) { 19 | if(Object.keys(data).length === 0 && JSON.stringify(data) === JSON.stringify({})){ 20 | data = data.toString(); 21 | } 22 | } 23 | 24 | if(data){ 25 | this.status(401).json({status: 'error', data: data, message: message ? message : 'unauthorized'}); 26 | }else{ 27 | this.status(401).json({status: 'error', message: message ? message : 'unauthorized'}); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. 4 | 5 | Communication through any of the project channels must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 6 | 7 | We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to this project to do the same. 8 | 9 | If any member of the community violates this code of conduct, the maintainers of the project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate. 10 | 11 | If you are subject to or witness unacceptable behavior, or have any other concerns, please email us at [iolufemi@ymail.com](mailto:iolufemi@ymail.com) 12 | -------------------------------------------------------------------------------- /services/response/unprocessable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var log = require('../logger'); 3 | var _ = require('lodash'); 4 | var queue = require('../queue'); 5 | 6 | module.exports = function(data, message){ 7 | log.warn('Sending unprocessable entity response: ', data, message || 'unprocessable entity'); 8 | var req = this.req; 9 | var res = this; 10 | 11 | // Dump it in the queue 12 | var response = {response: {status: 'error', data: data, message: message ? message : 'unprocessable entity'}}; 13 | response.requestId = req.requestId; 14 | 15 | queue.create('logResponse', response) 16 | .save(); 17 | 18 | if (data !== undefined && data !== null) { 19 | if(Object.keys(data).length === 0 && JSON.stringify(data) === JSON.stringify({})){ 20 | data = data.toString(); 21 | } 22 | } 23 | 24 | if(data){ 25 | this.status(422).json({status: 'error', data: data, message: message ? message : 'unprocessable entity'}); 26 | }else{ 27 | this.status(422).json({status: 'error', message: message ? message : 'unprocessable entity'}); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Express REST API Generator 2 | 3 | 1. Clone the repo 4 | 2. Create a new branch for the bugfix, enhancement or feature you want to add. The branch name should follow this format. eg. feature/featureName or bugFix/someBugName or enhancement/someImprovement 5 | 3. When committing your changes, follow angular's conventional changelog for your commit messages. eg. `feat(featureName): some message to describe this change` or `fix(featureName): more information on the bug fix`. See more details here [https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/master/convention.md](https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/master/convention.md) 6 | 4. Push your changes to your repo 7 | 5. Create a pull request to this repo 8 | 9 | > NOTE: you are required to write a complete test suit for all changes you make. 10 | 11 | Please feel free to add your name to the contributor list when you make a contribution 12 | 13 | View our [code of conduct](https://github.com/iolufemi/Express-REST-API-Generator/blob/master/CODE_OF_CONDUCT.md). 14 | -------------------------------------------------------------------------------- /services/queue/workers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var queue = require('./'); 4 | var jobs = require('./jobs'); 5 | var config = require('../../config'); 6 | var concurrency = config.workerConcurrency * 1; 7 | // Sets the number of listeners to prevent the annoying memory leak error. 8 | var maxListeners = 20 * concurrency; 9 | queue.setMaxListeners(maxListeners); 10 | 11 | queue.process('searchIndex', concurrency, function(job, done){ 12 | jobs.createSearchTags(job.data, done); 13 | }); 14 | 15 | queue.process('logRequest', concurrency, function(job, done){ 16 | jobs.createRequestLog(job.data, done); 17 | }); 18 | 19 | queue.process('logResponse', concurrency, function(job, done){ 20 | jobs.updateRequestLog(job.data, done); 21 | }); 22 | 23 | queue.process('saveToTrash', concurrency, function(job, done){ 24 | jobs.saveToTrash(job.data, done); 25 | }); 26 | 27 | queue.process('sendWebhook', concurrency, function(job,done){ 28 | jobs.sendWebhook(job.data, done); 29 | }); 30 | 31 | queue.process('sendHTTPRequest', concurrency, function(job,done){ 32 | jobs.sendHTTPRequest(job.data, done); 33 | }); 34 | 35 | module.exports = queue; 36 | -------------------------------------------------------------------------------- /services/response/serverError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var log = require('../logger'); 3 | var _ = require('lodash'); 4 | var queue = require('../queue'); 5 | 6 | module.exports = function(data, message){ 7 | log.error('sending server error response: ', data, message || 'server error'); 8 | 9 | var req = this.req; 10 | var res = this; 11 | 12 | // Dump it in the queue 13 | var response = {response: {status: 'error', data: data, message: message ? message : 'server error'}}; 14 | response.requestId = req.requestId; 15 | queue.create('logResponse', response) 16 | .save(); 17 | 18 | if (data !== undefined && data !== null) { 19 | if(Object.keys(data).length === 0 && JSON.stringify(data) === JSON.stringify({})){ 20 | data = data.toString(); 21 | } 22 | } 23 | var statusCode; 24 | if(data.statusCode){ 25 | statusCode = data.statusCode; 26 | }else{ 27 | statusCode = 500; 28 | } 29 | 30 | if(data){ 31 | this.status(statusCode).json({status: 'error', data: data, message: message ? message : 'server error'}); 32 | }else{ 33 | this.status(statusCode).json({status: 'error', message: message ? message : 'server error'}); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /services/validator/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { check, validationResult } = require('express-validator'); 3 | var debug = require('debug')('validator'); 4 | 5 | module.exports = async function(req, res, next){ 6 | debug('starting validation check.'); 7 | debug('What we got: ', req.body); 8 | var parameters = req._required; 9 | var ignores = req._ignored; 10 | if(parameters.length){ 11 | var last = parameters.length - 1; 12 | for(var n in parameters){ 13 | if(parameters[n]){ 14 | debug('validating '+parameters[n]); 15 | await check(parameters[n], parameters[n]+' is required').trim().escape().notEmpty().run(req); 16 | 17 | if(n*1 === last){ 18 | debug('parameters: ', parameters[n]); 19 | debug('validation over, lets take it home...'); 20 | var errors = validationResult(req); 21 | if (!errors.isEmpty()) { 22 | res.badRequest(errors.array(), 'Validation error.'); 23 | }else{ 24 | next(); 25 | } 26 | } 27 | } 28 | } 29 | }else{ 30 | next(); 31 | } 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /services/request/Model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var db = require('../database').logMongo; 4 | 5 | var collection = 'APICalls'; 6 | 7 | var debug = require('debug')(collection); 8 | 9 | var schemaObject = { 10 | RequestId: { 11 | type: 'String', 12 | unique: true 13 | }, 14 | uri: { 15 | type: 'String', 16 | index: true 17 | }, 18 | method: { 19 | type: 'String' 20 | }, 21 | service: { 22 | type: 'String' 23 | }, 24 | data: { 25 | type: db._mongoose.Schema.Types.Mixed 26 | }, 27 | headers: { 28 | type: db._mongoose.Schema.Types.Mixed 29 | }, 30 | response: { 31 | type: db._mongoose.Schema.Types.Mixed 32 | }, 33 | responseStatusCode: { 34 | type: 'Number', 35 | index:true 36 | } 37 | }; 38 | 39 | schemaObject.createdAt = { 40 | type: 'Date', 41 | default: Date.now 42 | }; 43 | 44 | schemaObject.updatedAt = { 45 | type: 'Date' 46 | // default: Date.now 47 | }; 48 | 49 | schemaObject.retriedAt = { 50 | type: 'Date' 51 | // default: Date.now 52 | }; 53 | 54 | // Let us define our schema 55 | var Schema = new db._mongoose.Schema(schemaObject); 56 | 57 | var Model = db.model(collection, Schema); 58 | 59 | module.exports = Model; 60 | -------------------------------------------------------------------------------- /template/route.tmpl: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var express = require('express'); 3 | var router = express.Router(); 4 | var validator = require('../services/validator'); 5 | var <%= object %>sController = require('../controllers/<%= service %>s'); 6 | 7 | var service = '<%= object %>s'; 8 | 9 | // get <%= object %>s or search <%= object %>s 10 | router.get('/'+service, <%= object %>sController.find); 11 | 12 | // get <%= object %> 13 | router.get('/'+service+'/:id', <%= object %>sController.findOne); 14 | 15 | // To add validation, add a middlewave like the below. Works for just POST calls only 16 | // function(req,res,next){ 17 | // req._required = [ // _required should contain all the fails that are required 18 | // 'name', 19 | // 'name2' 20 | // ]; 21 | 22 | // next(); 23 | // }, validator, 24 | 25 | // create <%= object %>(s) a single <%= object %> object will create one <%= object %> while an array of <%= object %>s will create multiple <%= object %>s 26 | router.post('/'+service, <%= object %>sController.create); 27 | 28 | // update all records that matches the query 29 | router.put('/'+service, <%= object %>sController.update); 30 | 31 | // update a single record 32 | router.patch('/'+service+'/:id', <%= object %>sController.updateOne); 33 | 34 | // delete all records that matches the query 35 | router.delete('/'+service, <%= object %>sController.delete); 36 | 37 | // Delete a single record 38 | router.delete('/'+service+'/:id', <%= object %>sController.deleteOne); 39 | 40 | // restore a previously deleted record 41 | router.post('/'+service+'/:id/restore', <%= object %>sController.restore); 42 | 43 | module.exports = router; 44 | -------------------------------------------------------------------------------- /services/logger/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var log = require('winston'); 3 | var config = require('../../config'); 4 | var response = require('../response'); 5 | var bugsnag = require('bugsnag'); 6 | var winstonBugsnag = require('winston-bugsnag'); 7 | var winstonLoggly = require('winston-loggly-bulk'); 8 | 9 | if (config.env === 'production') { 10 | if (!config.bugsnagKey && !config.logglyToken) { 11 | log.add(log.transports.File, { filename: 'app-' + new Date().toDateString().split(' ').join('_') + '.log', level: 'warn' }); 12 | log.remove(log.transports.Console); 13 | } 14 | else { 15 | if (config.bugsnagKey) { 16 | bugsnag.register(config.bugsnagKey); 17 | log.add(winstonBugsnag, { level: 'warn' }); 18 | } 19 | if (config.logglyToken) { 20 | log.add(log.transports.Loggly, { 21 | token: config.logglyToken, 22 | subdomain: config.logglySubdomain, 23 | tags: [config.logglyTag], 24 | json: true, 25 | level: 'warn' 26 | }); 27 | } 28 | log.add(log.transports.File, { filename: 'app-' + new Date().toDateString().split(' ').join('_') + '.log', level: 'warn' }); 29 | log.remove(log.transports.Console); 30 | } 31 | } 32 | 33 | module.exports = log; 34 | module.exports.errorHandler = function(err, req, res, next) { // jshint ignore:line 35 | response(req, res, next); 36 | log.error(err); 37 | if (err.statusCode === 404) { 38 | res.notFound(err); 39 | } 40 | else if (err.statusCode === 401) { 41 | res.unauthorized(err); 42 | } 43 | else if (err.statusCode === 400) { 44 | res.badRequest(err); 45 | } 46 | else if (err.statusCode === 403) { 47 | res.forbidden(err); 48 | } 49 | else if (err.statusCode === 422) { 50 | res.unprocessable(err); 51 | } 52 | else { 53 | res.serverError(err); 54 | } 55 | }; 56 | // ToDo: Test Error Handler 57 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var cluster = require('cluster'); 3 | var config = require('./config'); 4 | var log = require('./services/logger'); 5 | var basicAuth = require('basic-auth-connect'); 6 | var express = require('express'); 7 | var app; 8 | var server; 9 | 10 | if (cluster.isMaster && config.env === 'production') { 11 | var kue = require('./services/queue').kue; 12 | app = express(); 13 | app.use(basicAuth(config.queueUIUsername, config.queueUIPassword)); 14 | app.use(kue.app); 15 | server = app.listen(config.queueUIPort, function () { 16 | var host = server.address().address; 17 | var port = server.address().port; 18 | log.info('Queue UI listening on host '+host+', port '+port+'!'); 19 | }); 20 | // Count the machine's CPUs 21 | var cpuCount = require('os').cpus().length; 22 | 23 | // Create a worker for each CPU 24 | for (var i = 0; i < cpuCount; i += 1) { 25 | cluster.fork(); 26 | } 27 | 28 | // Listen for dying workers 29 | cluster.on('exit', function (worker) { 30 | // Replace the dead worker, 31 | // we're not sentimental 32 | log.info('Worker %d died', worker.id); 33 | cluster.fork(); 34 | }); 35 | 36 | } else { 37 | app = express(); 38 | var router = require('./routes'); 39 | var express_enforces_ssl = require('express-enforces-ssl'); 40 | 41 | if(config.trustProxy === 'yes'){ 42 | app.enable('trust proxy'); 43 | } 44 | 45 | if(config.enforceSSL === 'yes'){ 46 | app.use(express_enforces_ssl()); 47 | } 48 | 49 | app.use('/',router); 50 | 51 | if(config.env === 'production'){ 52 | log.info('Worker %d running!', cluster.worker.id); 53 | } 54 | 55 | 56 | server = app.listen(config.port, function () { 57 | var host = server.address().address; 58 | var port = server.address().port; 59 | log.info('API server listening on host '+host+', port '+port+'!'); 60 | }); 61 | 62 | } 63 | // ToDo: Write a complete Documentation for this project 64 | -------------------------------------------------------------------------------- /test/services/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.SECURE_MODE = true; 4 | 5 | var chai = require('chai'); 6 | chai.should(); 7 | var config = require('../../config'); 8 | var chaiAsPromised = require('chai-as-promised'); 9 | chai.use(chaiAsPromised); 10 | var crypto = require('crypto'); 11 | 12 | // init 13 | 14 | var res = {}; 15 | var req = {}; 16 | var demoData = '{"el escribimos": "silencio es dorado"}'; 17 | var demoDataHash = crypto.createHash('sha512') 18 | .update(demoData) 19 | .digest('hex'); 20 | 21 | console.log('hash', demoDataHash); 22 | var nextChecker = false; 23 | var next = function(){ 24 | if(arguments.length > 0){ 25 | console.log(arguments[0]); 26 | }else{ 27 | nextChecker = true; 28 | } 29 | 30 | return nextChecker; 31 | }; 32 | res.json = function(data){ 33 | return res; 34 | }; 35 | 36 | res.status = function(status){ 37 | return res; 38 | }; 39 | 40 | var header = {}; 41 | res.set = function(key, value){ 42 | header[key] = value; 43 | return header[key]; 44 | }; 45 | req.get = function(key){ 46 | return header[key]; 47 | }; 48 | 49 | header.set = function(data){ 50 | header.temp = data; 51 | return header.temp; 52 | }; 53 | 54 | req.method = ''; 55 | 56 | // Testing the logger service 57 | 58 | var logger = require('../../services/logger'); 59 | describe('#Logger service test', function(){ 60 | it('should return an object', function(done){ 61 | logger.should.be.an('object'); 62 | done(); 63 | }); 64 | 65 | it('should have property warn, error, info, verbose, debug, silly and log', function(done){ 66 | logger.should.be.have.property('warn'); 67 | logger.should.be.have.property('error'); 68 | logger.should.be.have.property('info'); 69 | logger.should.be.have.property('log'); 70 | logger.should.be.have.property('verbose'); 71 | logger.should.be.have.property('debug'); 72 | logger.should.be.have.property('silly'); 73 | done(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/routes/initialize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai'); 3 | chai.should(); 4 | var config = require('../../config'); 5 | var chaiAsPromised = require('chai-as-promised'); 6 | chai.use(chaiAsPromised); 7 | var request = require('supertest'); 8 | var sinon = require('sinon'); 9 | var sinonChai = require('sinon-chai'); 10 | chai.use(sinonChai); 11 | 12 | 13 | var response = require('../../services/response'); 14 | 15 | var express = require('express'); 16 | var bodyParser = require('body-parser'); 17 | var router = require('../../routes/initialize'); 18 | var routers = require('../../routes'); 19 | 20 | var app = express(); 21 | 22 | app.use(bodyParser.urlencoded({ extended: false })); 23 | app.use(bodyParser.json()); 24 | app.use(routers._APICache); 25 | app.use(response); 26 | 27 | 28 | 29 | 30 | app.use('/',router); 31 | 32 | 33 | var agent = request.agent(app); 34 | var requestId; 35 | 36 | var res = {}; 37 | var req = {}; 38 | 39 | var nextChecker = false; 40 | var next = function(){ 41 | if(arguments.length > 0){ 42 | console.log(arguments[0]); 43 | }else{ 44 | nextChecker = true; 45 | } 46 | 47 | return nextChecker; 48 | }; 49 | res.json = function(data){ 50 | return res; 51 | }; 52 | 53 | res.badRequest = sinon.spy(); 54 | 55 | res.status = function(status){ 56 | return res; 57 | }; 58 | 59 | var header = {}; 60 | res.set = function(key, value){ 61 | header[key] = value; 62 | return header[key]; 63 | }; 64 | req.get = function(key){ 65 | return header[key]; 66 | }; 67 | 68 | header.set = function(data){ 69 | header.temp = data; 70 | return header.temp; 71 | }; 72 | 73 | req.method = ''; 74 | 75 | describe('/initialize', function(){ 76 | it('should return a string', function(done){ 77 | agent 78 | .get('/initialize') 79 | .then(function(resp){ 80 | resp.body.data['x-tag'].should.be.a('string'); 81 | done(); 82 | }) 83 | .catch(function(err){ 84 | done(err); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /services/database/sql.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('../../config'); 4 | var log = require('../../services/logger'); 5 | var mongoose = require('mongoose'); 6 | var Sequelize = require('sequelize'); 7 | var debug = require('debug')('sql'); 8 | 9 | var Op = Sequelize.Op; 10 | // enable only operators that you need 11 | var operatorsAliases = { 12 | // $and: Op.and, 13 | // $or: Op.or, 14 | $gt: Op.gt, 15 | $gte: Op.gte, 16 | $lt: Op.lt, 17 | $lte: Op.lte, 18 | // $ne: Op.ne, 19 | // $eq: Op.eq, 20 | // $not: Op.not, 21 | // $between: Op.between, 22 | // $notBetween: Op.notBetween, 23 | // $in: Op.in, 24 | // $notIn: Op.notIn, 25 | $like: Op.like, 26 | // $notLike: Op.notLike, 27 | // $iLike: Op.iLike, // PG Only 28 | // $notILike: Op.notILike, // PG Only 29 | // $regexp: Op.regexp, // PG/mySQL Only 30 | // $notReqexp: Op.notRegexp, // PG/mySQL Only 31 | // $iRegexp: Op.iRegexp, // PG Only 32 | // $notIRegexp: Op.notIRegexp, // PG Only 33 | // $overlap: Op.overlap, // PG Only 34 | // $contains: Op.contains, // PG Only 35 | // $contained: Op.contained, // PG Only 36 | // $any: Op.any // PG Only 37 | }; 38 | 39 | var options = { operatorsAliases: operatorsAliases }; 40 | options.host = config.SQLHost; 41 | options.port = config.SQLPort; 42 | options.dialect = config.SQLDriver; 43 | options.pool = { 44 | max: 5, 45 | min: 0, 46 | acquire: 30000, 47 | idle: 10000 48 | }; 49 | options.timezone = config.SQLTimezone; 50 | // SQLite only 51 | // options.storage = 'path/to/database.sqlite'; 52 | 53 | options.logging = function(log){ 54 | return (process.env.NODE_ENV === 'production') ? false : debug(log); 55 | }; 56 | 57 | var sequelize = new Sequelize(config.SQLDatabase, config.SQLUsername, config.SQLPassword, options); 58 | 59 | sequelize 60 | .authenticate() 61 | .then(function(){ 62 | log.info('SQL database connection successful'); 63 | }) 64 | .catch(function(err){ 65 | log.error('Unable to connect to the database: ', err); 66 | }); 67 | 68 | module.exports = sequelize; 69 | module.exports._sequelize = Sequelize; 70 | module.exports._mongoose = mongoose; 71 | -------------------------------------------------------------------------------- /services/queue/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var config = require('../../config'); 3 | var kue = require('kue'); 4 | var queue = kue.createQueue({ 5 | redis: config.redisURL 6 | }); 7 | var log = require('../../services/logger'); 8 | var Model = require('./Model'); 9 | 10 | // Clean Up Completed Job 11 | queue 12 | .on('job enqueue', function(id, type){ 13 | log.info( 'Job %s got queued of type %s', id, type ); 14 | }) 15 | .on('job complete', function(id, result){ 16 | log.info('ID: ',id,' Result: ',result); 17 | kue.Job.get(id, function(err, job){ 18 | if (err) { 19 | return false; 20 | }else{ 21 | job.remove(function(err){ 22 | if (err) { 23 | throw err; 24 | }else{ 25 | log.info('removed completed job #%d', job.id); 26 | } 27 | }); 28 | } 29 | }); 30 | }); 31 | 32 | // Graceful Shutdown 33 | process.once( 'SIGTERM', function ( sig ) { 34 | queue.shutdown( 5000, function(err) { 35 | log.warn( 'Queue shutting down: ', err||'' ); 36 | process.exit(0); 37 | }); 38 | }); 39 | 40 | // Error Handling 41 | queue.on( 'error', function( err ) { 42 | log.error( 'Queue Error... ', err ); 43 | }); 44 | 45 | // Handle uncaughtExceptions 46 | process.once( 'uncaughtException', function(err){ 47 | log.error( 'Something bad happened[uncaughtException]: ', err ); 48 | queue.shutdown( 5000, function(err2){ 49 | log.error( 'Kue shutdown due to uncaughtException: ', err2 || 'OK' ); 50 | process.exit( 0 ); 51 | }); 52 | }); 53 | 54 | // Pull Jobs out of stuck state 55 | // queue.watchStuckJobs(1000); // Uses KEYs. Shouldn't be used in production 56 | 57 | // Process Jobs Here 58 | module.exports = queue; 59 | module.exports.kue = kue; 60 | module.exports.addSchedule = function(crontab, name, job, data){ 61 | Model.create({crontab: crontab, name: name, job: job, arguments: data}) 62 | .then(function(){ 63 | // Silencio es dorado 64 | }) 65 | .catch(function(err){ 66 | log.error('Error scheduling job - ', err); 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /test/services/database.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.SECURE_MODE = true; 4 | 5 | var chai = require('chai'); 6 | chai.should(); 7 | var config = require('../../config'); 8 | var chaiAsPromised = require('chai-as-promised'); 9 | chai.use(chaiAsPromised); 10 | var crypto = require('crypto'); 11 | 12 | // init 13 | 14 | var res = {}; 15 | var req = {}; 16 | var demoData = '{"el escribimos": "silencio es dorado"}'; 17 | var demoDataHash = crypto.createHash('sha512') 18 | .update(demoData) 19 | .digest('hex'); 20 | 21 | console.log('hash', demoDataHash); 22 | var nextChecker = false; 23 | var next = function(){ 24 | if(arguments.length > 0){ 25 | console.log(arguments[0]); 26 | }else{ 27 | nextChecker = true; 28 | } 29 | 30 | return nextChecker; 31 | }; 32 | res.json = function(data){ 33 | return res; 34 | }; 35 | 36 | res.status = function(status){ 37 | return res; 38 | }; 39 | 40 | var header = {}; 41 | res.set = function(key, value){ 42 | header[key] = value; 43 | return header[key]; 44 | }; 45 | req.get = function(key){ 46 | return header[key]; 47 | }; 48 | 49 | header.set = function(data){ 50 | header.temp = data; 51 | return header.temp; 52 | }; 53 | 54 | req.method = ''; 55 | 56 | 57 | // Testing database service 58 | 59 | var databases = require('../../services/database'); 60 | describe('#Database Service test', function(){ 61 | it('should return an object', function(done){ 62 | databases.should.be.an('object'); 63 | done(); 64 | }); 65 | }); 66 | 67 | 68 | var mongodb = require('../../services/database').mongo; 69 | 70 | describe('#MongoDB database service test', function(){ 71 | it('should exist as a function',function(done){ 72 | mongodb.should.exist; /* jslint ignore:line */ 73 | done(); 74 | }); 75 | }); 76 | 77 | var logmongodb = require('../../services/database').logMongo; 78 | 79 | describe('#MongoDB database service test', function(){ 80 | it('should exist as a function',function(done){ 81 | logmongodb.should.exist; /* jslint ignore:line */ 82 | done(); 83 | }); 84 | }); 85 | 86 | var redisdb = require('../../services/database').redis; 87 | 88 | describe('#Redis database service test', function(){ 89 | it('should exist as a function',function(done){ 90 | redisdb.should.exist; /* jslint ignore:line */ 91 | done(); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /template/model_sql.tmpl: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var db = require('../services/database').sql; 4 | var queue = require('../services/queue'); 5 | 6 | var table = '<%= service %>s'; 7 | var debug = require('debug')(table); 8 | 9 | // Define schema 10 | var schemaObject = { 11 | // ++++++++++++++ Modify to your own schema ++++++++++++++++++ 12 | name: { 13 | type: db._sequelize.STRING 14 | }, 15 | someOtherStringData: { 16 | type: db._sequelize.STRING 17 | }, 18 | toPop: { 19 | type: db._sequelize.INTEGER 20 | } 21 | // ++++++++++++++ Modify to your own schema ++++++++++++++++++ 22 | }; 23 | 24 | 25 | schemaObject.owner = { 26 | type: db._sequelize.STRING 27 | }; 28 | 29 | schemaObject.createdBy = { 30 | type: db._sequelize.STRING 31 | }; 32 | 33 | schemaObject.client = { 34 | type: db._sequelize.STRING 35 | }; 36 | 37 | schemaObject.developer = { 38 | type: db._sequelize.STRING 39 | }; 40 | 41 | schemaObject.tags = { 42 | type: db._sequelize.STRING 43 | }; 44 | 45 | schemaObject._id = { 46 | type: db._sequelize.INTEGER, 47 | primaryKey: true, 48 | autoIncrement: true 49 | }; 50 | 51 | 52 | // Define the table 53 | var <%= service %>s = db.define(table, schemaObject, { 54 | // Don't really delete data 55 | // paranoid: true, 56 | // define indexes here 57 | indexes:[{ 58 | fields:['tags'] 59 | }, 60 | { 61 | unique: true, 62 | fields:['_id'] 63 | }] 64 | }); 65 | 66 | <%= service %>s.associate = function (models) { 67 | // models.<%= service %>s.belongsTo(models.toPop, { foreignKey: 'toPop', sourceKey: '_id' }); 68 | }; 69 | 70 | // <%= service %>s.hasMany(<%= service %>s, {foreignKey: 'toPop', sourceKey: '_id'}); 71 | 72 | // Adding hooks 73 | <%= service %>s.afterCreate(function(user, options) { 74 | // Indexing for search 75 | var ourDoc = user.dataValues; 76 | ourDoc.isSQL = true; 77 | ourDoc.model = table; 78 | 79 | // Dump it in the queue 80 | queue.create('searchIndex', ourDoc) 81 | .save(); 82 | }); 83 | 84 | <%= service %>s.search = function(string){ 85 | return <%= service %>s.findAll({ 86 | where: { 87 | tags: { 88 | $like: '%'+string 89 | } 90 | } 91 | }); 92 | }; 93 | 94 | <%= service %>s.sync(); 95 | 96 | <%= service %>s.transaction = db.transaction; 97 | 98 | module.exports = <%= service %>s; 99 | // ToDo: Test transactions 100 | -------------------------------------------------------------------------------- /test/services/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.SECURE_MODE = true; 4 | 5 | var chai = require('chai'); 6 | chai.should(); 7 | var config = require('../../config'); 8 | var chaiAsPromised = require('chai-as-promised'); 9 | chai.use(chaiAsPromised); 10 | var crypto = require('crypto'); 11 | var express = require('express'); 12 | var app = express(); 13 | var request = require('supertest'); 14 | var response = require('../../services/response'); 15 | var bodyParser = require('body-parser'); 16 | 17 | // init 18 | 19 | var res = {}; 20 | var req = {}; 21 | var demoData = '{"el escribimos": "silencio es dorado"}'; 22 | var demoDataHash = crypto.createHash('sha512') 23 | .update(demoData) 24 | .digest('hex'); 25 | 26 | console.log('hash', demoDataHash); 27 | var nextChecker = false; 28 | var next = function(){ 29 | if(arguments.length > 0){ 30 | console.log(arguments[0]); 31 | }else{ 32 | nextChecker = true; 33 | } 34 | 35 | return nextChecker; 36 | }; 37 | res.json = function(data){ 38 | return res; 39 | }; 40 | 41 | res.status = function(status){ 42 | return res; 43 | }; 44 | 45 | var header = {}; 46 | res.set = function(key, value){ 47 | header[key] = value; 48 | return header[key]; 49 | }; 50 | req.get = function(key){ 51 | return header[key]; 52 | }; 53 | 54 | header.set = function(data){ 55 | header.temp = data; 56 | return header.temp; 57 | }; 58 | 59 | req.method = ''; 60 | 61 | // Testing validation service 62 | 63 | var validator = require('../../services/validator'); 64 | 65 | // Dummy App 66 | app.use(bodyParser.urlencoded({ extended: false })); 67 | app.use(bodyParser.json()); 68 | app.use(response); 69 | 70 | app.post('/', function(req,res,next){ 71 | req._required = [ 72 | 'name', 73 | 'name2' 74 | ]; 75 | 76 | next(); 77 | }, 78 | validator, 79 | function(req,res){ 80 | res.ok('It worked!'); 81 | } 82 | ); 83 | 84 | var agent = request(app); 85 | 86 | describe('#Validation service test', function(){ 87 | it('should exist as a function',function(done){ 88 | validator.should.exist; /* jslint ignore:line */ 89 | done(); 90 | }); 91 | 92 | it('should fail due to non existence of a required parameter',function(done){ 93 | agent 94 | .post('/') 95 | .send({name:'femi'}) 96 | .expect(400,done); 97 | }); 98 | 99 | it('should be successful', function(done){ 100 | agent 101 | .post('/') 102 | .send({name:'femi2',name2:'femi'}) 103 | .expect(200,done); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /config/development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | env: process.env.NODE_ENV || 'development', 4 | port: process.env.PORT || 8080, 5 | trustProxy: process.env.TRUST_PROXY || 'no', 6 | bugsnagKey: process.env.BUGSNAG_KEY || false, 7 | secureMode: process.env.SECURE_MODE || false, 8 | secret: process.env.SECRET || 'lakikihdgdfdjjjdgd67264664vdjhjdyncmxuei8336%%^#%gdvdhj????jjhdghduue', 9 | mongoURL: process.env.MONGOLAB_URL || 'mongodb://127.0.0.1/snipe', 10 | logMongoURL: process.env.LOG_MONGOLAB_URL || 'mongodb://127.0.0.1/snipelogs', 11 | noFrontendCaching: process.env.NO_CACHE || 'yes', 12 | frontendCacheExpiry: process.env.FRONTEND_CACHE_EXPIRY || '90', 13 | backendCacheExpiry: process.env.BACKEND_CACHE_EXPIRY || '90', 14 | rateLimit: process.env.RATE_LIMIT || '1800', 15 | rateLimitExpiry: process.env.RATE_LIMIT_EXPIRY || '3600000', 16 | redisURL: process.env.REDIS_URL || 'redis://127.0.0.1:6379/1', 17 | letsencryptSSLVerificationURL: process.env.LETSENCRYPT_VERIFICATION_URL || '/.well-known/acme-challenge/xvArhQBSilF4V30dGUagNAZ96ASipB0b0ex0kXn0za8', 18 | letsencryptSSLVerificationBody: process.env.LETSENCRYPT_VERIFICATION_BODY || 'xvArhQBSilF4V30dGUagNAZ96ASipB0b0ex0kXn0za8._v6aFbaRYWeOmSebtlD-X4Ixf5tPsyULMsXM8HjsK-Q', 19 | maxContentLength: process.env.MAX_CONTENT_LENGTH || '9999', 20 | enforceSSL: process.env.ENFORCE_SSL || 'no', 21 | gitOAuthToken: process.env.GIT_OAUTH_TOKEN || '86d6eb7abe03e8ae6a970cb67622e667c9c0f86a', 22 | queueUIUsername: process.env.QUEUE_UI_USERNAME || 'admin', 23 | queueUIPassword: process.env.QUEUE_UI_PASSWORD || 'password123/', 24 | queueUIPort: process.env.QUEUE_UI_PORT || 3000, 25 | enforceUserIdAppIdDeveloperId: process.env.ENFORCE_USER_ID_APP_ID_DEVELOPER_ID || 'no', 26 | apiDBKey: process.env.API_DB_Key || 'MDg4NWM1NTA0ZTZlNTQ5MjAzNzA1ODBlOWVkNzI3MzdlNmYxZTcyMjVkOTA3N2JjYTBhZjA0YmM0N2U4NDZkNi8vLy8vLzQ1MDY=', 27 | SQLUsername: process.env.SQL_USERNAME || 'root', 28 | SQLPassword: process.env.SQL_PASSWORD || null, 29 | SQLDatabase: process.env.SQL_DATABASE || 'snipe', 30 | SQLHost: process.env.SQL_HOST || '127.0.0.1', 31 | SQLPort: process.env.SQL_PORT || 3306, 32 | SQLDriver: process.env.SQL_DRIVER || 'mysql', //'mysql'|'sqlite'|'postgres'|'mssql' 33 | SQLTimezone: process.env.SQL_TIMEZONE || '+01:00', 34 | clockTimezone: process.env.CLOCK_TIMEZONE || 'Africa/Lagos', 35 | workerConcurrency: process.env.WORKER_CONCURRENCY || '1', 36 | logglyToken: process.env.LOGGLY_TOKEN || false, 37 | logglySubdomain: process.env.LOGGLY_SUBDOMAIN || false, 38 | logglyTag: process.env.LOGGLY_TAG || false, 39 | cleanUpFailedJobs: process.env.CLEANUP_FAILED_JOBS || 'yes' 40 | }; 41 | -------------------------------------------------------------------------------- /services/queue/clock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var CronJob = require('cron').CronJob; 4 | var log = require('../logger'); 5 | var Model = require('./Model'); 6 | var queue = require('./'); 7 | var _ = require('lodash'); 8 | var config = require('../../config'); 9 | 10 | log.info('Starting Queue Clock...'); 11 | Model.find({enabled: true}) 12 | .then(function(jobs){ 13 | _.forEach(jobs, function(job){ 14 | log.info('Initializing '+job.name+'...'); 15 | var cron = new CronJob({ 16 | cronTime: job.crontab, 17 | onTick: function(){ 18 | // run this 19 | // Check if job is enabled 20 | Model.findOne({_id: job._id, enabled: true}) 21 | .then(function(resp){ 22 | if(!resp){ 23 | throw {notEnabled: true}; 24 | }else{ 25 | log.info('Pushing '+job.name+' to queue...'); 26 | queue.create(job.job, job.arguments) 27 | .save(); 28 | resp.lastRunAt = new Date(); 29 | resp.save(); 30 | } 31 | }) 32 | .catch(function(err){ 33 | if(err.notEnabled){ 34 | log.info(job.name+' is not enabled. Skipping...'); 35 | }else{ 36 | log.error('An error occured while running Job - '+job.name, err); 37 | } 38 | }); 39 | 40 | }, 41 | start: true, 42 | timeZone: config.clockTimezone 43 | }); 44 | }); 45 | }) 46 | .catch(function(err){ 47 | log.error('An error occured while starting the queue clock: ', err); 48 | }); 49 | 50 | if(config.cleanUpFailedJobs === 'yes'){ 51 | var jobCleanUpCron = new CronJob({ 52 | cronTime: '*/5 * * * *', 53 | onTick: function(){ 54 | queue.kue.Job.rangeByState( 'failed', 0, 100, 'asc', function( err, jobs ) { 55 | jobs.forEach( function( job ) { 56 | job.remove( function(err){ 57 | if (err) { 58 | throw err; 59 | }else{ 60 | log.info('removed failed job #%d', job.id); 61 | } 62 | }); 63 | }); 64 | }); 65 | }, 66 | start: true, 67 | timeZone: config.clockTimezone 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /config/production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | env: process.env.NODE_ENV || 'production', 4 | port: process.env.PORT || 80, 5 | trustProxy: process.env.TRUST_PROXY || 'yes', 6 | bugsnagKey: process.env.BUGSNAG_KEY || false, 7 | secureMode: process.env.SECURE_MODE || true, 8 | secret: process.env.SECRET || 'lakikihdgdfdjjjdgd67264660okjnbgtrdxswerfgytg373745ei8336%%^#%gdvdhj????jjhdghduue', 9 | mongoURL: process.env.MONGOLAB_URL || 'mongodb://192.168.99.100/snipe', 10 | logMongoURL: process.env.LOG_MONGOLAB_URL || 'mongodb://192.168.99.100/snipelogs', 11 | noFrontendCaching: process.env.NO_CACHE || 'no', 12 | frontendCacheExpiry: process.env.FRONTEND_CACHE_EXPIRY || '90', 13 | backendCacheExpiry: process.env.BACKEND_CACHE_EXPIRY || '90', 14 | rateLimit: process.env.RATE_LIMIT || '1800', 15 | rateLimitExpiry: process.env.RATE_LIMIT_EXPIRY || '3600000', 16 | redisURL: process.env.REDIS_URL || 'redis://192.168.99.100:6379/1', 17 | letsencryptSSLVerificationURL: process.env.LETSENCRYPT_VERIFICATION_URL || '/.well-known/acme-challenge/xvArhQBSilF4V30dGUagNAZ96ASipB0b0ex0kXn0za8', 18 | letsencryptSSLVerificationBody: process.env.LETSENCRYPT_VERIFICATION_BODY || 'xvArhQBSilF4V30dGUagNAZ96ASipB0b0ex0kXn0za8._v6aFbaRYWeOmSebtlD-X4Ixf5tPsyULMsXM8HjsK-Q', 19 | maxContentLength: process.env.MAX_CONTENT_LENGTH || '9999', 20 | enforceSSL: process.env.ENFORCE_SSL || 'no', 21 | gitOAuthToken: process.env.GIT_OAUTH_TOKEN || '86d6eb7abe03e8ae6a970cb67622e667c9c0f86a', 22 | queueUIUsername: process.env.QUEUE_UI_USERNAME || 'admin', 23 | queueUIPassword: process.env.QUEUE_UI_PASSWORD || 'password123/', 24 | queueUIPort: process.env.QUEUE_UI_PORT || 3000, 25 | enforceUserIdAppIdDeveloperId: process.env.ENFORCE_USER_ID_APP_ID_DEVELOPER_ID || 'no', 26 | apiDBKey: process.env.API_DB_Key || 'MDg4NWM1NTA0ZTZlNTQ5MjAzNzA1ODBlOWVkNzI3MzdlNmYxZTcyMjVkOTA3N2JjYTBhZjA0YmM0N2U4NDZkNi8vLy8vLzQ1MDY=', 27 | SQLUsername: process.env.SQL_USERNAME || 'root', 28 | SQLPassword: process.env.SQL_PASSWORD || null, 29 | SQLDatabase: process.env.SQL_DATABASE || 'snipe', 30 | SQLHost: process.env.SQL_HOST || '192.168.99.100', 31 | SQLPort: process.env.SQL_PORT || 3306, 32 | SQLDriver: process.env.SQL_DRIVER || 'mysql', //'mysql'|'sqlite'|'postgres'|'mssql' 33 | SQLTimezone: process.env.SQL_TIMEZONE || '+01:00', 34 | clockTimezone: process.env.CLOCK_TIMEZONE || 'Africa/Lagos', 35 | workerConcurrency: process.env.WORKER_CONCURRENCY || '1', 36 | logglyToken: process.env.LOGGLY_TOKEN || false, 37 | logglySubdomain: process.env.LOGGLY_SUBDOMAIN || false, 38 | logglyTag: process.env.LOGGLY_TAG || false, 39 | cleanUpFailedJobs: process.env.CLEANUP_FAILED_JOBS || 'yes' 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-template", 3 | "version": "0.7.0", 4 | "description": "A template for creating APIs Fast", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "gulp sanity", 8 | "start": "node app", 9 | "clock": "node ./services/queue/clock", 10 | "workers": "node ./services/queue/workers" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/EnsembleLab/api-template.git" 15 | }, 16 | "keywords": [ 17 | "REST", 18 | "API" 19 | ], 20 | "author": "Olufemi Olanipekun ", 21 | "license": "GPL-3.0", 22 | "bugs": { 23 | "url": "https://github.com/EnsembleLab/api-template/issues" 24 | }, 25 | "homepage": "https://github.com/EnsembleLab/api-template#readme", 26 | "devDependencies": { 27 | "chai": "^3.5.0", 28 | "chai-as-promised": "^6.0.0", 29 | "conventional-github-releaser": "^1.1.12", 30 | "fs": "0.0.1-security", 31 | "gulp": "^4.0.2", 32 | "gulp-bump": "^2.7.0", 33 | "gulp-conventional-changelog": "^1.1.3", 34 | "gulp-eslint": "^6.0.0", 35 | "gulp-git": "^2.10.1", 36 | "gulp-mocha": "^7.0.2", 37 | "gulp-nodemon": "^2.5.0", 38 | "gulp-todo": "^5.4.0", 39 | "gulp-util": "^3.0.8", 40 | "jshint": "^2.11.0", 41 | "jshint-stylish": "^2.2.1", 42 | "minimist": "^1.2.6", 43 | "mocha": "^7.1.2", 44 | "mongoose-mock": "^0.4.0", 45 | "run-sequence": "^2.2.1", 46 | "sinon": "^2.3.4", 47 | "sinon-chai": "^2.11.0", 48 | "supertest": "^3.0.0" 49 | }, 50 | "dependencies": { 51 | "aes-js": "^3.0.0", 52 | "basic-auth-connect": "^1.0.0", 53 | "body-parser": "^1.17.1", 54 | "bugsnag": "^1.9.1", 55 | "cacheman": "^2.2.1", 56 | "cacheman-redis": "^1.1.2", 57 | "cluster": "^0.7.7", 58 | "cors": "^2.8.3", 59 | "cron": "^1.3.0", 60 | "crypto": "0.0.3", 61 | "debug": "^2.6.3", 62 | "express": "^4.15.2", 63 | "express-content-length-validator": "^1.0.0", 64 | "express-enforces-ssl": "^1.1.0", 65 | "express-limiter": "^1.6.0", 66 | "express-validator": "^6.4.0", 67 | "fnv-plus": "^1.2.12", 68 | "helmet": "^3.22.0", 69 | "hpp": "^0.2.2", 70 | "kue": "^0.11.6", 71 | "lodash": "^4.17.21", 72 | "mongoose": "^6.5.0", 73 | "mysql2": "^2.3.3", 74 | "nocache": "^2.1.0", 75 | "pg": "^6.4.2", 76 | "pg-hstore": "^2.3.2", 77 | "q": "^1.4.1", 78 | "randomstring": "^1.1.5", 79 | "redis": "^3.1.1", 80 | "request": "^2.83.0", 81 | "request-promise": "^4.2.1", 82 | "sequelize": "^6.0.0", 83 | "shortid": "^2.2.8", 84 | "sqlite3": "^4.0.1", 85 | "tedious": "^2.1.5", 86 | "util": "^0.10.3", 87 | "winston": "^2.3.1", 88 | "winston-bugsnag": "^2.1.0", 89 | "winston-loggly-bulk": "^2.0.2" 90 | }, 91 | "mocha": { 92 | "recursive": true, 93 | "timeout": 20000 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | # MySQL 5 | mysqldb: 6 | image: mysql:8.0 7 | restart: always 8 | environment: 9 | - MYSQL_ROOT_PASSWORD=command 10 | volumes: 11 | - mysql:/var/lib/mysql 12 | ports: 13 | - "3307:3306" 14 | restart: always 15 | 16 | # The Application 17 | api: 18 | build: 19 | context: ./ 20 | dockerfile: ./Dockerfile 21 | ports: 22 | - 8080:8080 23 | links: 24 | - redis 25 | - mongo 26 | - mysqldb 27 | environment: 28 | - MONGOLAB_URL=mongodb://mongo/giro 29 | - LOG_MONGOLAB_URL=mongodb://mongo/giroLogs 30 | - REDIS_URL=redis://redis:6379/1 31 | - SQL_USERNAME=root 32 | - SQL_PASSWORD=command 33 | - SQL_DATABASE=giro 34 | - SQL_HOST=mysqldb 35 | restart: always 36 | 37 | # The Web Server 38 | clock: 39 | build: 40 | context: ./ 41 | dockerfile: ./clock/Dockerfile 42 | links: 43 | - redis 44 | - mongo 45 | - mysqldb 46 | environment: 47 | - MONGOLAB_URL=mongodb://mongo/giro 48 | - LOG_MONGOLAB_URL=mongodb://mongo/giroLogs 49 | - REDIS_URL=redis://redis:6379/1 50 | - SQL_USERNAME=root 51 | - SQL_PASSWORD=command 52 | - SQL_DATABASE=giro 53 | - SQL_HOST=mysqldb 54 | restart: always 55 | 56 | # The Worker 57 | worker: 58 | build: 59 | context: ./ 60 | dockerfile: ./workers/Dockerfile 61 | links: 62 | - redis 63 | - mongo 64 | - mysqldb 65 | environment: 66 | - MONGOLAB_URL=mongodb://mongo/giro 67 | - LOG_MONGOLAB_URL=mongodb://mongo/giroLogs 68 | - REDIS_URL=redis://redis:6379/1 69 | - SQL_USERNAME=root 70 | - SQL_PASSWORD=command 71 | - SQL_DATABASE=giro 72 | - SQL_HOST=mysqldb 73 | restart: always 74 | 75 | # Php MyAdmin 76 | mysqladmin: 77 | image: phpmyadmin/phpmyadmin 78 | environment: 79 | - PMA_HOST=mysqldb 80 | restart: always 81 | links: 82 | - "mysqldb" 83 | ports: 84 | - "8000:80" 85 | 86 | # MongoDB Admin 87 | mongoadmin: 88 | image: mongo-express 89 | links: 90 | - mongo 91 | environment: 92 | - ME_CONFIG_MONGODB_SERVER=mongo 93 | restart: always 94 | links: 95 | - "mysqldb" 96 | ports: 97 | - "80:8081" 98 | 99 | # Redis 100 | redis: 101 | image: redis 102 | restart: always 103 | 104 | # Mongo 105 | mongo: 106 | image: mongo:6.0 107 | restart: always 108 | 109 | volumes: 110 | mysql: 111 | driver: local 112 | -------------------------------------------------------------------------------- /services/response/ok.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var log = require('../logger'); 3 | var config = require('../../config'); 4 | var encryption = require('../encryption'); 5 | var debug = require('debug')('response'); 6 | var _ = require('lodash'); 7 | var queue = require('../queue'); 8 | 9 | module.exports = function(data, cache, extraData){ 10 | debug('sending ok response'); 11 | var req = this.req; 12 | var res = this; 13 | 14 | // Dump it in the queue 15 | var response = {}; 16 | if(cache){ 17 | response.response = data; 18 | response.response.cached = cache; 19 | }else{ 20 | response.response = {status: 'success', data: data}; 21 | } 22 | 23 | if(extraData){ 24 | response.response = _.extend(response.response, extraData); 25 | } 26 | 27 | response.requestId = req.requestId; 28 | // Encrypt response here 29 | if(req.get('x-tag') && req.method === 'POST' && config.secureMode && req.body.secure === true && data){ 30 | debug('i want to encrypt'); 31 | var key = req.get('x-tag'); 32 | debug('our encryption key: ', key); 33 | var text = JSON.stringify(data); 34 | debug('about to call encryption method'); 35 | encryption.encrypt(text, key) 36 | .then(function(resp){ 37 | debug('got response from encryption method: ',resp); 38 | log.info('Sending ok response: ', response.response); 39 | response.response.secure = true; 40 | response.response.data = resp.encryptedText; 41 | response.response.truth = resp.truth; 42 | res.status(200).json(response.response); 43 | }) 44 | .catch(function(err){ 45 | debug('got error from encryption method: ', err); 46 | res.serverError(err,'Error encrypting response.'); 47 | }); 48 | }else{ 49 | log.info('Sending ok response: ', response.response); 50 | if(data){ 51 | // Only cache GET calls 52 | if(req.method === 'GET' && config.noFrontendCaching !== 'yes'){ 53 | 54 | // If this is a cached response, show response else cache the response and show response. 55 | if(cache){ 56 | res.status(200).json(response.response); 57 | }else{ 58 | // req.cacheKey 59 | req.cache.set(req.cacheKey,response.response) 60 | .then(function(resp){ 61 | res.status(200).json(response.response); 62 | }) 63 | .catch(function(err){ 64 | log.error('Failed to cache data: ', err); 65 | // This error shouldn't stop our response 66 | res.status(200).json(response.response); 67 | }); 68 | } 69 | }else{ 70 | res.status(200).json(response.response); 71 | } 72 | }else{ 73 | res.status(200).json(response.response); 74 | } 75 | } 76 | 77 | queue.create('logResponse', response) 78 | .save(); 79 | 80 | }; 81 | -------------------------------------------------------------------------------- /services/request/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var request = require('request-promise'); 3 | var Model = require('./Model'); 4 | var q = require('q'); 5 | var log = require('../logger'); 6 | 7 | module.exports = function(service, requestId, uri, method, data, headers){ 8 | // have you made this request before? 9 | // if yes, return the response from the previous request 10 | // else make request 11 | // log the response, the response code and updated at 12 | // end 13 | 14 | var options = { 15 | method: method, 16 | uri: uri, 17 | data: data, 18 | headers: headers, 19 | insecure: true 20 | }; 21 | 22 | var existss; 23 | return q.Promise(function(resolve, reject){ 24 | Model.findOne({RequestId: requestId, service: service}) 25 | .then(function(resp){ 26 | existss = resp; 27 | if(!resp){ 28 | options.RequestId = requestId; 29 | options.service = service; 30 | return Model.create(options); 31 | }else if(resp.response && resp.responseStatusCode === 200){ 32 | throw {RequestId: resp.RequestId, response: resp.response}; 33 | }else{ 34 | options.retriedAt = Date.now(); 35 | return Model.findByIdAndUpdate(resp._id, options); 36 | } 37 | }) 38 | .then(function(resp){ 39 | existss = true; 40 | options.json = true; 41 | if(options.method === 'GET'){ 42 | options.qs = options.data; 43 | }else if(options.method === 'POST'){ 44 | options.body = options.data; 45 | }else{ 46 | options.qs = options.data; 47 | options.body = options.data; 48 | } 49 | 50 | options.data = null; 51 | options.RequestId = null; 52 | options.service = null; 53 | options.retriedAt = null; 54 | return request(options); 55 | }) 56 | .then(function(resp){ 57 | return [Model.update({RequestId: requestId, service: service}, {response: resp, responseStatusCode: 200, updatedAt: Date.now()}), resp]; 58 | }) 59 | .spread(function(update, resp){ 60 | return resolve(resp); 61 | }) 62 | .catch(function(err){ 63 | if(err.RequestId){ 64 | return resolve(err.response); 65 | }else{ 66 | var updateddd; 67 | if(existss){ 68 | updateddd = Model.update({RequestId: requestId, service: service}, {response: (err.response && err.response.body) ? err.response.body : { type: 'internal error', message: err.message}, responseStatusCode: err.statusCode ? err.statusCode : 500, updatedAt: Date.now()}); 69 | }else{ 70 | options.RequestId = requestId; 71 | options.service = service; 72 | options.response = (err.response && err.response.body) ? err.response.body : {type: 'internal error', message: err.message}; 73 | options.responseStatusCode = err.statusCode ? err.statusCode : 500; 74 | options.updatedAt = Date.now(); 75 | updateddd = Model.create(options); 76 | } 77 | 78 | updateddd 79 | .then(function(resp){ 80 | return reject((err.response && err.response.body) ? err.response.body : {type: 'internal error', message: err.message}); 81 | }) 82 | .catch(function(err2){ 83 | log.error('Error while trying to update API request: ',err2); 84 | return reject((err.response && err.response.body) ? err.response.body : {type: 'internal error', message: err.message}); 85 | }); 86 | } 87 | }); 88 | }); 89 | }; 90 | // ToDo: Test request module -------------------------------------------------------------------------------- /test/services/encryption.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.SECURE_MODE = true; 4 | 5 | var chai = require('chai'); 6 | chai.should(); 7 | var config = require('../../config'); 8 | var chaiAsPromised = require('chai-as-promised'); 9 | chai.use(chaiAsPromised); 10 | var crypto = require('crypto'); 11 | 12 | // init 13 | 14 | var res = {}; 15 | var req = {}; 16 | var demoData = '{"el escribimos": "silencio es dorado"}'; 17 | var demoDataHash = crypto.createHash('sha512') 18 | .update(demoData) 19 | .digest('hex'); 20 | 21 | console.log('hash', demoDataHash); 22 | var nextChecker = false; 23 | var next = function(){ 24 | if(arguments.length > 0){ 25 | console.log(arguments[0]); 26 | }else{ 27 | nextChecker = true; 28 | } 29 | 30 | return nextChecker; 31 | }; 32 | res.json = function(data){ 33 | return res; 34 | }; 35 | 36 | res.status = function(status){ 37 | return res; 38 | }; 39 | 40 | var header = {}; 41 | res.set = function(key, value){ 42 | header[key] = value; 43 | return header[key]; 44 | }; 45 | req.get = function(key){ 46 | return header[key]; 47 | }; 48 | 49 | header.set = function(data){ 50 | header.temp = data; 51 | return header.temp; 52 | }; 53 | 54 | req.method = ''; 55 | 56 | // Testing encryption service 57 | 58 | var encryption = require('../../services/encryption'); 59 | 60 | describe('#Encryption service test', function(){ 61 | it('should have property generateKey, encrypt, decrypt and interpreter', function(done){ 62 | 63 | encryption.should.have.property('generateKey'); 64 | encryption.should.have.property('encrypt'); 65 | encryption.should.have.property('decrypt'); 66 | encryption.should.have.property('interpreter'); 67 | done(); 68 | }); 69 | 70 | 71 | it('should generate key', function(done){ 72 | encryption.generateKey().should.eventually.be.a('string').notify(done); 73 | }); 74 | 75 | it('should encrypt and decrypt data', function(done){ 76 | encryption.generateKey() 77 | .then(function(resp){ 78 | header['x-tag'] = resp; 79 | 80 | return encryption.encrypt(demoData, req.get('x-tag')); 81 | }) 82 | .then(function(resp){ 83 | console.log('encrypted data: ', resp); 84 | res.set('encryptedData', resp.encryptedText); 85 | console.log('hash: ', demoDataHash, 'generated hash: ', resp.truth); 86 | return encryption.decrypt(resp.encryptedText, req.get('x-tag'), resp.truth); 87 | }) 88 | .then(function(resp){ 89 | console.log('decrypted data: ', resp); 90 | resp.should.be.a('string'); 91 | done(); 92 | }) 93 | .catch(function(err){ 94 | done(err); 95 | }); 96 | }); 97 | 98 | it('should detect compromised data', function(done){ 99 | encryption.decrypt('5b9f535be7edbad69ac03aced46f6586c1b2d', req.get('x-tag'), demoDataHash) 100 | .then(function(resp){ 101 | done(resp); 102 | }) 103 | .catch(function(err){ 104 | if(err.message === 'Data integrity compromised!'){ 105 | done(); 106 | }else{ 107 | done(err); 108 | } 109 | }); 110 | }); 111 | 112 | it('should interpret data when data is not POST', function(done){ 113 | encryption.interpreter(req,res,next); 114 | nextChecker.should.be.true; /* jslint ignore:line */ 115 | nextChecker = false; 116 | done(); 117 | }); 118 | 119 | it('should interpret data when data is POST', function(done){ 120 | req.method = 'POST'; 121 | req.body = {}; 122 | req.body.secureData = req.get('encryptedData'); 123 | req.body.truth = demoDataHash; 124 | encryption.interpreter(req,res,next); 125 | setTimeout(function(){ 126 | nextChecker.should.be.true; /* jslint ignore:line */ 127 | nextChecker = false; 128 | },1000); 129 | done(); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /models/Trash.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var db = require('../services/database').logMongo; 4 | 5 | var queue = require('../services/queue'); 6 | 7 | var collection = 'Trash'; 8 | 9 | var service = 'Users'; 10 | 11 | var debug = require('debug')(collection); 12 | 13 | var schemaObject = { 14 | data: { 15 | type: db._mongoose.Schema.Types.Mixed 16 | }, 17 | service: { 18 | type: 'String', 19 | default: service 20 | } 21 | }; 22 | 23 | schemaObject.createdAt = { 24 | type: 'Date', 25 | default: Date.now, 26 | index: true 27 | }; 28 | 29 | schemaObject.updatedAt = { 30 | type: 'Date' 31 | // default: Date.now 32 | }; 33 | 34 | schemaObject.owner = { 35 | type: db._mongoose.Schema.Types.ObjectId, 36 | ref: 'Accounts' 37 | }; 38 | 39 | schemaObject.deletedBy = { 40 | type: db._mongoose.Schema.Types.ObjectId, 41 | ref: 'Accounts' 42 | }; 43 | 44 | schemaObject.client = { 45 | type: db._mongoose.Schema.Types.ObjectId, 46 | ref: 'Clients' 47 | }; 48 | 49 | schemaObject.developer = { 50 | type: db._mongoose.Schema.Types.ObjectId, 51 | ref: 'Users' 52 | }; 53 | 54 | schemaObject.tags = { 55 | type: [String], 56 | index: 'text' 57 | }; 58 | 59 | // Let us define our schema 60 | var Schema = new db._mongoose.Schema(schemaObject); 61 | 62 | // Index all text for full text search 63 | // MyModel.find({$text: {$search: searchString}}) 64 | // .skip(20) 65 | // .limit(10) 66 | // .exec(function(err, docs) { ... }); 67 | // Schema.index({'tags': 'text'}); 68 | 69 | Schema.statics.search = function(string) { 70 | return this.find({$text: {$search: string}}, { score : { $meta: 'textScore' } }) 71 | .sort({ score : { $meta : 'textScore' } }); 72 | }; 73 | 74 | // assign a function to the "methods" object of our Schema 75 | // Schema.methods.someMethod = function (cb) { 76 | // return this.model(collection).find({}, cb); 77 | // }; 78 | 79 | // assign a function to the "statics" object of our Schema 80 | // Schema.statics.someStaticFunction = function(query, cb) { 81 | // eg. pagination 82 | // this.find(query, null, { skip: 10, limit: 5 }, cb); 83 | // }; 84 | 85 | // Adding hooks 86 | 87 | Schema.pre('save', function(next) { 88 | // Indexing for search 89 | var ourDoc = this._doc; 90 | 91 | ourDoc.model = collection; 92 | 93 | // Dump it in the queue 94 | queue.create('searchIndex', ourDoc) 95 | .save(); 96 | 97 | next(); 98 | }); 99 | 100 | Schema.post('init', function(doc) { 101 | debug('%s has been initialized from the db', doc._id); 102 | }); 103 | 104 | Schema.post('validate', function(doc) { 105 | debug('%s has been validated (but not saved yet)', doc._id); 106 | }); 107 | 108 | Schema.post('save', function(doc) { 109 | debug('%s has been saved', doc._id); 110 | }); 111 | 112 | Schema.post('remove', function(doc) { 113 | debug('%s has been removed', doc._id); 114 | }); 115 | 116 | Schema.pre('validate', function(next) { 117 | debug('this gets printed first'); 118 | next(); 119 | }); 120 | 121 | Schema.post('validate', function() { 122 | debug('this gets printed second'); 123 | }); 124 | 125 | Schema.pre('find', function(next) { 126 | debug(this instanceof db._mongoose.Query); // true 127 | this.start = Date.now(); 128 | next(); 129 | }); 130 | 131 | Schema.post('find', function(result) { 132 | debug(this instanceof db._mongoose.Query); // true 133 | // prints returned documents 134 | debug('find() returned ' + JSON.stringify(result)); 135 | // prints number of milliseconds the query took 136 | debug('find() took ' + (Date.now() - this.start) + ' millis'); 137 | }); 138 | 139 | Schema.pre('update', function(next) { 140 | // Indexing for search 141 | var ourDoc = this._update; 142 | ourDoc.model = collection; 143 | ourDoc.update = true; 144 | 145 | if(ourDoc.updatedAt || ourDoc.tags){ /* jslint ignore:line */ 146 | // Move along! Nothing to see here!! 147 | }else{ 148 | // Dump it in the queue 149 | queue.create('searchIndex', ourDoc) 150 | .save(); 151 | } 152 | ourDoc.updatedAt = new Date(Date.now()).toISOString(); 153 | next(); 154 | }); 155 | 156 | var Model = db.model(collection, Schema); 157 | Model._mongoose = db._mongoose; 158 | 159 | module.exports = Model; 160 | -------------------------------------------------------------------------------- /template/model.tmpl: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var db = require('../services/database').mongo; 4 | 5 | var queue = require('../services/queue'); 6 | 7 | var collection = '<%= service %>s'; 8 | 9 | var debug = require('debug')(collection); 10 | 11 | var schemaObject = { 12 | // ++++++++++++++ Modify to your own schema ++++++++++++++++++ 13 | name: { 14 | type: 'String' 15 | }, 16 | someOtherStringData: { 17 | type: 'String' 18 | }, 19 | toPop: { 20 | type: db._mongoose.Schema.Types.ObjectId, 21 | ref: '<%= service %>s' 22 | } 23 | 24 | // ++++++++++++++ Modify to your own schema ++++++++++++++++++ 25 | }; 26 | 27 | schemaObject.createdAt = { 28 | type: 'Date', 29 | default: Date.now 30 | }; 31 | 32 | schemaObject.updatedAt = { 33 | type: 'Date' 34 | // default: new Date().toISOString() 35 | }; 36 | 37 | schemaObject.owner = { 38 | type: db._mongoose.Schema.Types.ObjectId, 39 | ref: 'Accounts' 40 | }; 41 | 42 | schemaObject.createdBy = { 43 | type: db._mongoose.Schema.Types.ObjectId, 44 | ref: 'Accounts' 45 | }; 46 | 47 | schemaObject.client = { 48 | type: db._mongoose.Schema.Types.ObjectId, 49 | ref: 'Clients' 50 | }; 51 | 52 | schemaObject.developer = { 53 | type: db._mongoose.Schema.Types.ObjectId, 54 | ref: 'Users' 55 | }; 56 | 57 | schemaObject.tags = { 58 | type: [String], 59 | index: 'text' 60 | }; 61 | 62 | // Let us define our schema 63 | var Schema = db._mongoose.Schema(schemaObject); 64 | 65 | // Index all text for full text search 66 | // MyModel.find({$text: {$search: searchString}}) 67 | // .skip(20) 68 | // .limit(10) 69 | // .exec(function(err, docs) { ... }); 70 | // Schema.index({'tags': 'text'}); 71 | 72 | Schema.statics.search = function(string) { 73 | return this.find({$text: {$search: string}}, { score : { $meta: "textScore" } }) 74 | .sort({ score : { $meta : 'textScore' } }); 75 | }; 76 | 77 | // assign a function to the "methods" object of our Schema 78 | // Schema.methods.someMethod = function (cb) { 79 | // return this.model(collection).find({}, cb); 80 | // }; 81 | 82 | // assign a function to the "statics" object of our Schema 83 | // Schema.statics.someStaticFunction = function(query, cb) { 84 | // eg. pagination 85 | // this.find(query, null, { skip: 10, limit: 5 }, cb); 86 | // }; 87 | 88 | // Adding hooks 89 | 90 | Schema.pre('save', function(next) { 91 | // Indexing for search 92 | var ourDoc = this._doc; 93 | 94 | ourDoc.model = collection; 95 | 96 | // Dump it in the queue 97 | queue.create('searchIndex', ourDoc) 98 | .save(); 99 | 100 | next(); 101 | }); 102 | 103 | Schema.post('init', function(doc) { 104 | debug('%s has been initialized from the db', doc._id); 105 | }); 106 | 107 | Schema.post('validate', function(doc) { 108 | debug('%s has been validated (but not saved yet)', doc._id); 109 | }); 110 | 111 | Schema.post('save', function(doc) { 112 | debug('%s has been saved', doc._id); 113 | }); 114 | 115 | Schema.post('remove', function(doc) { 116 | debug('%s has been removed', doc._id); 117 | }); 118 | 119 | Schema.pre('validate', function(next) { 120 | debug('this gets printed first'); 121 | next(); 122 | }); 123 | 124 | Schema.post('validate', function() { 125 | debug('this gets printed second'); 126 | }); 127 | 128 | Schema.pre('find', function(next) { 129 | debug(this instanceof db._mongoose.Query); // true 130 | this.start = Date.now(); 131 | next(); 132 | }); 133 | 134 | Schema.post('find', function(result) { 135 | debug(this instanceof db._mongoose.Query); // true 136 | // prints returned documents 137 | debug('find() returned ' + JSON.stringify(result)); 138 | // prints number of milliseconds the query took 139 | debug('find() took ' + (Date.now() - this.start) + ' millis'); 140 | }); 141 | 142 | Schema.pre('update', function(next) { 143 | // Indexing for search 144 | var ourDoc = this._update; 145 | ourDoc.model = collection; 146 | ourDoc.update = true; 147 | if(ourDoc.updatedAt || ourDoc.tags){ /* jslint ignore:line */ 148 | // Move along! Nothing to see here!! 149 | }else{ 150 | // Dump it in the queue 151 | queue.create('searchIndex', ourDoc) 152 | .save(); 153 | } 154 | 155 | ourDoc.updatedAt = new Date(Date.now()).toISOString(); 156 | 157 | next(); 158 | }); 159 | 160 | var Model = db.model(collection, Schema); 161 | Model._mongoose = db._mongoose; 162 | 163 | module.exports = Model; 164 | -------------------------------------------------------------------------------- /models/RequestLogs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var db = require('../services/database').logMongo; 4 | 5 | var collection = 'RequestLogs'; 6 | 7 | var service = 'Users'; 8 | 9 | var debug = require('debug')(collection); 10 | 11 | var queue = require('../services/queue'); 12 | 13 | var schemaObject = { 14 | RequestId: { 15 | type: 'String', 16 | unique: true 17 | }, 18 | ipAddress: { 19 | type: 'String' 20 | }, 21 | url: { 22 | type: 'String', 23 | index: true 24 | }, 25 | method: { 26 | type: 'String', 27 | index: true 28 | }, 29 | service: { 30 | type: 'String', 31 | default: service 32 | }, 33 | body: { 34 | type: db._mongoose.Schema.Types.Mixed 35 | }, 36 | app: { 37 | type: db._mongoose.Schema.Types.ObjectId, 38 | ref: 'Applications' 39 | }, 40 | user: { 41 | type: db._mongoose.Schema.Types.ObjectId, 42 | ref: 'Users', 43 | index: true 44 | }, 45 | device: { 46 | type: 'String' 47 | }, 48 | response: { 49 | type: db._mongoose.Schema.Types.Mixed 50 | }, 51 | }; 52 | 53 | schemaObject.createdAt = { 54 | type: 'Date', 55 | default: Date.now, 56 | index: true 57 | }; 58 | 59 | schemaObject.updatedAt = { 60 | type: 'Date' 61 | // default: Date.now 62 | }; 63 | 64 | schemaObject.owner = { 65 | type: db._mongoose.Schema.Types.ObjectId, 66 | ref: 'Accounts' 67 | }; 68 | 69 | schemaObject.createdBy = { 70 | type: db._mongoose.Schema.Types.ObjectId, 71 | ref: 'Accounts' 72 | }; 73 | 74 | schemaObject.client = { 75 | type: db._mongoose.Schema.Types.ObjectId, 76 | ref: 'Clients' 77 | }; 78 | 79 | schemaObject.developer = { 80 | type: db._mongoose.Schema.Types.ObjectId, 81 | ref: 'Users' 82 | }; 83 | 84 | schemaObject.tags = { 85 | type: [String], 86 | index: 'text' 87 | }; 88 | 89 | // Let us define our schema 90 | var Schema = new db._mongoose.Schema(schemaObject); 91 | 92 | // Index all text for full text search 93 | // MyModel.find({$text: {$search: searchString}}) 94 | // .skip(20) 95 | // .limit(10) 96 | // .exec(function(err, docs) { ... }); 97 | // Schema.index({'tags': 'text'}); 98 | 99 | Schema.statics.search = function(string) { 100 | return this.find({$text: {$search: string}}, { score : { $meta: 'textScore' } }) 101 | .sort({ score : { $meta : 'textScore' } }); 102 | }; 103 | 104 | // assign a function to the "methods" object of our Schema 105 | // Schema.methods.someMethod = function (cb) { 106 | // return this.model(collection).find({}, cb); 107 | // }; 108 | 109 | // assign a function to the "statics" object of our Schema 110 | // Schema.statics.someStaticFunction = function(query, cb) { 111 | // eg. pagination 112 | // this.find(query, null, { skip: 10, limit: 5 }, cb); 113 | // }; 114 | 115 | // Adding hooks 116 | 117 | Schema.pre('save', function(next) { 118 | // Indexing for search 119 | var ourDoc = this._doc; 120 | 121 | ourDoc.model = collection; 122 | 123 | // Dump it in the queue 124 | queue.create('searchIndex', ourDoc) 125 | .save(); 126 | 127 | next(); 128 | }); 129 | 130 | Schema.post('init', function(doc) { 131 | debug('%s has been initialized from the db', doc._id); 132 | }); 133 | 134 | Schema.post('validate', function(doc) { 135 | debug('%s has been validated (but not saved yet)', doc._id); 136 | }); 137 | 138 | Schema.post('save', function(doc) { 139 | debug('%s has been saved', doc._id); 140 | }); 141 | 142 | Schema.post('remove', function(doc) { 143 | debug('%s has been removed', doc._id); 144 | }); 145 | 146 | Schema.pre('validate', function(next) { 147 | debug('this gets printed first'); 148 | next(); 149 | }); 150 | 151 | Schema.post('validate', function() { 152 | debug('this gets printed second'); 153 | }); 154 | 155 | Schema.pre('find', function(next) { 156 | debug(this instanceof db._mongoose.Query); // true 157 | this.start = Date.now(); 158 | next(); 159 | }); 160 | 161 | Schema.post('find', function(result) { 162 | debug(this instanceof db._mongoose.Query); // true 163 | // prints returned documents 164 | debug('find() returned ' + JSON.stringify(result)); 165 | // prints number of milliseconds the query took 166 | debug('find() took ' + (Date.now() - this.start) + ' millis'); 167 | }); 168 | 169 | Schema.pre('update', function(next) { 170 | 171 | // Indexing for search 172 | var ourDoc = this._update; 173 | // debug('What we are updating: ', ourDoc); 174 | // ourDoc.model = collection; 175 | // ourDoc.update = true; 176 | // debug('what do we have here: ', ourDoc); 177 | // if(ourDoc.updatedAt || ourDoc.tags){ 178 | // debug('updatedAt: ', ourDoc.updatedAt); 179 | // debug('tags: ', ourDoc.tags); 180 | // // Move along! Nothing to see here!! 181 | // }else{ 182 | // // Dump it in the queue 183 | // queue.create('searchIndex', ourDoc) 184 | // .save(); 185 | // } 186 | ourDoc.updatedAt = new Date(Date.now()).toISOString(); 187 | next(); 188 | }); 189 | 190 | var Model = db.model(collection, Schema); 191 | Model._mongoose = db._mongoose; 192 | 193 | module.exports = Model; 194 | -------------------------------------------------------------------------------- /services/encryption/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var aesjs = require('aes-js'); 3 | var crypto = require('crypto'); 4 | var config = require('../../config'); 5 | var randomstring = require('randomstring'); 6 | var q = require('q'); 7 | var debug = require('debug')('encryption'); 8 | 9 | module.exports = { 10 | generateKey: function () { 11 | return q.Promise(function (resolve, reject) { 12 | var salt = randomstring.generate(256); 13 | debug('salt: ', salt); 14 | 15 | var bits = 256; 16 | 17 | crypto.pbkdf2(config.secret, salt, 100000, bits / 8, 'sha512', function (err, key) { 18 | if (err) { 19 | reject(err); 20 | } else { 21 | var randomNumber = Math.floor((Math.random() * 9999) + 1); 22 | resolve(Buffer.from(aesjs.utils.hex.fromBytes(key) + '//////' + randomNumber).toString('base64')); 23 | } 24 | }); 25 | }); 26 | }, 27 | 28 | encrypt: function (text, key) { 29 | debug('started encryption'); 30 | debug('using key: ', key); 31 | key = Buffer.from(key, 'base64').toString('utf-8'); 32 | debug('What i see here: ', key); 33 | var splitKey = key.split('//////'); 34 | key = splitKey[0]; 35 | var counter = ((splitKey[1] * 1) * 10) / 5; 36 | debug('our counter: ', counter); 37 | debug('our key: ', key); 38 | key = aesjs.utils.hex.toBytes(key); 39 | debug('in buffer: ', key); 40 | var truth = crypto.createHash('sha512') 41 | .update(text) 42 | .digest('hex'); 43 | return q.Promise(function (resolve) { 44 | debug('encrypting...'); 45 | debug('our key: ', key); 46 | 47 | // Convert text to bytes 48 | debug('our text: ', text); 49 | var textBytes = aesjs.utils.utf8.toBytes(text); 50 | debug('textBytes: ', textBytes); 51 | var aesCbc = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(counter)); 52 | var encryptedBytes = aesCbc.encrypt(textBytes); 53 | 54 | // Convert our bytes back into text 55 | var encryptedText = aesjs.utils.hex.fromBytes(encryptedBytes); 56 | debug('finished encryption'); 57 | resolve({ 58 | truth: truth, 59 | encryptedText: encryptedText 60 | }); 61 | }); 62 | }, 63 | 64 | decrypt: function (text, key, truthHash) { 65 | debug('text: ', text); 66 | key = Buffer.from(key, 'base64').toString('utf-8'); 67 | debug('What i see here: ', key); 68 | var splitKey = key.split('//////'); 69 | key = splitKey[0]; 70 | var counter = ((splitKey[1] * 1) * 10) / 5; 71 | debug('our counter: ', counter); 72 | debug('our key: ', key); 73 | key = aesjs.utils.hex.toBytes(key); 74 | 75 | return q.Promise(function (resolve, reject, notify) { 76 | debug('our key2: ', key); 77 | // Convert text to bytes 78 | var textBytes = aesjs.utils.hex.toBytes(text); 79 | 80 | // The cipher-block chaining mode of operation maintains internal 81 | // state, so to decrypt a new instance must be instantiated. 82 | var aesCbc = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(counter)); 83 | var decryptedBytes = aesCbc.decrypt(textBytes); 84 | 85 | // Convert our bytes back into text 86 | var decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); 87 | 88 | notify('checking if data was not hijacked...'); 89 | 90 | var truthConfirmationHash = crypto.createHash('sha512') 91 | .update(decryptedText) 92 | .digest('hex'); 93 | debug('sent hash: ', truthHash); 94 | debug('generated hash: ', truthConfirmationHash); 95 | 96 | if (truthHash === truthConfirmationHash) { 97 | resolve(decryptedText); 98 | } else { 99 | reject({statusCode: 400, message: 'Data integrity compromised!'}); 100 | } 101 | 102 | }); 103 | }, 104 | 105 | interpreter: function (req, res, next) { 106 | var encryption = require('./'); 107 | if (req.get('x-tag') || req.query['x-tag']) { 108 | var key = req.get('x-tag') || req.query['x-tag']; 109 | res.set('x-tag', key); 110 | res.set('Access-Control-Expose-Headers', 'x-tag'); 111 | if (req.query && req.query['x-tag']) { 112 | delete req.query['x-tag']; 113 | } 114 | 115 | if (req.method === 'POST' && config.secureMode && req.body.secure === true) { 116 | if (req.body.secureData) { 117 | var truthHash = req.body.truth; 118 | encryption.decrypt(req.body.secureData, key, truthHash) 119 | .then(function (resp) { 120 | if (typeof resp === 'object') { 121 | req.body = resp; 122 | next(); 123 | } else { 124 | debug('decryptedText: ', resp); 125 | var parsedJSON = JSON.parse(resp); 126 | req.body = parsedJSON; 127 | req.body.secure = true; 128 | next(); 129 | } 130 | }) 131 | .catch(function (err) { 132 | next(err); 133 | }); 134 | } else { 135 | res.badRequest(false, 'Expecting an encrypted data to be sent in the secureData body parameter.'); 136 | } 137 | } else { 138 | next(); 139 | } 140 | } else if (req.method !== 'POST') { 141 | next(); 142 | } else { 143 | res.badRequest(false, 'Please initialize and send the x-tag header in every request.'); 144 | } 145 | } 146 | }; 147 | -------------------------------------------------------------------------------- /test/routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.RATE_LIMIT = 10; 4 | var chai = require('chai'); 5 | chai.should(); 6 | var config = require('../../config'); 7 | var chaiAsPromised = require('chai-as-promised'); 8 | chai.use(chaiAsPromised); 9 | var request = require('supertest'); 10 | var router = require('../../routes'); 11 | var express = require('express'); 12 | var sinon = require('sinon'); 13 | var sinonChai = require('sinon-chai'); 14 | chai.use(sinonChai); 15 | 16 | var app4 = express(); 17 | 18 | app4.use('/',router); 19 | 20 | 21 | var agent4 = request.agent(app4); 22 | var requestId; 23 | 24 | var res = {}; 25 | var req = {}; 26 | 27 | var nextChecker = false; 28 | var next = function(){ 29 | if(arguments.length > 0){ 30 | console.log(arguments[0]); 31 | }else{ 32 | nextChecker = true; 33 | } 34 | 35 | return nextChecker; 36 | }; 37 | res.json = function(data){ 38 | return res; 39 | }; 40 | 41 | res.badRequest = sinon.spy(); 42 | 43 | res.status = function(status){ 44 | return res; 45 | }; 46 | 47 | var header = {}; 48 | res.set = function(key, value){ 49 | header[key] = value; 50 | return header[key]; 51 | }; 52 | req.get = function(key){ 53 | return header[key]; 54 | }; 55 | 56 | header.set = function(data){ 57 | header.temp = data; 58 | return header.temp; 59 | }; 60 | 61 | req.method = ''; 62 | 63 | // describe('Test rate limiting', function(){ 64 | 65 | // before(function(){ /* jslint ignore:line */ 66 | // var workers = require('../../services/queue/workers'); 67 | // var workers2 = require('../../services/queue/workers'); 68 | // var workers3 = require('../../services/queue/workers'); 69 | // }); 70 | 71 | // it('should reach request rate limit', function(done){ 72 | // agent4 73 | // .get('/initialize') 74 | // .then(function(res){ 75 | // console.log('Limit: ', res.headers['x-ratelimit-limit'], '| Remaining: ', res.headers['x-ratelimit-remaining'], ' | Body: ', res.body); 76 | // return agent4 77 | // .get('/initialize'); 78 | // }) 79 | // .then(function(res){ 80 | // console.log('Limit: ', res.headers['x-ratelimit-limit'], '| Remaining: ', res.headers['x-ratelimit-remaining'], ' | Body: ', res.body); 81 | // return agent4 82 | // .get('/initialize'); 83 | // }) 84 | // .then(function(res){ 85 | // console.log('Limit: ', res.headers['x-ratelimit-limit'], '| Remaining: ', res.headers['x-ratelimit-remaining'], ' | Body: ', res.body); 86 | // return agent4 87 | // .get('/initialize'); 88 | // }) 89 | // .then(function(res){ 90 | // console.log('Limit: ', res.headers['x-ratelimit-limit'], '| Remaining: ', res.headers['x-ratelimit-remaining'], ' | Body: ', res.body); 91 | // return agent4 92 | // .get('/initialize'); 93 | // }) 94 | // .then(function(res){ 95 | // console.log('Limit: ', res.headers['x-ratelimit-limit'], '| Remaining: ', res.headers['x-ratelimit-remaining'], ' | Body: ', res.body); 96 | // return agent4 97 | // .get('/initialize'); 98 | // }) 99 | // .then(function(res){ 100 | // console.log('Limit: ', res.headers['x-ratelimit-limit'], '| Remaining: ', res.headers['x-ratelimit-remaining'], ' | Body: ', res.body); 101 | // return agent4 102 | // .get('/initialize'); 103 | // }) 104 | // .then(function(res){ 105 | // console.log('Limit: ', res.headers['x-ratelimit-limit'], '| Remaining: ', res.headers['x-ratelimit-remaining'], ' | Body: ', res.body); 106 | // return agent4 107 | // .get('/initialize'); 108 | // }) 109 | // .then(function(res){ 110 | // console.log('Limit: ', res.headers['x-ratelimit-limit'], '| Remaining: ', res.headers['x-ratelimit-remaining'], ' | Body: ', res.body); 111 | // return agent4 112 | // .get('/initialize'); 113 | // }) 114 | // .then(function(res){ 115 | // console.log('Limit: ', res.headers['x-ratelimit-limit'], '| Remaining: ', res.headers['x-ratelimit-remaining'], ' | Body: ', res.body); 116 | // return agent4 117 | // .get('/initialize'); 118 | // }) 119 | // .then(function(res){ 120 | // console.log('Limit: ', res.headers['x-ratelimit-limit'], '| Remaining: ', res.headers['x-ratelimit-remaining'], ' | Body: ', res.body); 121 | // return agent4 122 | // .get('/initialize') 123 | // .expect(429); 124 | // }) 125 | // .then(function(res){ 126 | // console.log('Limit: ', res.headers['x-ratelimit-limit'], '| Remaining: ', res.headers['x-ratelimit-remaining'], ' | Body: ', res.body); 127 | // requestId = res.headers['x-request-id']; 128 | // done(); 129 | // }) 130 | // .catch(function(err){ 131 | // done(err); 132 | // }); 133 | // }); 134 | 135 | // it('should save rate limit error on request log', function(done){ 136 | // var RequestLog = require('../../models/RequestLogs'); 137 | // // It takes a while for the request log to update. So let us delay the test for 1 sec 138 | // setTimeout(function(){ 139 | // RequestLog.findOne({RequestId: requestId}) 140 | // .then(function(res){ 141 | // console.log('the request', res); 142 | // res.response.data.statusCode.should.equal(429); 143 | // done(); 144 | // }) 145 | // .catch(function(err){ 146 | // done(err); 147 | // }); 148 | // },10000); 149 | 150 | // }); 151 | 152 | // }); 153 | 154 | describe('Router', function(){ 155 | 156 | it('should contain a param function', function(done){ 157 | router._allRequestData(req, res, next); 158 | nextChecker.should.be.true; /* jslint ignore:line */ 159 | nextChecker = false; 160 | req.param.should.be.a('function'); 161 | done(); 162 | }); 163 | 164 | }); 165 | 166 | 167 | describe('Cache Test', function(){ 168 | it('should initialize the API cache', function(done){ 169 | res.set = sinon.spy(); 170 | router._APICache(req, res, next); 171 | nextChecker.should.be.true; /* jslint ignore:line */ 172 | nextChecker = false; 173 | req.cache.should.be.a('object'); 174 | req.cacheKey.should.be.a('array'); 175 | res.set.should.be.called.once; /* jslint ignore:line */ 176 | res.set.should.be.calledWith({'Cache-Control':'private, max-age='+config.frontendCacheExpiry+''}); 177 | done(); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /services/queue/jobs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var models = require('../../models'); 4 | var _ = require('lodash'); 5 | var log = require('../logger'); 6 | var encryption = require('../encryption'); 7 | var crypto = require('crypto'); 8 | var request = require('request-promise'); 9 | var q = require('q'); 10 | var debug = require('debug')('jobs'); 11 | var request2 = require('../request'); 12 | var queue = require('./'); 13 | 14 | var jobs = {}; 15 | 16 | // Logs all API requests 17 | jobs.createRequestLog = function(request, done){ 18 | log.info('logging API request: ',request.RequestId); 19 | models.RequestLogs.create(request) 20 | .then(function(res){ 21 | return done(false, res); 22 | }) 23 | .catch(function(err){ 24 | log.error(err); 25 | return done({statusCode: 422 , message: err}); 26 | }); 27 | }; 28 | 29 | // Logs all API responses 30 | jobs.updateRequestLog = function(response, done){ 31 | log.info('logging API response: ',response.requestId); 32 | var requestId = response.requestId; 33 | if(response && response.requestId){ 34 | delete response.requestId; 35 | } 36 | 37 | models.RequestLogs.update({RequestId: requestId},response) 38 | .then(function(res){ 39 | return done(false, res); 40 | }) 41 | .catch(function(err){ 42 | log.error(err); 43 | return done({statusCode: 422 , message: err}); 44 | }); 45 | }; 46 | 47 | // Creates search tags for all db records 48 | jobs.createSearchTags = function(data, done){ 49 | log.info('Creating search index for: ', data._id); 50 | var dataClone = _.extend({},data); 51 | var model = data.model; 52 | var isSQL = data.isSQL; 53 | 54 | var update = data.update ? true : false; 55 | if(dataClone && dataClone.update){ 56 | delete dataClone.update; 57 | } 58 | if(dataClone && dataClone.model){ 59 | delete dataClone.model; 60 | } 61 | if(dataClone && dataClone.isSQL){ 62 | delete dataClone.isSQL; 63 | } 64 | if(dataClone && dataClone.createdAt){ 65 | delete dataClone.createdAt; 66 | } 67 | if(dataClone && dataClone.updatedAt){ 68 | delete dataClone.updatedAt; 69 | } 70 | 71 | var ourDoc = dataClone; 72 | var split = []; 73 | 74 | for(var n in ourDoc){ 75 | if(ourDoc[n] === ourDoc._id){ /* jslint ignore:line */ 76 | // Skip 77 | }else if(ourDoc[n] === ourDoc.createdAt){ /* jslint ignore:line */ 78 | // Skip 79 | }else if(ourDoc[n] === ourDoc.updatedAt){ /* jslint ignore:line */ 80 | // Skip 81 | }else if(ourDoc[n] === ourDoc.tags){ /* jslint ignore:line */ 82 | // Skip 83 | }else{ 84 | if(typeof ourDoc[n] === 'string'){ 85 | split.push(ourDoc[n].split(' ')); 86 | }else{ /* jslint ignore:line */ 87 | // Move on nothing to see here 88 | } 89 | } 90 | 91 | } 92 | split = _.flattenDeep(split); 93 | 94 | var task; 95 | if(model){ 96 | if(isSQL){ 97 | task = models[model].update({ tags: split.join(', ')}, {where: dataClone} ); 98 | }else{ 99 | if(update){ 100 | task = models[model].update(dataClone,{ updatedAt: new Date(Date.now()).toISOString(), tags: split}); 101 | }else{ 102 | task = models[model].update(dataClone,{ tags: split}); 103 | } 104 | } 105 | 106 | task 107 | .then(function(res){ 108 | return done(false, res); 109 | }) 110 | .catch(function(err){ 111 | log.error(err); 112 | return done({statusCode: 422 , message: err}); 113 | }); 114 | }else{ 115 | return done({statusCode: 400 , message: 'No Model Passed!'}); 116 | } 117 | 118 | }; 119 | 120 | // Backup Data to Trash 121 | jobs.saveToTrash = function(data, done){ 122 | if(data.data){ 123 | log.info('Saving '+data.data._id+' to Trash...'); 124 | models.Trash.create(data) 125 | .then(function(res){ 126 | debug('Finished saving to trash: ', res); 127 | done(false, res); 128 | }) 129 | .catch(function(err){ 130 | done({statusCode: 422 , message: err}); 131 | }); 132 | }else{ 133 | done({statusCode: 400 , message: 'No data was passed'}); 134 | } 135 | 136 | }; 137 | 138 | // Send Webhook Event 139 | jobs.sendWebhook = function (data, done) { 140 | log.info('Sending Webhook...'); 141 | request2('webhook', data.reference, data.webhookURL, 'POST', data.data, { 142 | 'content-type': 'application/json' 143 | }) 144 | .then(function (resp) { 145 | done(false, resp); 146 | }) 147 | .catch(function (err) { 148 | // Retry in 5 minutes time 149 | queue.create('sendWebhook', data) 150 | .delay(5 * 60000) 151 | .save(); 152 | 153 | done(err); 154 | }); 155 | }; 156 | 157 | // Send HTTP Request 158 | // This is for jobs that can be configured from an admin dashboard. So an admin an configure the system to all an api at a particular time daily. 159 | // This can be used within the code too, to do some jobs. 160 | // Supports POST or GET 161 | // Other methods not quaranteed 162 | jobs.sendHTTPRequest = function(data, done){ 163 | log.info('Sending HTTP ' +data.method+' request to '+data.url+' with data => '+JSON.stringify(data.data)+' and headers => '+JSON.stringify(data.headers)); 164 | // Expected data 165 | // { 166 | // url: 'http://string.com', 167 | // method: 'POST', // or any http method 168 | // headers: { 169 | // 'User-Agent': 'Request-Promise' 170 | // }, 171 | // data: { 172 | // someData: 'this', 173 | // someOtherData: 'and this' 174 | // } 175 | // } 176 | // 177 | 178 | var options = { 179 | method: data.method, 180 | uri: data.url, 181 | body: data.data, 182 | headers: data.headers, 183 | json: true // Automatically parses the JSON string in the response 184 | }; 185 | 186 | if(data.method === 'GET'){ 187 | options.qs = data.data; 188 | }else if(data.method === 'POST'){ 189 | options.body = data.data; 190 | }else{ 191 | options.qs = data.data; 192 | options.body = data.data; 193 | } 194 | request(options) 195 | .then(function(resp){ 196 | done(false, resp); 197 | }) 198 | .catch(function(err){ 199 | done({statusCode: 422 , message: err.message}); 200 | }); 201 | }; 202 | 203 | module.exports = jobs; 204 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // ----------------- 3 | // -------------------------------------------------------------------- 4 | // JSHint Configuration, Strict Edition 5 | // -------------------------------------------------------------------- 6 | // 7 | // This is a options template for [JSHint][1], using [JSHint example][2] 8 | // and [Ory Band's example][3] as basis and setting config values to 9 | // be most strict: 10 | // 11 | // * set all enforcing options to true 12 | // * set all relaxing options to false 13 | // * set all environment options to false, except the browser value 14 | // * set all JSLint legacy options to false 15 | // 16 | // [1]: http://www.jshint.com/ 17 | // [2]: https://github.com/jshint/node-jshint/blob/master/example/config.json 18 | // [3]: https://github.com/oryband/dotfiles/blob/master/jshintrc 19 | // 20 | // @author http://michael.haschke.biz/ 21 | // @license http://unlicense.org/ 22 | 23 | // == Enforcing Options =============================================== 24 | // 25 | // These options tell JSHint to be more strict towards your code. Use 26 | // them if you want to allow only a safe subset of JavaScript, very 27 | // useful when your codebase is shared with a big number of developers 28 | // with different skill levels. 29 | 30 | "bitwise": true, // Prohibit bitwise operators (&, |, ^, etc.). 31 | "curly": true, // Require {} for every new block or scope. 32 | "eqeqeq": true, // Require triple equals i.e. `===`. 33 | "forin": true, // Tolerate `for in` loops without `hasOwnPrototype`. 34 | "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 35 | "latedef": true, // Prohibit variable use before definition. 36 | "newcap": true, // Require capitalization of all constructor functions e.g. `new F()`. 37 | "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. 38 | "noempty": true, // Prohibit use of empty blocks. 39 | "nonew": true, // Prohibit use of constructors for side-effects. 40 | "plusplus": true, // Prohibit use of `++` & `--`. 41 | "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. 42 | "undef": true, // Require all non-global variables be declared before they are used. 43 | "strict": true, // Require `use strict` pragma in every file. 44 | "trailing": true, // Prohibit trailing whitespaces. 45 | 46 | // == Relaxing Options ================================================ 47 | // 48 | // These options allow you to suppress certain types of warnings. Use 49 | // them only if you are absolutely positive that you know what you are 50 | // doing. 51 | 52 | "asi": false, // Tolerate Automatic Semicolon Insertion (no semicolons). 53 | "boss": false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 54 | "debug": false, // Allow debugger statements e.g. browser breakpoints. 55 | "eqnull": false, // Tolerate use of `== null`. 56 | "es5": false, // Allow EcmaScript 5 syntax. 57 | "esnext": false, // Allow ES.next specific features such as `const` and `let`. 58 | "evil": false, // Tolerate use of `eval`. 59 | "expr": false, // Tolerate `ExpressionStatement` as Programs. 60 | "funcscope": false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside. 61 | "globalstrict": false, // Allow global "use strict" (also enables 'strict'). 62 | "iterator": false, // Allow usage of __iterator__ property. 63 | "lastsemic": false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block. 64 | "laxbreak": false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 65 | "laxcomma": false, // Suppress warnings about comma-first coding style. 66 | "loopfunc": false, // Allow functions to be defined within loops. 67 | "multistr": false, // Tolerate multi-line strings. 68 | "onecase": false, // Tolerate switches with just one case. 69 | "proto": false, // Tolerate __proto__ property. This property is deprecated. 70 | "regexdash": false, // Tolerate unescaped last dash i.e. `[-...]`. 71 | "scripturl": false, // Tolerate script-targeted URLs. 72 | "smarttabs": false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only. 73 | "shadow": false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 74 | "sub": false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 75 | "supernew": false, // Tolerate `new function () { ... };` and `new Object;`. 76 | "validthis": false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function. 77 | 78 | // == Environments ==================================================== 79 | // 80 | // These options pre-define global variables that are exposed by 81 | // popular JavaScript libraries and runtime environments—such as 82 | // browser or node.js. 83 | 84 | "browser": false, // Standard browser globals e.g. `window`, `document`. 85 | "couch": false, // Enable globals exposed by CouchDB. 86 | "devel": false, // Allow development statements e.g. `console.log();`. 87 | "dojo": false, // Enable globals exposed by Dojo Toolkit. 88 | "jquery": false, // Enable globals exposed by jQuery JavaScript library. 89 | "mootools": false, // Enable globals exposed by MooTools JavaScript framework. 90 | "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. 91 | "nonstandard": false, // Define non-standard but widely adopted globals such as escape and unescape. 92 | "prototypejs": false, // Enable globals exposed by Prototype JavaScript framework. 93 | "rhino": false, // Enable globals available when your code is running inside of the Rhino runtime environment. 94 | "wsh": false, // Enable globals available when your code is running as a script for the Windows Script Host. 95 | 96 | // == JSLint Legacy =================================================== 97 | // 98 | // These options are legacy from JSLint. Aside from bug fixes they will 99 | // not be improved in any way and might be removed at any point. 100 | 101 | "nomen": false, // Prohibit use of initial or trailing underbars in names. 102 | "onevar": true, // Allow only one `var` statement per function. 103 | "passfail": false, // Stop on first error. 104 | "white": true, // Check against strict whitespace and indentation rules. 105 | 106 | // == Undocumented Options ============================================ 107 | // 108 | // While I've found these options in [example1][2] and [example2][3] 109 | // they are not described in the [JSHint Options documentation][4]. 110 | // 111 | // [4]: http://www.jshint.com/options/ 112 | 113 | "maxerr": 100, // Maximum errors before stopping. 114 | "predef": [ // Extra globals. 115 | //"exampleVar", 116 | "it", 117 | "describe" 118 | ], 119 | "indent": 4 // Specify indentation spacing 120 | } 121 | -------------------------------------------------------------------------------- /template/model_sql_test.tmpl: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | chai.should(); 5 | var config = require('../../config'); 6 | var chaiAsPromised = require('chai-as-promised'); 7 | // chai.use(chaiAsPromised); 8 | var mongooseMock = require('mongoose-mock'); 9 | // var expect = chai.expect; 10 | var sinon = require('sinon'); 11 | var q = require('q'); 12 | var sinonChai = require('sinon-chai'); 13 | chai.use(sinonChai); 14 | var <%= service %>; 15 | // Testing The <%= service %> Model 16 | describe('<%= service %> Model',function(){ 17 | 18 | var id; 19 | var id2; 20 | 21 | before(function(){ /* jslint ignore:line */ 22 | <%= service %> = require('../../models/<%= service %>s'); 23 | var workers = require('../../services/queue/workers'); 24 | var workers1 = require('../../services/queue/workers'); 25 | var workers2 = require('../../services/queue/workers'); 26 | var workers3 = require('../../services/queue/workers'); 27 | }); 28 | 29 | describe('<%= service %> CRUDS', function(){ 30 | it('should save data', function(done){ 31 | var my<%= object %> = <%= service %>.create({name: 'femi'}); 32 | 33 | my<%= object %>.then(function(res){ 34 | res.should.be.an.object; /* jslint ignore:line */ 35 | done(); 36 | }) 37 | .catch(function(err){ 38 | done(err); 39 | }); 40 | }); 41 | 42 | it('should read data', function(done){ 43 | var my<%= object %> = <%= service %>.findOne({where: {name: 'femi'}}); 44 | 45 | my<%= object %>.then(function(res){ 46 | res.should.be.an.object; /* jslint ignore:line */ 47 | done(); 48 | }) 49 | .catch(function(err){ 50 | done(err); 51 | }); 52 | }); 53 | 54 | it('should read all data', function(done){ 55 | var my<%= object %> = <%= service %>.findAll(); 56 | 57 | my<%= object %>.then(function(res){ 58 | res.should.be.an.array; /* jslint ignore:line */ 59 | done(); 60 | }) 61 | .catch(function(err){ 62 | done(err); 63 | }); 64 | }); 65 | 66 | it('should update data', function(done){ 67 | var cb = sinon.spy(); 68 | var my<%= object %> = <%= service %>.update({name: 'Olaoluwa'}, { where: {name: 'femi'}}); 69 | 70 | my<%= object %>.then(function(res){ 71 | cb(); 72 | cb.should.have.been.calledOnce; /* jslint ignore:line */ 73 | done(); 74 | }) 75 | .catch(function(err){ 76 | done(err); 77 | }); 78 | }); 79 | 80 | it('should search data', function(done){ 81 | // Search needs more work for more accuracy 82 | var my<%= object %> = <%= service %>.search('femi'); 83 | 84 | my<%= object %>.then(function(res){ 85 | res.should.be.an.object; /* jslint ignore:line */ 86 | done(); 87 | }) 88 | .catch(function(err){ 89 | done(err); 90 | }); 91 | }); 92 | 93 | it('should delete data', function(done){ 94 | var cb2 = sinon.spy(); 95 | var our<%= object %> = <%= service %>.bulkCreate([{name:'Olaolu'},{name: 'fola'},{name: 'bolu'}]); 96 | 97 | our<%= object %>.then(function(res){ 98 | return <%= service %>.destroy({where: {name: 'bolu'}}); 99 | }).then(function(res){ 100 | cb2(); 101 | cb2.should.have.been.calledOnce; /* jslint ignore:line */ 102 | done(); 103 | }) 104 | .catch(function(err){ 105 | done(err); 106 | }); 107 | }); 108 | 109 | it('should add createdAt', function(done){ 110 | var my<%= object %> = <%= service %>.create({name: 'this is for the gods'}); 111 | 112 | my<%= object %>.then(function(res){ 113 | id = res._id; 114 | res.should.have.property('createdAt'); 115 | done(); 116 | }) 117 | .catch(function(err){ 118 | done(err); 119 | }); 120 | }); 121 | 122 | it('should add updatedAt', function(done){ 123 | var my<%= object %> = <%= service %>.create({name: 'i am a demigod!'}); 124 | my<%= object %>.then(function(res){ 125 | id2 = res._id; 126 | return <%= service %>.update({name: 'This is the titan'}, {where: {_id: id}}); 127 | }) 128 | .then(function(res){ 129 | return <%= service %>.findOne({where: {name: 'This is the titan'}}); 130 | }) 131 | .then(function(res){ 132 | res.should.have.property('updatedAt'); 133 | done(); 134 | }) 135 | .catch(function(err){ 136 | done(err); 137 | }); 138 | }); 139 | 140 | it('should tag database entries properly', async function(){ 141 | var my<%= object %> = await <%= service %>.create({name: 'femi',someOtherStringData: 'stuff'}); 142 | 143 | return q.Promise(function(resolve, reject) { 144 | setTimeout(function(){ 145 | <%= service %>.findOne({where: {_id: my<%= object %>._id}}) 146 | .then(function(res){ 147 | if(typeof res.tags === 'string'){ 148 | var _tags = res.tags.split(', '); 149 | _tags.length.should.equal(2);/* jslint ignore:line */ 150 | }else{ 151 | res.tags.length.should.equal(2);/* jslint ignore:line */ 152 | } 153 | 154 | resolve(res); 155 | }) 156 | .catch(function(err){ 157 | reject(err); 158 | }); 159 | },3000); 160 | }); 161 | }); 162 | 163 | it('should count returned records', function(done){ 164 | var my<%= object %> = <%= service %>.count({where: {name: 'This is the titan'}}); 165 | 166 | my<%= object %>.then(function(res){ 167 | res.should.be.a.number; /* jslint ignore:line */ 168 | done(); 169 | }) 170 | .catch(function(err){ 171 | done(err); 172 | }); 173 | }); 174 | 175 | it('should find a record by id and delete', function(done){ 176 | var my<%= object %> = <%= service %>.findOne({where: {_id: id2}}); 177 | 178 | var <%= object %>er = my<%= object %>.then(function(res){ 179 | return res.destroy(); 180 | }) 181 | .then(function(res){ 182 | res.should.be.an('object'); 183 | done(); 184 | }); 185 | 186 | }); 187 | 188 | it('should have transaction object', function(done){ 189 | var my<%= object %> = <%= service %>.transaction.should.exist; /* jslint ignore:line */ 190 | done(); 191 | }); 192 | 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /test/services/response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | chai.should(); 5 | var config = require('../../config'); 6 | var chaiAsPromised = require('chai-as-promised'); 7 | chai.use(chaiAsPromised); 8 | var crypto = require('crypto'); 9 | 10 | // init 11 | 12 | var res = {}; 13 | var req = {}; 14 | var demoData = '{"el escribimos": "silencio es dorado"}'; 15 | var demoDataHash = crypto.createHash('sha512') 16 | .update(demoData) 17 | .digest('hex'); 18 | 19 | console.log('hash', demoDataHash); 20 | var nextChecker = false; 21 | var next = function(){ 22 | if(arguments.length > 0){ 23 | console.log(arguments[0]); 24 | }else{ 25 | nextChecker = true; 26 | } 27 | 28 | return nextChecker; 29 | }; 30 | res.json = function(data){ 31 | return res; 32 | }; 33 | 34 | res.status = function(status){ 35 | return res; 36 | }; 37 | 38 | var header = {}; 39 | res.set = function(key, value){ 40 | header[key] = value; 41 | return header[key]; 42 | }; 43 | req.get = function(key){ 44 | return header[key]; 45 | }; 46 | 47 | header.set = function(data){ 48 | header.temp = data; 49 | return header.temp; 50 | }; 51 | 52 | req.method = ''; 53 | 54 | var response = require('../../services/response'); 55 | 56 | var express = require('express'); 57 | var app = express(); 58 | var bodyParser = require('body-parser'); 59 | var request = require('supertest'); 60 | var router = require('../../routes'); 61 | 62 | // Dummy App 63 | app.use(bodyParser.urlencoded({ extended: false })); 64 | app.use(bodyParser.json()); 65 | app.use(response); 66 | app.use(router._APICache); 67 | 68 | 69 | app.get('/ok', function(req,res){ 70 | res.ok('It worked!'); 71 | }); 72 | 73 | app.get('/badRequest', function(req,res){ 74 | res.badRequest('It worked!'); 75 | }); 76 | 77 | app.get('/forbidden', function(req,res){ 78 | res.forbidden('It worked!'); 79 | }); 80 | 81 | app.get('/notFound', function(req,res){ 82 | res.notFound('It worked!'); 83 | }); 84 | 85 | app.get('/serverError', function(req,res){ 86 | res.serverError('It worked!'); 87 | }); 88 | 89 | app.get('/unauthorized', function(req,res){ 90 | res.unauthorized('It worked!'); 91 | }); 92 | 93 | app.get('/unprocessable', function(req,res){ 94 | res.unprocessable('It worked!'); 95 | }); 96 | 97 | 98 | 99 | 100 | var encryption = require('../../services/encryption'); 101 | var app2 = express(); 102 | 103 | // Dummy App 104 | app2.use(bodyParser.urlencoded({ extended: false })); 105 | app2.use(bodyParser.json()); 106 | app2.use(response); 107 | app2.use(encryption.interpreter); 108 | 109 | app2.post('/secure', function(req,res){ 110 | res.ok('It worked!'); 111 | }); 112 | 113 | var agent = request(app); 114 | 115 | var agent2 = request(app2); 116 | 117 | // Testing response service 118 | 119 | 120 | describe('#Response service test', function(){ 121 | 122 | before(function(){ /* jslint ignore:line */ 123 | var workers = require('../../services/queue/workers'); 124 | }); 125 | 126 | it('should add property ok, badRequest, forbidden, notFound, serverError and unauthorized to res object', function(done){ 127 | response(req,res,next); 128 | nextChecker = false; 129 | res.should.have.property('ok'); 130 | res.should.have.property('badRequest'); 131 | res.should.have.property('forbidden'); 132 | res.should.have.property('notFound'); 133 | res.should.have.property('serverError'); 134 | res.should.have.property('unauthorized'); 135 | res.should.have.property('unprocessable'); 136 | done(); 137 | }); 138 | 139 | it('should be ok', function(done){ 140 | agent. 141 | get('/ok') 142 | .expect(200,done); 143 | }); 144 | 145 | console.log(process.env.NO_CACHE); 146 | if(config.noFrontendCaching !== 'yes'){ 147 | it('should be a cached response', function(done){ 148 | agent. 149 | get('/ok') 150 | .expect(200) 151 | .then(function(res){ 152 | console.log(res.body); 153 | res.body.cached.should.be.true; /* jslint ignore:line */ 154 | done(); 155 | }) 156 | .catch(function(err){ 157 | done(err); 158 | }); 159 | }); 160 | } 161 | 162 | it('should be a badRequest', function(done){ 163 | agent. 164 | get('/badRequest') 165 | .expect(400,done); 166 | }); 167 | it('should be forbidden', function(done){ 168 | agent. 169 | get('/forbidden') 170 | .expect(403,done); 171 | }); 172 | it('should not be found', function(done){ 173 | agent. 174 | get('/notFound') 175 | .expect(404,done); 176 | }); 177 | it('should be unauthorized', function(done){ 178 | agent. 179 | get('/unauthorized') 180 | .expect(401,done); 181 | }); 182 | it('should be a serverError', function(done){ 183 | agent. 184 | get('/serverError') 185 | .expect(500,done); 186 | }); 187 | it('should be an unprocessable entity response', function(done){ 188 | agent. 189 | get('/unprocessable') 190 | .expect(422,done); 191 | }); 192 | 193 | it('should be an encrypted response', function(done){ 194 | var tag; 195 | encryption.generateKey() 196 | .then(function(res){ 197 | console.log(res); 198 | tag = res; 199 | return encryption.encrypt(demoData, tag); 200 | }) 201 | .then(function(res){ 202 | console.log('Our encrypted data: ', res.encryptedText); 203 | return agent2. 204 | post('/secure') 205 | .set('x-tag', tag) 206 | .send({truth: res.truth,secureData: res.encryptedText, secure: true}) 207 | .expect(200); 208 | }) 209 | .then(function(res){ 210 | console.log('Our response body: ', res.body); 211 | var data = res.body; 212 | data.secure.should.be.true; /* jslint ignore:line */ 213 | done(); 214 | }) 215 | .catch(function(err){ 216 | done(err); 217 | }); 218 | }); 219 | 220 | it('should detect tampered data', function(done){ 221 | var tag; 222 | encryption.generateKey() 223 | .then(function(res){ 224 | tag = res; 225 | var demoData2 = '{"escribimos": "silencios es dorado"}'; 226 | return encryption.encrypt(demoData2, tag); 227 | }) 228 | .then(function(res){ 229 | console.log('Our encrypted data: ', res); 230 | return agent2. 231 | post('/secure') 232 | .set('x-tag', tag) 233 | .send({truth: demoDataHash,secureData: res.encryptedText, secure: true}) 234 | .expect(400); 235 | }) 236 | .then(function(res){ 237 | console.log('Our response body: ', res.body); 238 | done(); 239 | }) 240 | .catch(function(err){ 241 | done(err); 242 | }); 243 | }); 244 | 245 | }); 246 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Express REST API Generator 2 | 3 | [![Build Status](https://travis-ci.org/iolufemi/Express-REST-API-Generator.svg?branch=dev)](https://travis-ci.org/iolufemi/Express-REST-API-Generator) [![codecov](https://codecov.io/gh/iolufemi/Express-REST-API-Generator/branch/master/graph/badge.svg)](https://codecov.io/gh/iolufemi/Express-REST-API-Generator) [![Documentation Status](https://readthedocs.org/projects/api-template/badge/?version=latest)](http://api-template.readthedocs.io/en/latest/?badge=latest) 4 | 5 | Express REST API Generator is an Express Based API skeleton. A template for starting projects with express as an API. This project can be used for creating a RESTful API using Node JS, Express as the framework, Mongoose to interact with a MongoDB instance and Sequelize for support of SQL compatible databases. Mocha is also used for running unit tests in the project. 6 | 7 | The resulting API from this project is a JSON REST API which will respond to requests over HTTP. REST Clients can, therefore, connect to the resulting REST server. 8 | 9 | ## What is API? 10 | 11 | In computer programming, an application programming interface (API) is a set of clearly defined methods of communication between various software components. A good API makes it easier to develop a computer program by providing all the building blocks, which are then put together by the programmer. An API may be for a web-based system, operating system, database system, computer hardware or software library. Just as a graphical user interface makes it easier for people to use programs, application programming interfaces make it easier for developers to use certain technologies in building applications. - [Wikipedia](https://en.wikipedia.org/wiki/Application_programming_interface) 12 | 13 | ## What is REST? 14 | 15 | Representational state transfer (REST) or RESTful web services is a way of providing interoperability between computer systems on the Internet. REST-compliant Web services allow requesting systems to access and manipulate textual representations of Web resources using a uniform and predefined set of stateless operations. - [Wikipedia](https://en.wikipedia.org/wiki/Representational_state_transfer) 16 | 17 | > NOTE: The use of this project requires that you have a basic knowledge of using express in building a REST API. If you are a newbie, here are some awesome tutorials to get you started. 18 | 19 | - [Build Node.js RESTful APIs in 10 Minutes](https://www.codementor.io/olatundegaruba/nodejs-restful-apis-in-10-minutes-q0sgsfhbd) 20 | - [Easily Develop Node.js and MongoDB Apps with Mongoose](https://scotch.io/tutorials/using-mongoosejs-in-node-js-and-mongodb-applications) 21 | - [Build a RESTful API Using Node and Express 4](https://scotch.io/tutorials/build-a-restful-api-using-node-and-express-4) 22 | 23 | ## Why use Express REST API Generator? 24 | 25 | 1. To enable you to develop REST APIs in the fastest way possible. 26 | 2. To encourage endpoint versioning. 27 | 3. To encourage unit testing and make it super easy to get started with writing unit tests by generating basic unit tests for generated components. 28 | 4. To enforce best practice in writing javascript apps by using lint. 29 | 5. To encourage good code file structure that can be easily followed by other team members, especially new team members. 30 | 6. To make it easy to build secure APIs with the ability to communicate with the frontend in an encrypted fashion. 31 | 7. To encourage backing up of deleted data. 32 | 8. To encourage logging API requests and responses for audit purposes. 33 | 9. To encourage proper Error handling and logging. 34 | 10. To encourage a uniform API response format across teams. 35 | 11. To make it easy to write asynchronous logic and applications using the inbuilt distributed job queue. 36 | 37 | ## Installation 38 | 39 | To start your project with Express REST API Generator, clone the repository from GitHub and install the dependencies. 40 | 41 | ``` 42 | $ git clone https://github.com/iolufemi/Express-REST-API-Generator.git ./yourProjectName 43 | $ cd yourProjectName 44 | $ npm install 45 | $ npm install -g mocha gulp 46 | ``` 47 | 48 | Then generate your first API endpoint 49 | 50 | ``` 51 | $ gulp service --name yourFirstEndpoint // This command will create a CRUD endpoint for yourFirstEndpoint. 52 | ``` 53 | 54 | With the `gulp service` command, you have the option of using either Mongo DB, an SQL compatible DB or using an API generated by this Express Generator as a database model. To use an API as a database you can pass the `baseurl` and the `endpoint` option for the API to the `gulp service `; for an SQL compatible db, pass the `sql` option. See an example below 55 | 56 | ### Using an API as a DB 57 | 58 | ``` 59 | $ gulp service --name yourEndpointWithAPIAsDB --baseurl http://localhost:8080 --endpoint users 60 | ``` 61 | 62 | ### Using an SQL compatible database 63 | 64 | ``` 65 | $ gulp service --name yourEndpointWITHSQL --sql 66 | ``` 67 | 68 | > Note: You can use -n instead of --name, -b instead of --baseurl, -e instead of --endpoint 69 | 70 | Try out your new endpoint. 71 | 72 | Start the app 73 | 74 | ``` 75 | $ npm start 76 | ``` 77 | by default, the app will start on `POST 8080` 78 | 79 | You can change the PORT by adding a `PORT` environment variable. 80 | eg. 81 | 82 | ``` 83 | $ PORT=6000 npm start 84 | ``` 85 | now the app will start on `PORT 6000` 86 | 87 | To start the app for development, run 88 | 89 | ``` 90 | $ gulp 91 | ``` 92 | This will automatically restart your app whenever a change is detected. 93 | 94 | You will now be able to access CRUD (create, read, update and delete) endpoints 95 | 96 | `[POST] http://localhost:8080/yourFirstEndpoint` Create yourFirstEndpoint resources 97 | `[GET] http://localhost:8080/yourFirstEndpoint` Get yourFirstEndpoint resources. Supports limits, sorting, pagination, select (projection), search and date range 98 | `[GET] http://localhost:8080/yourFirstEndpoint/:id` Get a yourFirstEndpoint resource 99 | `[PUT] http://localhost:8080/yourFirstEndpoint` Update yourFirstEndpoint resources 100 | `[PATCH] http://localhost:8080/yourFirstEndpoint/:id` Update one yourFirstEndpoint resource 101 | `[DELETE] http://localhost:8080/yourFirstEndpoint?key=value` Delete yourFirstEndpoint resources 102 | `[DELETE] http://localhost:8080/yourFirstEndpoint/:id` Delete one yourFirstEndpoint resource 103 | `[POST] http://localhost:8080/yourFirstEndpoint/:id/restore` Restore a previously deleted yourFirstEndpoint resource 104 | 105 | > Note: For every `POST` API calls you need to send an `x-tag` value in the header. This value is used for secure communication between the server and client. It is used for AES encrytion when secure mode is enabled. To get a valid `x-tag` call the `[GET] /initialize` endpoint. 106 | 107 | ## Some asynchronous goodness 108 | 109 | Start the clock (You should only have a single instance of this at all times.) 110 | 111 | ``` 112 | $ npm run clock 113 | ``` 114 | The clock is similar to a crontab. It dispatches tasks to available workers at a predefined interval. 115 | 116 | To define a clock, look for the `clock` collection in the MongoDB you connected to the `LOG_MONGOLAB_URL` environment variable, and create a record similar to the below 117 | 118 | ```json 119 | { 120 | "crontab" : "* * * * *", 121 | "name" : "Task Name", 122 | "job" : "theJobAsDefinedInTheWorkerFile", 123 | "enabled" : true 124 | } 125 | ``` 126 | 127 | > NOTE: Whenever you change the value of a clock on the DB, you need to restart the clock. (Still looking for the best way to make this automatic) 128 | 129 | Start the workers 130 | 131 | ``` 132 | $ npm run workers 133 | ``` 134 | A worker runs tasks or processes in the background. It is useful for running long running processes and background tasks. 135 | 136 | See `/services/queue/jobs` for sample tasks and `/services/queue/workers` for how to setup worker processes. 137 | 138 | ## Versioning your API endpoints 139 | 140 | You can create multiple versions of your API endpoints by simply adding the version number to your route file name. eg. `users.v1.js` will put a version of the users resources on the `/v1/users` endpoint. users.v2.js will put a version of the users resources on the `/v2/users` endpoint. The latest version of the resources will always be available at the `/users` endpoint. 141 | 142 | > NOTE: This project will automatically load route files found in the routes folder. 143 | 144 | ## File Structure 145 | 146 | - config 147 | - controllers 148 | - models 149 | - routes 150 | - services 151 | - templates 152 | - test 153 | 154 | ## Getting support, Reporting Bugs and Issues 155 | 156 | If you need support or want to report a bug, please log an issue [here](https://github.com/iolufemi/Express-REST-API-Generator/issues) 157 | 158 | ## Running Unit Tests 159 | 160 | All generated endpoints come with complete test suits, we encourage you to update the tests as you extend the logic 161 | 162 | ``` 163 | $ npm test 164 | ``` 165 | 166 | ## How to contribute 167 | 168 | View how to contribute [here](https://github.com/iolufemi/Express-REST-API-Generator/blob/master/CONTRIBUTING.md) 169 | 170 | ## Code of Conduct 171 | 172 | View the code of conduct [here](https://github.com/iolufemi/Express-REST-API-Generator/blob/master/CODE_OF_CONDUCT.md) 173 | 174 | ## Contributors 175 | 176 | - [Olufemi Olanipekun](https://github.com/iolufemi) 177 | 178 | ## FAQs 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express REST API Generator 2 | 3 | [![Build Status](https://travis-ci.org/iolufemi/Express-REST-API-Generator.svg?branch=dev)](https://travis-ci.org/iolufemi/Express-REST-API-Generator) [![codecov](https://codecov.io/gh/iolufemi/Express-REST-API-Generator/branch/master/graph/badge.svg)](https://codecov.io/gh/iolufemi/Express-REST-API-Generator) [![Documentation Status](https://readthedocs.org/projects/api-template/badge/?version=latest)](http://api-template.readthedocs.io/en/latest/?badge=latest) 4 | 5 | Express REST API Generator is an Express Based API skeleton. A template for starting projects with express as an API. This project can be used for creating a RESTful API using Node JS, Express as the framework, Mongoose to interact with a MongoDB instance and Sequelize for support of SQL compatible databases. Mocha is also used for running unit tests in the project. 6 | 7 | The resulting API from this project is a JSON REST API which will respond to requests over HTTP. REST Clients can, therefore, connect to the resulting REST server. 8 | 9 | ## What is API? 10 | 11 | In computer programming, an application programming interface (API) is a set of clearly defined methods of communication between various software components. A good API makes it easier to develop a computer program by providing all the building blocks, which are then put together by the programmer. An API may be for a web-based system, operating system, database system, computer hardware or software library. Just as a graphical user interface makes it easier for people to use programs, application programming interfaces make it easier for developers to use certain technologies in building applications. - [Wikipedia](https://en.wikipedia.org/wiki/Application_programming_interface) 12 | 13 | ## What is REST? 14 | 15 | Representational state transfer (REST) or RESTful web services is a way of providing interoperability between computer systems on the Internet. REST-compliant Web services allow requesting systems to access and manipulate textual representations of Web resources using a uniform and predefined set of stateless operations. - [Wikipedia](https://en.wikipedia.org/wiki/Representational_state_transfer) 16 | 17 | > NOTE: The use of this project requires that you have a basic knowledge of using express in building a REST API. If you are a newbie, here are some awesome tutorials to get you started. 18 | 19 | - [Build Node.js RESTful APIs in 10 Minutes](https://www.codementor.io/olatundegaruba/nodejs-restful-apis-in-10-minutes-q0sgsfhbd) 20 | - [Easily Develop Node.js and MongoDB Apps with Mongoose](https://scotch.io/tutorials/using-mongoosejs-in-node-js-and-mongodb-applications) 21 | - [Build a RESTful API Using Node and Express 4](https://scotch.io/tutorials/build-a-restful-api-using-node-and-express-4) 22 | 23 | ## Why use Express REST API Generator? 24 | 25 | 1. To enable you to develop REST APIs in the fastest way possible. 26 | 2. To encourage endpoint versioning. 27 | 3. To encourage unit testing and make it super easy to get started with writing unit tests by generating basic unit tests for generated components. 28 | 4. To enforce best practice in writing javascript apps by using lint. 29 | 5. To encourage good code file structure that can be easily followed by other team members, especially new team members. 30 | 6. To make it easy to build secure APIs with the ability to communicate with the frontend in an encrypted fashion. 31 | 7. To encourage backing up of deleted data. 32 | 8. To encourage logging API requests and responses for audit purposes. 33 | 9. To encourage proper Error handling and logging. 34 | 10. To encourage a uniform API response format across teams. 35 | 11. To make it easy to write asynchronous logic and applications using the inbuilt distributed job queue. 36 | 37 | ## Installation 38 | 39 | To start your project with Express REST API Generator, clone the repository from GitHub and install the dependencies. 40 | 41 | ``` 42 | $ git clone https://github.com/iolufemi/Express-REST-API-Generator.git ./yourProjectName 43 | $ cd yourProjectName 44 | $ npm install 45 | $ npm install -g mocha gulp 46 | ``` 47 | 48 | Then generate your first API endpoint 49 | 50 | ``` 51 | $ gulp service --name yourFirstEndpoint // This command will create a CRUD endpoint for yourFirstEndpoint. 52 | ``` 53 | 54 | With the `gulp service` command, you have the option of using either Mongo DB, an SQL compatible DB or using an API generated by this Express Generator as a database model. To use an API as a database you can pass the `baseurl` and the `endpoint` option for the API to the `gulp service `; for an SQL compatible db, pass the `sql` option. See an example below 55 | 56 | ### Using an API as a DB 57 | 58 | ``` 59 | $ gulp service --name yourEndpointWithAPIAsDB --baseurl http://localhost:8080 --endpoint users 60 | ``` 61 | 62 | ### Using an SQL compatible database 63 | 64 | ``` 65 | $ gulp service --name yourEndpointWITHSQL --sql 66 | ``` 67 | 68 | > Note: You can use -n instead of --name, -b instead of --baseurl, -e instead of --endpoint 69 | 70 | Try out your new endpoint. 71 | 72 | Start the app 73 | 74 | ``` 75 | $ npm start 76 | ``` 77 | by default, the app will start on `POST 8080` 78 | 79 | You can change the PORT by adding a `PORT` environment variable. 80 | eg. 81 | 82 | ``` 83 | $ PORT=6000 npm start 84 | ``` 85 | now the app will start on `PORT 6000` 86 | 87 | To start the app for development, run 88 | 89 | ``` 90 | $ gulp 91 | ``` 92 | This will automatically restart your app whenever a change is detected. 93 | 94 | You will now be able to access CRUD (create, read, update and delete) endpoints 95 | 96 | - `[POST] http://localhost:8080/yourFirstEndpoint` Create yourFirstEndpoint resources 97 | - `[GET] http://localhost:8080/yourFirstEndpoint` Get yourFirstEndpoint resources. Supports limits, sorting, pagination, select (projection), search and date range 98 | - `[GET] http://localhost:8080/yourFirstEndpoint/:id` Get a yourFirstEndpoint resource 99 | - `[PUT] http://localhost:8080/yourFirstEndpoint` Update yourFirstEndpoint resources 100 | - `[PATCH] http://localhost:8080/yourFirstEndpoint/:id` Update one yourFirstEndpoint resource 101 | - `[DELETE] http://localhost:8080/yourFirstEndpoint?key=value` Delete yourFirstEndpoint resources 102 | - `[DELETE] http://localhost:8080/yourFirstEndpoint/:id` Delete one yourFirstEndpoint resource 103 | - `[POST] http://localhost:8080/yourFirstEndpoint/:id/restore` Restore a previously deleted yourFirstEndpoint resource 104 | 105 | > Note: For every `POST` API calls you need to send an `x-tag` value in the header. This value is used for secure communication between the server and client. It is used for AES encrytion when secure mode is enabled. To get a valid `x-tag` call the `[GET] /initialize` endpoint. 106 | 107 | ## Some asynchronous goodness 108 | 109 | Start the clock (You should only have a single instance of this at all times.) 110 | 111 | ``` 112 | $ npm run clock 113 | ``` 114 | The clock is similar to a crontab. It dispatches tasks to available workers at a predefined interval. 115 | 116 | To define a clock, look for the `clock` collection in the MongoDB you connected to the `LOG_MONGOLAB_URL` environment variable, and create a record similar to the below 117 | 118 | ```json 119 | { 120 | "crontab" : "* * * * *", 121 | "name" : "Task Name", 122 | "job" : "theJobAsDefinedInTheWorkerFile", 123 | "enabled" : true 124 | } 125 | ``` 126 | 127 | > NOTE: Whenever you change the value of a clock on the DB, you need to restart the clock. (Still looking for the best way to make this automatic) 128 | 129 | Start the workers 130 | 131 | ``` 132 | $ npm run workers 133 | ``` 134 | A worker runs tasks or processes in the background. It is useful for running long running processes and background tasks. 135 | 136 | See `/services/queue/jobs` for sample tasks and `/services/queue/workers` for how to setup worker processes. 137 | 138 | ## Versioning your API endpoints 139 | 140 | You can create multiple versions of your API endpoints by simply adding the version number to your route file name. eg. `users.v1.js` will put a version of the users resources on the `/v1/users` endpoint. users.v2.js will put a version of the users resources on the `/v2/users` endpoint. The latest version of the resources will always be available at the `/users` endpoint. 141 | 142 | > NOTE: This project will automatically load route files found in the routes folder. 143 | 144 | ## File Structure 145 | 146 | - config 147 | - controllers 148 | - models 149 | - routes 150 | - services 151 | - templates 152 | - test 153 | 154 | ## Getting support, Reporting Bugs and Issues 155 | 156 | If you need support or want to report a bug, please log an issue [here](https://github.com/iolufemi/Express-REST-API-Generator/issues) 157 | 158 | ## Running Unit Tests 159 | 160 | All generated endpoints come with complete test suits, we encourage you to update the tests as you extend the logic 161 | 162 | ``` 163 | $ npm test 164 | ``` 165 | 166 | ## How to contribute 167 | 168 | View how to contribute [here](https://github.com/iolufemi/Express-REST-API-Generator/blob/master/CONTRIBUTING.md) 169 | 170 | ## Code of Conduct 171 | 172 | View the code of conduct [here](https://github.com/iolufemi/Express-REST-API-Generator/blob/master/CODE_OF_CONDUCT.md) 173 | 174 | ## Contributors 175 | 176 | - [Olufemi Olanipekun](https://github.com/iolufemi) 177 | 178 | ## FAQs 179 | -------------------------------------------------------------------------------- /test/services/queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | chai.should(); 5 | var config = require('../../config'); 6 | var chaiAsPromised = require('chai-as-promised'); 7 | chai.use(chaiAsPromised); 8 | var queue = require('../../services/queue'); 9 | 10 | var workers = require('../../services/queue/workers'); 11 | var sinon = require('sinon'); 12 | var sinonChai = require('sinon-chai'); 13 | chai.use(sinonChai); 14 | var fnv = require('fnv-plus'); 15 | var mongoose = require('mongoose'); 16 | 17 | 18 | // Test Index 19 | var jobs; 20 | describe('#Queue service', function(){ 21 | before(function() { /* jslint ignore:line */ 22 | queue.testMode.enter(); 23 | jobs = require('../../services/queue/jobs'); 24 | // Mock Server 25 | var express = require('express'); 26 | var app = express(); 27 | app.all('/',function(req,res,next){ 28 | res.json({status: 'ok'}); 29 | }); 30 | 31 | var server = app.listen('8081',function () { 32 | var host = server.address().address; 33 | var port = server.address().port; 34 | console.log('API server listening on host '+host+', port '+port+'!'); 35 | }); 36 | }); 37 | 38 | afterEach(function() { /* jslint ignore:line */ 39 | queue.testMode.clear(); 40 | }); 41 | 42 | after(function() { /* jslint ignore:line */ 43 | queue.testMode.exit(); 44 | }); 45 | 46 | it('should return an object', function(done){ 47 | queue.should.be.an('object'); 48 | queue.should.be.have.property('kue'); 49 | jobs.should.be.an('object'); 50 | workers.should.be.an('object'); 51 | done(); 52 | }); 53 | 54 | it('should pass basic smoke test', function() { 55 | queue.createJob('myJob', 'foo').save(); 56 | queue.createJob('anotherJob', { baz: 'bip' }).save(); 57 | queue.testMode.jobs.length.should.equal(2); 58 | queue.testMode.jobs[0].type.should.equal('myJob'); 59 | queue.testMode.jobs[0].data.should.equal('foo'); 60 | }); 61 | 62 | it('should load processes', function() { 63 | var process = require('../../services/queue/workers'); 64 | // We have configured queue to create 2 workers per job making a total of 6 workers for 3 jobs that we currently have 65 | process.workers.length.should.equal(6); 66 | }); 67 | 68 | // Test Jobs 69 | describe('#Testing Jobs', function(){ 70 | 71 | it('should run createRequestLog successfully', function(done){ 72 | var myrequestlog = { 73 | RequestId: fnv.hash(new Date().valueOf() + '59abab38ead925031a714967', 128).str(), 74 | ipAddress: '192.168.90.9', 75 | url: 'http://google.com', 76 | method: 'POST', 77 | body: {name: 'femi'}, 78 | createdAt: new Date() 79 | }; 80 | jobs.createRequestLog(myrequestlog,done); 81 | }); 82 | 83 | it('should run updateRequestLog successfully', function(done){ 84 | var myrequestlog = { 85 | requestId: fnv.hash(new Date().valueOf() + '59abab38ead925031a714966', 128).str(), 86 | response: { 87 | ipAddress: '192.168.90.9', 88 | url: 'http://google.com', 89 | method: 'POST', 90 | body: {name: 'femi'}, 91 | createdAt: new Date() 92 | } 93 | }; 94 | jobs.updateRequestLog(myrequestlog,done); 95 | }); 96 | it('should run createSearchTags successfully for saving data', function(done){ 97 | var myrequestlog = { 98 | RequestId: fnv.hash(new Date().valueOf() + '59abab38ead925031a714969', 128).str(), 99 | ipAddress: '192.168.90.9', 100 | url: 'http://google.com', 101 | method: 'POST', 102 | body: {name: 'femi'}, 103 | createdAt: new Date() 104 | }; 105 | 106 | myrequestlog.model = 'RequestLogs'; 107 | jobs.createSearchTags(myrequestlog,done); 108 | }); 109 | 110 | it('should run createSearchTags successfully for updating data', function(done){ 111 | var myrequestlog = { 112 | RequestId: fnv.hash(new Date().valueOf() + '59abab38ead925031a714968', 128).str(), 113 | ipAddress: '192.168.90.9', 114 | url: 'http://google.com', 115 | method: 'POST', 116 | body: {name: 'femi'}, 117 | createdAt: new Date() 118 | }; 119 | 120 | myrequestlog.model = 'RequestLogs'; 121 | myrequestlog.update = true; 122 | jobs.createSearchTags(myrequestlog,done); 123 | }); 124 | 125 | it('should run saveToTrash successfully for backing up data', function(done){ 126 | var backup = { 127 | data: { 128 | _id: mongoose.Types.ObjectId('59abab38ead925031a71496e'), 129 | name: 'foo' 130 | } 131 | }; 132 | 133 | jobs.saveToTrash(backup,done); 134 | }); 135 | 136 | it('should run sendWebhook successfully for sending realtime HTTP notifications', function(done){ 137 | var data = { 138 | reference: Date.now(), 139 | webhookURL: 'https://postman-echo.com/post', 140 | data: { 141 | someData: 'this', 142 | someOtherData: 'and this' 143 | } 144 | }; 145 | 146 | jobs.sendWebhook(data,done); 147 | }); 148 | 149 | // it('should run sendWebhook successfully for sending realtime HTTP notifications securely', function(done){ 150 | // var data = { 151 | // url: 'http://localhost:8081', 152 | // secure: true, // true or false 153 | // data: { 154 | // someData: 'this', 155 | // someOtherData: 'and this' 156 | // } 157 | // }; 158 | 159 | // jobs.sendWebhook(data,done); 160 | // }); 161 | 162 | it('should run sendHTTPRequest successfully for calling web services with POST method', function(done){ 163 | var data = { 164 | url: 'http://localhost:8081', 165 | method: 'POST', // or any http method 166 | headers: { 167 | 'User-Agent': 'Femi' 168 | }, 169 | data: { 170 | someData: 'this', 171 | someOtherData: 'and this' 172 | } 173 | }; 174 | 175 | jobs.sendHTTPRequest(data,done); 176 | }); 177 | 178 | it('should run sendHTTPRequest successfully for calling web services with GET method', function(done){ 179 | var data = { 180 | url: 'http://localhost:8081', 181 | method: 'GET', // or any http method 182 | headers: { 183 | 'User-Agent': 'Femi' 184 | }, 185 | data: { 186 | someData: 'this', 187 | someOtherData: 'and this' 188 | } 189 | }; 190 | 191 | jobs.sendHTTPRequest(data,done); 192 | }); 193 | 194 | it('should run sendHTTPRequest successfully for calling web services with PUT method', function(done){ 195 | var data = { 196 | url: 'http://localhost:8081', 197 | method: 'PUT', // or any http method 198 | headers: { 199 | 'User-Agent': 'Femi' 200 | }, 201 | data: { 202 | someData: 'this', 203 | someOtherData: 'and this' 204 | } 205 | }; 206 | 207 | jobs.sendHTTPRequest(data,done); 208 | }); 209 | 210 | it('should run sendHTTPRequest successfully for calling web services with DELETE method', function(done){ 211 | var data = { 212 | url: 'http://localhost:8081', 213 | method: 'DELETE', // or any http method 214 | headers: { 215 | 'User-Agent': 'Femi' 216 | }, 217 | data: { 218 | someData: 'this', 219 | someOtherData: 'and this' 220 | } 221 | }; 222 | 223 | jobs.sendHTTPRequest(data,done); 224 | }); 225 | 226 | it('should run sendHTTPRequest successfully for calling web services with PATCH method', function(done){ 227 | var data = { 228 | url: 'http://localhost:8081', 229 | method: 'PATCH', // or any http method 230 | headers: { 231 | 'User-Agent': 'Femi' 232 | }, 233 | data: { 234 | someData: 'this', 235 | someOtherData: 'and this' 236 | } 237 | }; 238 | 239 | jobs.sendHTTPRequest(data,function(err,data){ 240 | if(err){ 241 | done(err); 242 | }else{ 243 | console.log('data Yayyyy! ',data); 244 | done(); 245 | } 246 | 247 | 248 | }); 249 | }); 250 | 251 | }); 252 | }); 253 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var express = require('express'); 3 | var router = express.Router(); 4 | var response = require('../services/response'); 5 | var encryption = require('../services/encryption'); 6 | var log = require('../services/logger'); 7 | var me = require('../package.json'); 8 | var initialize = require('./initialize'); 9 | var config = require('../config'); 10 | var helmet = require('helmet'); 11 | var redisClient = require('../services/database').redis; 12 | var limiter = require('express-limiter')(router, redisClient); 13 | var _ = require('lodash'); 14 | var bodyParser = require('body-parser'); 15 | var cors = require('cors'); 16 | var hpp = require('hpp'); 17 | var contentLength = require('express-content-length-validator'); 18 | var MAX_CONTENT_LENGTH_ACCEPTED = config.maxContentLength * 1; 19 | var url = require('url'); 20 | var fnv = require('fnv-plus'); 21 | var RequestLogs = require('../models').RequestLogs; 22 | var Cacheman = require('cacheman'); 23 | var EngineRedis = require('cacheman-redis'); 24 | var queue = require('../services/queue'); 25 | var fileSystem = require('fs'); 26 | var shortId = require('shortid'); 27 | const nocache = require('nocache'); 28 | 29 | // load routes. Comes with versioning. unversioned routes should be named like 'user.js' 30 | // versioned files or routes should be named as user.v1.js. 31 | // The versioned routes will be available at /v1/routename or as the route version reads 32 | // The latest version will also be loaded on the default route /routename 33 | router._loadRoutes = function(routeFiles){ 34 | var versions = []; 35 | var ourRoutes = {}; 36 | // Number of routes, removing index and initialize 37 | var currentRoute = 0; 38 | var routeNum = routeFiles.length * 1; 39 | 40 | // Comes with endpoint versioning 41 | routeFiles.forEach(function(file) { 42 | currentRoute = currentRoute + 1; 43 | var splitFileName = file.split('.'); 44 | if(splitFileName[0] !== 'index' && splitFileName[0] !== 'initialize'){ 45 | 46 | if(splitFileName.length === 3){ 47 | ourRoutes[splitFileName[0]+'.'+splitFileName[1]] = require('./'+splitFileName[0]+'.'+splitFileName[1]); 48 | router.use('/'+splitFileName[1], ourRoutes[splitFileName[0]+'.'+splitFileName[1]]); 49 | var splitVersion = splitFileName[1].split('v'); 50 | var versionMap = {}; 51 | versionMap[splitFileName[0]] = splitVersion[1]; 52 | versions.push(versionMap); 53 | }else{ 54 | ourRoutes[splitFileName[0]] = require('./'+splitFileName[0]+'.'+splitFileName[1]); 55 | router.use('/', ourRoutes[splitFileName[0]]); 56 | } 57 | 58 | } 59 | if(currentRoute === routeNum){ 60 | var finalVersions = {}; 61 | _.forEach(versions, function(value){ 62 | _.forOwn(value, function(value, key){ 63 | if(_.has(finalVersions, key)){ 64 | finalVersions[key].push(value); 65 | }else{ 66 | finalVersions[key] = []; 67 | finalVersions[key].push(value); 68 | } 69 | }); 70 | }); 71 | _.forOwn(finalVersions, function(value, key){ 72 | var sorted = value.sort(); 73 | var sortedlength = sorted.length * 1; 74 | router.use('/', ourRoutes[key+'.v'+sortedlength]); 75 | }); 76 | } 77 | }); 78 | return ourRoutes; 79 | }; 80 | 81 | router._sanitizeRequestUrl = function(req) { 82 | var requestUrl = url.format({ 83 | protocol: req.protocol, 84 | host: req.hostname, 85 | pathname: req.originalUrl || req.url, 86 | query: req.query 87 | }); 88 | 89 | return requestUrl.replace(/(password=).*?(&|$)/ig, '$1$2'); 90 | }; 91 | 92 | router._allRequestData = function(req,res,next){ 93 | var requestData = {}; 94 | req.param = function(key, defaultValue){ 95 | var newRequestData = _.assignIn(requestData, req.params, req.body, req.query); 96 | if(newRequestData[key]){ 97 | return newRequestData[key]; 98 | }else if(defaultValue){ 99 | return defaultValue; 100 | }else{ 101 | return false; 102 | } 103 | }; 104 | next(); 105 | }; 106 | 107 | 108 | router._APICache = function(req,res,next){ 109 | var cache = new EngineRedis(redisClient); 110 | var APICache = new Cacheman(me.name, {engine: cache, ttl: config.backendCacheExpiry}); 111 | req.cache = APICache; 112 | // Tell Frontend to Cache responses 113 | res.set({'Cache-Control':'private, max-age='+config.frontendCacheExpiry+''}); 114 | 115 | var key = []; 116 | key.push(req.url); 117 | key.push(req.ip); 118 | key.push(req.get('user-agent')); 119 | if(req.accountId){ 120 | key.push(req.accountId); 121 | } 122 | // if(req.appId){ 123 | // key.push(req.appId); 124 | // } 125 | req.cacheKey = key; 126 | // Remember to delete cache when you get a POST call 127 | // Only cache GET calls 128 | if(req.method === 'GET'){ 129 | // if record is not in cache, set cache else get cache 130 | req.cache.get(req.cacheKey) 131 | .then(function(resp){ 132 | if(!resp){ 133 | // Will be set on successful response 134 | next(); 135 | }else{ 136 | res.ok(resp, true); 137 | } 138 | }) 139 | .catch(function(err){ 140 | log.error('Failed to get cached data: ', err); 141 | // Don't block the call because of this failure. 142 | next(); 143 | }); 144 | }else{ 145 | if(req.method === 'POST' || req.method === 'PUT' || req.method === 'PUSH' || req.method === 'PATCH' || req.method === 'DELETE'){ 146 | req.cache.del(req.cacheKey) 147 | .then(function(res){}) 148 | .catch(function(err){ 149 | log.error('Failed to delete cached data: ', err); 150 | // Don't block the call because of this failure. 151 | }); // No delays 152 | } 153 | next(); 154 | } 155 | 156 | }; 157 | 158 | router.use(helmet()); 159 | router.use(cors()); 160 | router.options('*', cors()); 161 | router.use(bodyParser.urlencoded({ extended: false })); 162 | router.use(bodyParser.json({limit: '50mb'})); 163 | router.use(bodyParser.raw({limit: '50mb'})); 164 | router.use(bodyParser.text({limit: '50mb'})); 165 | router.use(bodyParser.json()); 166 | router.use(bodyParser.raw()); 167 | router.use(bodyParser.text()); 168 | 169 | // Log requests here 170 | router.use(function(req,res,next){ 171 | var ipAddress = req.ip; 172 | req.requestId = fnv.hash(shortId.generate() + Math.floor(100000 + Math.random() * 900000) + '' + Date.now() + '' + ipAddress, 128).str(); 173 | res.set('X-Request-Id',req.requestId); 174 | 175 | var reqLog = { 176 | RequestId: req.requestId, 177 | ipAddress: ipAddress, 178 | url: router._sanitizeRequestUrl(req), 179 | method: req.method, 180 | body: _.omit(req.body, ['password','cardno']), 181 | app: req.appId, 182 | user: req.accountId, 183 | device: req.get('user-agent'), 184 | createdAt: new Date() 185 | }; 186 | 187 | // Dump it in the queue 188 | queue.create('logRequest', reqLog) 189 | .save(); 190 | 191 | 192 | // persist RequestLog entry in the background; continue immediately 193 | 194 | log.info(reqLog); 195 | next(); 196 | }); 197 | // load response handlers 198 | router.use(response); 199 | // Watch for encrypted requests 200 | router.use(encryption.interpreter); 201 | router.use(hpp()); 202 | router.use(contentLength.validateMax({max: MAX_CONTENT_LENGTH_ACCEPTED, status: 400, message: 'Stop! Maximum content length exceeded.'})); // max size accepted for the content-length 203 | // add the param function to request object 204 | router.use(router._allRequestData); 205 | 206 | // API Rate limiter 207 | limiter({ 208 | path: '*', 209 | method: 'all', 210 | lookup: ['ip','accountId','appId','developer'], 211 | total: config.rateLimit * 1, 212 | expire: config.rateLimitExpiry * 1, 213 | onRateLimited: function (req, res, next) { 214 | next({ message: 'Rate limit exceeded', statusCode: 429 }); 215 | } 216 | }); 217 | 218 | // no client side caching 219 | if(config.noFrontendCaching === 'yes'){ 220 | router.use(nocache()); 221 | }else{ 222 | router.use(router._APICache); 223 | } 224 | 225 | router.get('/', function (req, res) { 226 | res.ok({name: me.name, version: me.version}); 227 | }); 228 | 229 | // Let's Encrypt Setup 230 | router.get(config.letsencryptSSLVerificationURL, function(req,res){ 231 | res.send(config.letsencryptSSLVerificationBody); 232 | }); 233 | 234 | 235 | 236 | // Publicly available routes here, IE. routes that should work with out requiring userid, appid and developer. 237 | router.use('/', initialize); 238 | 239 | // Should automatically load routes 240 | // Other routes here 241 | 242 | var normalizedPath = require('path').join(__dirname, './'); 243 | var routeFiles = fileSystem.readdirSync(normalizedPath); 244 | 245 | router._loadRoutes(routeFiles); 246 | 247 | // Finished loading routes 248 | 249 | router.use(function(req, res, next) { // jshint ignore:line 250 | res.notFound(); 251 | }); 252 | 253 | router.use(log.errorHandler); 254 | 255 | module.exports = router; 256 | 257 | // ToDo: Test API versioning 258 | // ToDo: Test rate limiting 259 | // ToDo: Test complete route Loader test 260 | // ToDo: Test _sanitizeRequestUrl middleware function 261 | // ToDo: Test _allRequestData middleware function for default value scenario 262 | // ToDo: Make Log requests testable and write unit tests for it 263 | // ToDo: Develop the route loader into a separate node module to be publish on npm 264 | // ToDo: Develop all services onto separate node module to be publish on npm 265 | -------------------------------------------------------------------------------- /template/model_test.tmpl: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | chai.should(); 5 | var config = require('../../config'); 6 | var chaiAsPromised = require('chai-as-promised'); 7 | // chai.use(chaiAsPromised); 8 | var mongooseMock = require('mongoose-mock'); 9 | // var expect = chai.expect; 10 | var sinon = require('sinon'); 11 | var q = require('q'); 12 | var sinonChai = require('sinon-chai'); 13 | chai.use(sinonChai); 14 | var <%= service %>; 15 | // Testing The <%= service %> Model 16 | describe('<%= service %> Model',function(){ 17 | 18 | var id; 19 | var id2; 20 | 21 | before(function(){ /* jslint ignore:line */ 22 | <%= service %> = require('../../models/<%= service %>s'); 23 | var workers = require('../../services/queue/workers'); 24 | var workers1 = require('../../services/queue/workers'); 25 | var workers2 = require('../../services/queue/workers'); 26 | var workers3 = require('../../services/queue/workers'); 27 | }); 28 | 29 | describe('Test CRUDS', function(){ 30 | it('should save data', function(done){ 31 | var my<%= object %> = <%= service %>.create({name: 'femi'}); 32 | 33 | my<%= object %>.then(function(res){ 34 | res.should.be.an.object; /* jslint ignore:line */ 35 | done(); 36 | }) 37 | .catch(function(err){ 38 | done(err); 39 | }); 40 | }); 41 | 42 | it('should read data', function(done){ 43 | var my<%= object %> = <%= service %>.findOne({name: 'femi'}); 44 | 45 | my<%= object %>.then(function(res){ 46 | res.should.be.an.object; /* jslint ignore:line */ 47 | done(); 48 | }) 49 | .catch(function(err){ 50 | done(err); 51 | }); 52 | }); 53 | 54 | it('should read all data', function(done){ 55 | var my<%= object %> = <%= service %>.find(); 56 | 57 | my<%= object %>.then(function(res){ 58 | res.should.be.an.array; /* jslint ignore:line */ 59 | done(); 60 | }) 61 | .catch(function(err){ 62 | done(err); 63 | }); 64 | }); 65 | 66 | it('should update data', function(done){ 67 | var cb = sinon.spy(); 68 | var my<%= object %> = <%= service %>.updateMany({name: 'femi'},{name: 'Olaoluwa'}); 69 | 70 | my<%= object %>.then(function(res){ 71 | cb(); 72 | cb.should.have.been.calledOnce; /* jslint ignore:line */ 73 | done(); 74 | }) 75 | .catch(function(err){ 76 | done(err); 77 | }); 78 | }); 79 | 80 | it('should update many data', function(done){ 81 | var cb = sinon.spy(); 82 | var my<%= object %> = <%= service %>.updateMany({name: 'femi'},{name: 'Olaoluwa Olanipekun'}); 83 | 84 | my<%= object %>.then(function(res){ 85 | cb(); 86 | cb.should.have.been.calledOnce; /* jslint ignore:line */ 87 | done(); 88 | }) 89 | .catch(function(err){ 90 | done(err); 91 | }); 92 | }); 93 | 94 | it('should search data', function(done){ 95 | // Search needs more work for more accuracy 96 | var my<%= object %> = <%= service %>.search('femi'); 97 | 98 | my<%= object %>.then(function(res){ 99 | res.should.be.an.object; /* jslint ignore:line */ 100 | done(); 101 | }) 102 | .catch(function(err){ 103 | done(err); 104 | }); 105 | }); 106 | 107 | it('should delete data', function(done){ 108 | var cb2 = sinon.spy(); 109 | var our<%= object %> = <%= service %>.create([{name:'Olaolu'},{name: 'fola'},{name: 'bolu'}]); 110 | 111 | our<%= object %>.then(function(res){ 112 | res.should.be.an.object; /* jslint ignore:line */ 113 | return <%= service %>.deleteOne({name: 'bolu'}); 114 | }).then(function(res){ 115 | cb2(); 116 | cb2.should.have.been.calledOnce; /* jslint ignore:line */ 117 | done(); 118 | }) 119 | .catch(function(err){ 120 | done(err); 121 | }); 122 | }); 123 | 124 | it('should delete many data', function(done){ 125 | var cb = sinon.spy(); 126 | var my<%= object %> = <%= service %>.deleteMany({name: 'femi'}); 127 | 128 | my<%= object %>.then(function(res){ 129 | cb(); 130 | cb.should.have.been.calledOnce; /* jslint ignore:line */ 131 | done(); 132 | }) 133 | .catch(function(err){ 134 | done(err); 135 | }); 136 | }); 137 | 138 | it('should add createdAt', function(done){ 139 | var my<%= object %> = <%= service %>.create({name: 'this is for the gods'}); 140 | 141 | my<%= object %>.then(function(res){ 142 | id = res._id; 143 | res.should.have.property('createdAt'); 144 | done(); 145 | }) 146 | .catch(function(err){ 147 | done(err); 148 | }); 149 | }); 150 | 151 | it('should add updatedAt', function(done){ 152 | var my<%= object %> = <%= service %>.create({name: 'i am a demigod!'}); 153 | my<%= object %>.then(function(res){ 154 | id2 = res._id; 155 | return <%= service %>.updateMany({_id: id},{name: 'This is the titan'}); 156 | }) 157 | .then(function(res){ 158 | return <%= service %>.findById(id); 159 | }) 160 | .then(function(res){ 161 | res.should.have.property('updatedAt'); 162 | done(); 163 | }) 164 | .catch(function(err){ 165 | done(err); 166 | }); 167 | }); 168 | 169 | it('should tag database entries properly', async function(){ 170 | var my<%= object %> = await <%= service %>.create({name: 'femi',someOtherStringData: 'stuff'}); 171 | 172 | return q.Promise(function(resolve, reject) { 173 | setTimeout(function(){ 174 | <%= service %>.findById(my<%= object %>._id) 175 | .then(function(res){ 176 | console.log(res); 177 | res.tags.length.should.equal(2);/* jslint ignore:line */ 178 | resolve(res); 179 | }) 180 | .catch(function(err){ 181 | reject(err); 182 | }); 183 | },3000); 184 | }); 185 | 186 | }); 187 | 188 | it('should count returned records', function(done){ 189 | var my<%= object %> = <%= service %>.estimatedDocumentCount({name: 'This is the titan'}); 190 | 191 | my<%= object %>.then(function(res){ 192 | res.should.be.a.number; /* jslint ignore:line */ 193 | done(); 194 | }) 195 | .catch(function(err){ 196 | done(err); 197 | }); 198 | }); 199 | 200 | it('should find a record by id', function(done){ 201 | var my<%= object %> = <%= service %>.findById(id); 202 | 203 | my<%= object %>.then(function(res){ 204 | res.should.be.an.object; /* jslint ignore:line */ 205 | done(); 206 | }) 207 | .catch(function(err){ 208 | done(err); 209 | }); 210 | }); 211 | 212 | it('should find a record by id and delete', function(done){ 213 | var my<%= object %> = <%= service %>.findByIdAndRemove(id2); 214 | 215 | my<%= object %>.then(function(res){ 216 | res.should.be.an.object; /* jslint ignore:line */ 217 | done(); 218 | }) 219 | .catch(function(err){ 220 | done(err); 221 | }); 222 | }); 223 | 224 | it('should find a record by id and update', function(done){ 225 | var my<%= object %> = <%= service %>.findByIdAndUpdate(id,{name: 'fufu'}); 226 | 227 | my<%= object %>.then(function(res){ 228 | res.should.be.an.object; /* jslint ignore:line */ 229 | done(); 230 | }) 231 | .catch(function(err){ 232 | done(err); 233 | }); 234 | }); 235 | 236 | it('should find the first match from a query', function(done){ 237 | var my<%= object %> = <%= service %>.findOne({name: 'fufu'}); 238 | 239 | my<%= object %>.then(function(res){ 240 | res.should.be.an.object; /* jslint ignore:line */ 241 | done(); 242 | }) 243 | .catch(function(err){ 244 | done(err); 245 | }); 246 | }); 247 | 248 | it('should find the first match from a query and update', function(done){ 249 | var my<%= object %> = <%= service %>.findOneAndUpdate({name: 'fufu'},{name: 'funmi'}); 250 | 251 | my<%= object %>.then(function(res){ 252 | res.should.be.an.object; /* jslint ignore:line */ 253 | done(); 254 | }) 255 | .catch(function(err){ 256 | done(err); 257 | }); 258 | }); 259 | 260 | it('should find the first match from a query and delete', function(done){ 261 | var my<%= object %> = <%= service %>.findOneAndRemove({name: 'funmi'}); 262 | 263 | my<%= object %>.then(function(res){ 264 | res.should.be.an.object; /* jslint ignore:line */ 265 | done(); 266 | }) 267 | .catch(function(err){ 268 | done(err); 269 | }); 270 | }); 271 | 272 | }); 273 | }); 274 | -------------------------------------------------------------------------------- /test/models/requestlogs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | chai.should(); 5 | var config = require('../../config'); 6 | var chaiAsPromised = require('chai-as-promised'); 7 | // chai.use(chaiAsPromised); 8 | var mongooseMock = require('mongoose-mock'); 9 | // var expect = chai.expect; 10 | var sinon = require('sinon'); 11 | var sinonChai = require('sinon-chai'); 12 | chai.use(sinonChai); 13 | var fnv = require('fnv-plus'); 14 | 15 | var RequestLog; 16 | var objId1 = fnv.hash(new Date().valueOf() + '59abab38ead925031a714961', 128).str(); 17 | var objId2 = fnv.hash(new Date().valueOf() + '59abab38ead925031a714962', 128).str(); 18 | var objId3 = fnv.hash(new Date().valueOf() + '59abab38ead925031a714963', 128).str(); 19 | var objId4 = fnv.hash(new Date().valueOf() + '59abab38ead925031a714964', 128).str(); 20 | // Testing The RequestLogs Model 21 | describe('RequestLog Model',function(){ 22 | 23 | var id; 24 | var id2; 25 | 26 | before(function(){ /* jslint ignore:line */ 27 | RequestLog = require('../../models/RequestLogs'); 28 | var workers = require('../../services/queue/workers'); 29 | }); 30 | 31 | describe('Test CRUDS', function(){ 32 | it('should save data', function(done){ 33 | var myrequestlog = RequestLog.create({ 34 | RequestId: objId1, 35 | ipAddress: '192.168.90.9', 36 | url: 'http://google.com', 37 | method: 'POST', 38 | body: {name: 'femi'}, 39 | createdAt: new Date() 40 | }); 41 | 42 | myrequestlog.then(function(res){ 43 | res.should.be.an.object; /* jslint ignore:line */ 44 | done(); 45 | }) 46 | .catch(function(err){ 47 | done(err); 48 | }); 49 | }); 50 | 51 | it('should read data', function(done){ 52 | var myrequestlog = RequestLog.findOne({RequestId: objId1}); 53 | 54 | myrequestlog.then(function(res){ 55 | res.should.be.an.object; /* jslint ignore:line */ 56 | done(); 57 | }) 58 | .catch(function(err){ 59 | done(err); 60 | }); 61 | }); 62 | 63 | it('should read all data', function(done){ 64 | var myrequestlog = RequestLog.find(); 65 | 66 | myrequestlog.then(function(res){ 67 | res.should.be.an.array; /* jslint ignore:line */ 68 | done(); 69 | }) 70 | .catch(function(err){ 71 | done(err); 72 | }); 73 | }); 74 | 75 | it('should update data', function(done){ 76 | var cb = sinon.spy(); 77 | var myrequestlog = RequestLog.updateMany({RequestId: objId1},{RequestId: objId2}); 78 | 79 | myrequestlog.then(function(res){ 80 | cb(); 81 | cb.should.have.been.calledOnce; /* jslint ignore:line */ 82 | done(); 83 | }) 84 | .catch(function(err){ 85 | done(err); 86 | }); 87 | }); 88 | 89 | it('should update many data', function(done){ 90 | var cb = sinon.spy(); 91 | var myrequestlog = RequestLog.updateMany({RequestId: objId2},{RequestId: objId3}); 92 | 93 | myrequestlog.then(function(res){ 94 | cb(); 95 | cb.should.have.been.calledOnce; /* jslint ignore:line */ 96 | done(); 97 | }) 98 | .catch(function(err){ 99 | done(err); 100 | }); 101 | }); 102 | 103 | it('should search data', function(done){ 104 | // Search needs more work for more accuracy 105 | var myrequestlog = RequestLog.search('gftgd'); 106 | 107 | myrequestlog.then(function(res){ 108 | res.should.be.an.object; /* jslint ignore:line */ 109 | done(); 110 | }) 111 | .catch(function(err){ 112 | done(err); 113 | }); 114 | }); 115 | 116 | it('should delete data', function(done){ 117 | var cb2 = sinon.spy(); 118 | var ourrequestlog = RequestLog.create([{ 119 | RequestId: objId2, 120 | ipAddress: '192.168.90.9', 121 | url: 'http://google.com', 122 | method: 'POST', 123 | body: {name: 'femi'}, 124 | createdAt: new Date() 125 | },{ 126 | RequestId: objId1, 127 | ipAddress: '192.168.90.9', 128 | url: 'http://google.com', 129 | method: 'POST', 130 | body: {name: 'fqwwemi'}, 131 | createdAt: new Date() 132 | }]); 133 | var myrequestlog = RequestLog.deleteOne({RequestId: objId1}); 134 | 135 | ourrequestlog.then(function(res){ 136 | res.should.be.an.object; /* jslint ignore:line */ 137 | return myrequestlog; 138 | }).then(function(res){ 139 | cb2(); 140 | cb2.should.have.been.calledOnce; /* jslint ignore:line */ 141 | done(); 142 | }) 143 | .catch(function(err){ 144 | done(err); 145 | }); 146 | }); 147 | 148 | it('should delete many data', function(done){ 149 | var cb = sinon.spy(); 150 | var myrequestlog = RequestLog.deleteMany({RequestId: objId2}); 151 | 152 | myrequestlog.then(function(res){ 153 | cb(); 154 | cb.should.have.been.calledOnce; /* jslint ignore:line */ 155 | done(); 156 | }) 157 | .catch(function(err){ 158 | done(err); 159 | }); 160 | }); 161 | 162 | it('should add createdAt', function(done){ 163 | var myrequestlog = RequestLog.create({RequestId: objId2}); 164 | 165 | myrequestlog.then(function(res){ 166 | id = res._id; 167 | res.should.have.property('createdAt'); 168 | done(); 169 | }) 170 | .catch(function(err){ 171 | done(err); 172 | }); 173 | }); 174 | 175 | it('should add updatedAt', function(done){ 176 | var myrequestlog = RequestLog.create({RequestId: objId1}); 177 | myrequestlog.then(function(res){ 178 | id2 = res._id; 179 | return RequestLog.updateMany({_id: id},{RequestId: objId4}); 180 | }) 181 | .then(function(res){ 182 | return RequestLog.findOne({_id: id}); 183 | }) 184 | .then(function(res){ 185 | res.should.have.property('updatedAt'); 186 | done(); 187 | }) 188 | .catch(function(err){ 189 | done(err); 190 | }); 191 | }); 192 | 193 | it('should count returned records', function(done){ 194 | var myrequestlog = RequestLog.estimatedDocumentCount({RequestId: objId2}); 195 | 196 | myrequestlog.then(function(res){ 197 | res.should.be.a.number; /* jslint ignore:line */ 198 | done(); 199 | }) 200 | .catch(function(err){ 201 | done(err); 202 | }); 203 | }); 204 | 205 | it('should find a record by id', function(done){ 206 | var myrequestlog = RequestLog.findById(id); 207 | 208 | myrequestlog.then(function(res){ 209 | res.should.be.an.object; /* jslint ignore:line */ 210 | done(); 211 | }) 212 | .catch(function(err){ 213 | done(err); 214 | }); 215 | }); 216 | 217 | it('should find a record by id and delete', function(done){ 218 | var myrequestlog = RequestLog.findByIdAndRemove(id2); 219 | 220 | myrequestlog.then(function(res){ 221 | res.should.be.an.object; /* jslint ignore:line */ 222 | done(); 223 | }) 224 | .catch(function(err){ 225 | done(err); 226 | }); 227 | }); 228 | 229 | it('should find a record by id and update', function(done){ 230 | var myrequestlog = RequestLog.findByIdAndUpdate(id,{name: 'fufu'}); 231 | 232 | myrequestlog.then(function(res){ 233 | res.should.be.an.object; /* jslint ignore:line */ 234 | done(); 235 | }) 236 | .catch(function(err){ 237 | done(err); 238 | }); 239 | }); 240 | 241 | it('should find the first match from a query', function(done){ 242 | var myrequestlog = RequestLog.findOne({RequestId: objId4}); 243 | 244 | myrequestlog.then(function(res){ 245 | res.should.be.an.object; /* jslint ignore:line */ 246 | done(); 247 | }) 248 | .catch(function(err){ 249 | done(err); 250 | }); 251 | }); 252 | 253 | it('should find the first match from a query and update', function(done){ 254 | var myrequestlog = RequestLog.findOneAndUpdate({RequestId: objId4},{RequestId: objId1}); 255 | 256 | myrequestlog.then(function(res){ 257 | res.should.be.an.object; /* jslint ignore:line */ 258 | done(); 259 | }) 260 | .catch(function(err){ 261 | done(err); 262 | }); 263 | }); 264 | 265 | it('should find the first match from a query and delete', function(done){ 266 | var myrequestlog = RequestLog.findOneAndRemove({RequestId: objId1}); 267 | 268 | myrequestlog.then(function(res){ 269 | res.should.be.an.object; /* jslint ignore:line */ 270 | done(); 271 | }) 272 | .catch(function(err){ 273 | done(err); 274 | }); 275 | }); 276 | 277 | }); 278 | }); 279 | 280 | 281 | // test populate 282 | -------------------------------------------------------------------------------- /test/models/trash.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | chai.should(); 5 | var config = require('../../config'); 6 | var chaiAsPromised = require('chai-as-promised'); 7 | // chai.use(chaiAsPromised); 8 | var mongooseMock = require('mongoose-mock'); 9 | // var expect = chai.expect; 10 | var sinon = require('sinon'); 11 | var sinonChai = require('sinon-chai'); 12 | chai.use(sinonChai); 13 | 14 | var Trash; 15 | // Testing The Trashs Model 16 | describe('Trash Model',function(){ 17 | 18 | var id; 19 | var id2; 20 | 21 | before(function(){ /* jslint ignore:line */ 22 | Trash = require('../../models/Trash'); 23 | var workers = require('../../services/queue/workers'); 24 | }); 25 | 26 | describe('Test CRUDS', function(){ 27 | it('should save data', function(done){ 28 | var mytrash = Trash.create({data: { 29 | RequestId: 'gdfd6563', 30 | ipAddress: '192.168.90.9', 31 | url: 'http://google.com', 32 | method: 'POST', 33 | body: {name: 'femi'}, 34 | createdAt: new Date() 35 | }}); 36 | 37 | mytrash.then(function(res){ 38 | res.should.be.an.object; /* jslint ignore:line */ 39 | done(); 40 | }) 41 | .catch(function(err){ 42 | done(err); 43 | }); 44 | }); 45 | 46 | it('should read data', function(done){ 47 | var mytrash = Trash.findOne({'data.RequestId': 'gdfd6563'}); 48 | 49 | mytrash.then(function(res){ 50 | res.should.be.an.object; /* jslint ignore:line */ 51 | done(); 52 | }) 53 | .catch(function(err){ 54 | done(err); 55 | }); 56 | }); 57 | 58 | it('should read all data', function(done){ 59 | var mytrash = Trash.find(); 60 | 61 | mytrash.then(function(res){ 62 | res.should.be.an.array; /* jslint ignore:line */ 63 | done(); 64 | }) 65 | .catch(function(err){ 66 | done(err); 67 | }); 68 | }); 69 | 70 | it('should update data', function(done){ 71 | var cb = sinon.spy(); 72 | var mytrash = Trash.updateMany({'data.RequestId': 'gdfd6563'},{'data.RequestId': 'gfdvdt09876543456789'}); 73 | 74 | mytrash.then(function(res){ 75 | cb(); 76 | cb.should.have.been.calledOnce; /* jslint ignore:line */ 77 | done(); 78 | }) 79 | .catch(function(err){ 80 | done(err); 81 | }); 82 | }); 83 | 84 | it('should update many data', function(done){ 85 | var cb = sinon.spy(); 86 | var mytrash = Trash.updateMany({'data.RequestId': 'gfdvdt09876543456789'},{'data.RequestId': 'kokoko456789'}); 87 | 88 | mytrash.then(function(res){ 89 | cb(); 90 | cb.should.have.been.calledOnce; /* jslint ignore:line */ 91 | done(); 92 | }) 93 | .catch(function(err){ 94 | done(err); 95 | }); 96 | }); 97 | 98 | it('should search data', function(done){ 99 | // Search needs more work for more accuracy 100 | var mytrash = Trash.search('kokoko456789'); 101 | 102 | mytrash.then(function(res){ 103 | res.should.be.an.object; /* jslint ignore:line */ 104 | done(); 105 | }) 106 | .catch(function(err){ 107 | done(err); 108 | }); 109 | }); 110 | 111 | it('should delete data', function(done){ 112 | var cb2 = sinon.spy(); 113 | var ourtrash = Trash.create([{data: { 114 | RequestId: 'gdfd6563', 115 | ipAddress: '192.168.90.9', 116 | url: 'http://google.com', 117 | method: 'POST', 118 | body: {name: 'femi'}, 119 | createdAt: new Date() 120 | }},{data: { 121 | RequestId: 'gsdfghjk98765dfd6563', 122 | ipAddress: '192.168.90.9', 123 | url: 'http://google.com', 124 | method: 'POST', 125 | body: {name: 'fe6mi'}, 126 | createdAt: new Date() 127 | }},{data: { 128 | RequestId: 'gdf099olllojd6563', 129 | ipAddress: '192.168.90.9', 130 | url: 'http://google.com', 131 | method: 'POST', 132 | body: {name: 'femi4'}, 133 | createdAt: new Date() 134 | }}]); 135 | var mytrash = Trash.deleteOne({'data.RequestId': 'kokoko456789'}); 136 | 137 | ourtrash.then(function(res){ 138 | res.should.be.an.object; /* jslint ignore:line */ 139 | return mytrash; 140 | }).then(function(res){ 141 | cb2(); 142 | cb2.should.have.been.calledOnce; /* jslint ignore:line */ 143 | done(); 144 | }) 145 | .catch(function(err){ 146 | done(err); 147 | }); 148 | }); 149 | 150 | it('should delete many data', function(done){ 151 | var cb = sinon.spy(); 152 | var mytrash = Trash.deleteMany({'data.RequestId': 'kokoko456789'}); 153 | 154 | mytrash.then(function(res){ 155 | cb(); 156 | cb.should.have.been.calledOnce; /* jslint ignore:line */ 157 | done(); 158 | }) 159 | .catch(function(err){ 160 | done(err); 161 | }); 162 | }); 163 | 164 | it('should add createdAt', function(done){ 165 | var mytrash = Trash.create({data: { 166 | RequestId: 'gdf099olllojd6563', 167 | ipAddress: '192.168.90.9', 168 | url: 'http://google.com', 169 | method: 'POST', 170 | body: {name: 'femi4'}, 171 | createdAt: new Date() 172 | }}); 173 | 174 | mytrash.then(function(res){ 175 | id = res._id; 176 | res.should.have.property('createdAt'); 177 | done(); 178 | }) 179 | .catch(function(err){ 180 | done(err); 181 | }); 182 | }); 183 | 184 | it('should add updatedAt', function(done){ 185 | var mytrash = Trash.create({data: { 186 | RequestId: 'gdf099olllojd6563', 187 | ipAddress: '192.168.90.9', 188 | url: 'http://google.com', 189 | method: 'POST', 190 | body: {name: 'femi4'}, 191 | createdAt: new Date() 192 | }}); 193 | mytrash.then(function(res){ 194 | id2 = res._id; 195 | return Trash.updateMany({_id: id},{'data.RequestId': 'kgtggokoko456789'}); 196 | }) 197 | .then(function(res){ 198 | return Trash.findOne({_id: id}); 199 | }) 200 | .then(function(res){ 201 | res.should.have.property('updatedAt'); 202 | done(); 203 | }) 204 | .catch(function(err){ 205 | done(err); 206 | }); 207 | }); 208 | 209 | it('should count returned records', function(done){ 210 | var mytrash = Trash.estimatedDocumentCount({'data.RequestId': 'kgtggokoko456789'}); 211 | 212 | mytrash.then(function(res){ 213 | res.should.be.a.number; /* jslint ignore:line */ 214 | done(); 215 | }) 216 | .catch(function(err){ 217 | done(err); 218 | }); 219 | }); 220 | 221 | it('should find a record by id', function(done){ 222 | var mytrash = Trash.findById(id); 223 | 224 | mytrash.then(function(res){ 225 | res.should.be.an.object; /* jslint ignore:line */ 226 | done(); 227 | }) 228 | .catch(function(err){ 229 | done(err); 230 | }); 231 | }); 232 | 233 | it('should find a record by id and delete', function(done){ 234 | var mytrash = Trash.findByIdAndRemove(id2); 235 | 236 | mytrash.then(function(res){ 237 | res.should.be.an.object; /* jslint ignore:line */ 238 | done(); 239 | }) 240 | .catch(function(err){ 241 | done(err); 242 | }); 243 | }); 244 | 245 | it('should find a record by id and update', function(done){ 246 | var mytrash = Trash.findByIdAndUpdate(id,{name: 'fufu'}); 247 | 248 | mytrash.then(function(res){ 249 | res.should.be.an.object; /* jslint ignore:line */ 250 | done(); 251 | }) 252 | .catch(function(err){ 253 | done(err); 254 | }); 255 | }); 256 | 257 | it('should find the first match from a query', function(done){ 258 | var mytrash = Trash.findOne({'data.RequestId': 'kgtggokoko456789'}); 259 | 260 | mytrash.then(function(res){ 261 | res.should.be.an.object; /* jslint ignore:line */ 262 | done(); 263 | }) 264 | .catch(function(err){ 265 | done(err); 266 | }); 267 | }); 268 | 269 | it('should find the first match from a query and update', function(done){ 270 | var mytrash = Trash.findOneAndUpdate({'data.RequestId': 'kgtggokoko456789'},{'data.RequestId': 'kgtggohyu0900koko456789'}); 271 | 272 | mytrash.then(function(res){ 273 | res.should.be.an.object; /* jslint ignore:line */ 274 | done(); 275 | }) 276 | .catch(function(err){ 277 | done(err); 278 | }); 279 | }); 280 | 281 | it('should find the first match from a query and delete', function(done){ 282 | var mytrash = Trash.findOneAndRemove({'data.RequestId': 'kgtggohyu0900koko456789'}); 283 | 284 | mytrash.then(function(res){ 285 | res.should.be.an.object; /* jslint ignore:line */ 286 | done(); 287 | }) 288 | .catch(function(err){ 289 | done(err); 290 | }); 291 | }); 292 | 293 | }); 294 | }); 295 | 296 | 297 | // test populate 298 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var eslint = require('gulp-eslint'); 3 | var gulp = require('gulp'); 4 | var nodemon = require('gulp-nodemon'); 5 | var debug = require('debug')('gulp'); 6 | var todo = require('gulp-todo'); 7 | var mocha = require('gulp-mocha'); 8 | var _ = require('lodash'); 9 | 10 | var runSequence = require('run-sequence'); 11 | var conventionalChangelog = require('gulp-conventional-changelog'); 12 | var conventionalGithubReleaser = require('conventional-github-releaser'); 13 | var bump = require('gulp-bump'); 14 | var gutil = require('gulp-util'); 15 | var git = require('gulp-git'); 16 | var fs = require('fs'); 17 | var config = require('./config'); 18 | var argv = require('minimist'); 19 | 20 | 21 | 22 | gulp.task('lint', function() { 23 | return gulp.src(['./*.js', './**/*.js', '!./node_modules/**', '!./node_modules/*.js', '!./template/*.js']) 24 | .pipe(eslint()) 25 | .pipe(eslint.format()) 26 | .pipe(eslint.failAfterError()); 27 | }); 28 | 29 | gulp.task('default', function() { 30 | var stream = nodemon({ script: 'app.js', env: { 'NODE_ENV': 'development', 'DEBUG': 'gulp' }, tasks: ['lint', 'test'] }); 31 | 32 | stream 33 | .on('restart', function() { 34 | debug('restarted!'); 35 | }) 36 | .on('crash', function() { 37 | debug('Application has crashed!\n'); 38 | stream.emit('restart', 10); // restart the server in 10 seconds 39 | }); 40 | }); 41 | 42 | gulp.task('test', function() { 43 | // Override RATE LIMIT HERE FOR UNIT TEST 44 | // process.env.RATE_LIMIT = 10; 45 | process.env.SECURE_MODE = true; 46 | process.env.NO_CACHE = 'no'; 47 | process.env.NODE_ENV = 'test'; 48 | gulp.src('./test', { read: false }) 49 | // `gulp-mocha` needs filepaths so you can't have any plugins before it 50 | .pipe(mocha({ 51 | reporter: 'spec' 52 | })); 53 | }); 54 | 55 | // Remember to pass argument '--name TheServiceName' or '-n TheServiceName' to the service creation command 56 | // If you want to use an API as a database model, pass the base url and the endpoint. '--baseurl http://google.com' or '--b http://google.com' 57 | // '--endpoint users' or '--e users' 58 | // Note that the name must be singular 59 | gulp.task('service', function(done) { 60 | var args = argv(process.argv.slice(2)); 61 | var name; 62 | var baseurl; 63 | var endpoint; 64 | var isSQL; 65 | baseurl = args.baseurl; 66 | endpoint = args.endpoint; 67 | if (!baseurl) { 68 | baseurl = args.b; 69 | } 70 | 71 | if (!endpoint) { 72 | endpoint = args.e; 73 | } 74 | 75 | isSQL = args.sql; 76 | 77 | name = args.name; 78 | 79 | if (!name) { 80 | name = args.n; 81 | } 82 | 83 | if (!name) { 84 | throw new Error('Please, pass the service name using the "-n" argument or "--name" argument'); 85 | } 86 | 87 | var namePlural = _.lowerCase(name) + 's'; 88 | var nameCapitalise = _.capitalize(name); 89 | var nameCapitalisePlural = _.capitalize(name) + 's'; 90 | var nameLowerCase = _.lowerCase(name); 91 | 92 | // Create the Route 93 | fs.readFile('./template/route.tmpl', function(err, data) { 94 | if (err) { 95 | throw err; 96 | } 97 | var tpl = _.template(data); 98 | var result = tpl({ service: nameCapitalise, object: nameLowerCase }); 99 | 100 | fs.writeFile('./routes/' + namePlural + '.js', result, function(err) { 101 | if (err) { 102 | throw err; 103 | } 104 | console.log('Route created at ./routes/' + namePlural + '.js'); 105 | }); 106 | }); 107 | 108 | // Create the Route Unit Test 109 | fs.readFile(isSQL ? './template/route_sql_test.tmpl' : './template/route_test.tmpl', function(err, data) { 110 | if (err) { 111 | throw err; 112 | } 113 | var tpl = _.template(data); 114 | var result = tpl({ service: nameCapitalise, object: nameLowerCase }); 115 | 116 | fs.writeFile('./test/routes/' + namePlural + '.js', result, function(err) { 117 | if (err) { 118 | throw err; 119 | } 120 | console.log('Route unit test created at ./test/routes/' + namePlural + '.js'); 121 | }); 122 | }); 123 | 124 | // Create the Model 125 | if (baseurl && endpoint) { 126 | fs.readFile('./template/model_api.tmpl', function(err, data) { 127 | if (err) { 128 | throw err; 129 | } 130 | var tpl = _.template(data); 131 | var result = tpl({ baseurl: baseurl, endpoint: endpoint }); 132 | 133 | fs.writeFile('./models/' + nameCapitalisePlural + '.js', result, function(err) { 134 | if (err) { 135 | throw err; 136 | } 137 | console.log('Model created at ./models/' + nameCapitalisePlural + '.js'); 138 | }); 139 | }); 140 | } 141 | else { 142 | fs.readFile(isSQL ? './template/model_sql.tmpl' : './template/model.tmpl', function(err, data) { 143 | if (err) { 144 | throw err; 145 | } 146 | var tpl = _.template(data); 147 | var result = tpl({ service: nameCapitalise, object: nameLowerCase }); 148 | 149 | fs.writeFile('./models/' + nameCapitalisePlural + '.js', result, function(err) { 150 | if (err) { 151 | throw err; 152 | } 153 | console.log('Model created at ./models/' + nameCapitalisePlural + '.js'); 154 | }); 155 | }); 156 | } 157 | 158 | // Create the Model Unit Test 159 | fs.readFile(isSQL ? './template/model_sql_test.tmpl' : './template/model_test.tmpl', function(err, data) { 160 | if (err) { 161 | throw err; 162 | } 163 | var tpl = _.template(data); 164 | var result = tpl({ service: nameCapitalise, object: nameLowerCase }); 165 | 166 | fs.writeFile('./test/models/' + namePlural + '.js', result, function(err) { 167 | if (err) { 168 | throw err; 169 | } 170 | console.log('Model unit test created at ./test/models/' + namePlural + '.js'); 171 | }); 172 | }); 173 | 174 | // Create the controller 175 | fs.readFile(isSQL ? './template/controller_sql.tmpl' : './template/controller.tmpl', function(err, data) { 176 | if (err) { 177 | throw err; 178 | } 179 | var tpl = _.template(data); 180 | var result = tpl({ service: nameCapitalise, object: nameLowerCase }); 181 | 182 | fs.writeFile('./controllers/' + nameCapitalisePlural + '.js', result, function(err) { 183 | if (err) { 184 | throw err; 185 | } 186 | console.log('Controller created at ./controllers/' + nameCapitalisePlural + '.js'); 187 | }); 188 | }); 189 | 190 | // Create the controller Unit test 191 | fs.readFile(isSQL ? './template/controller_sql_test.tmpl' : './template/controller_test.tmpl', function(err, data) { 192 | if (err) { 193 | throw err; 194 | } 195 | var tpl = _.template(data); 196 | var result = tpl({ service: nameCapitalise, object: nameLowerCase }); 197 | 198 | fs.writeFile('./test/controllers/' + namePlural + '.js', result, function(err) { 199 | if (err) { 200 | throw err; 201 | } 202 | console.log('Controller unit test created at ./test/controllers/' + namePlural + '.js'); 203 | }); 204 | }); 205 | 206 | return done(); 207 | 208 | }); 209 | 210 | // generate a todo.md from your javascript files 211 | gulp.task('todo', function() { 212 | gulp.src(['./*.js', './**/*.js', '!./node_modules/**', '!./node_modules/*.js']) 213 | .pipe(todo()) 214 | .pipe(gulp.dest('./')); 215 | // -> Will output a TODO.md with your todos 216 | }); 217 | 218 | gulp.task('sanity', gulp.series('lint', 'test', 'todo')); 219 | 220 | // Release 221 | 222 | gulp.task('changelog', function() { 223 | return gulp.src('./CHANGELOG.md', { 224 | buffer: false 225 | }) 226 | .pipe(conventionalChangelog({ 227 | preset: 'angular' // Or to any other commit message convention you use. 228 | })) 229 | .pipe(gulp.dest('./')); 230 | }); 231 | 232 | gulp.task('github-release', function(done) { 233 | conventionalGithubReleaser({ 234 | type: 'oauth', 235 | token: config.gitOAuthToken // change this to your own GitHub token or use an environment variable 236 | }, { 237 | preset: 'angular' // Or to any other commit message convention you use. 238 | }, done); 239 | }); 240 | 241 | // Remember to pass argument '-r patch/minor/major' to the release command 242 | gulp.task('bump-version', function() { 243 | var args = argv(process.argv.slice(2)); 244 | // We hardcode the version change type to 'patch' but it may be a good idea to 245 | // use minimist (https://www.npmjs.com/package/minimist) to determine with a 246 | // command argument whether you are doing a 'major', 'minor' or a 'patch' change. 247 | if (!args.r) { 248 | throw new Error('The release type is not defined! Please pass the -r switch with a release type argument (patch/minor/major)'); 249 | } 250 | else { 251 | return gulp.src(['./package.json']) 252 | .pipe(bump({ type: args.r }).on('error', gutil.log)) 253 | .pipe(gulp.dest('./')); 254 | } 255 | }); 256 | 257 | gulp.task('commit-changes', function() { 258 | return gulp.src('.') 259 | .pipe(git.add()) 260 | .pipe(git.commit('[Prerelease] Bumped version number')); 261 | }); 262 | 263 | gulp.task('push-changes', function(cb) { 264 | git.push('origin', 'dev', cb); 265 | }); 266 | 267 | gulp.task('create-new-tag', function(cb) { 268 | var getPackageJsonVersion = function() { 269 | // We parse the json file instead of using require because require caches 270 | // multiple calls so the version number won't be updated 271 | return JSON.parse(fs.readFileSync('./package.json', 'utf8')).version; 272 | }; 273 | var version = getPackageJsonVersion(); 274 | git.tag(version, 'Created Tag for version: ' + version, function(error) { 275 | if (error) { 276 | return cb(error); 277 | } 278 | git.push('origin', 'dev', { args: '--tags' }, cb); 279 | }); 280 | }); 281 | 282 | gulp.task('release_done', function(cb) { 283 | console.log('RELEASE FINISHED SUCCESSFULLY'); 284 | cb(); 285 | }); 286 | 287 | gulp.task('release', gulp.series( 288 | 'bump-version', 289 | 'changelog', 290 | 'commit-changes', 291 | 'push-changes', 292 | 'create-new-tag', 293 | 'github-release', 294 | 'release_done')); 295 | -------------------------------------------------------------------------------- /template/route_test.tmpl: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai'); 3 | chai.should(); 4 | var config = require('../../config'); 5 | var chaiAsPromised = require("chai-as-promised"); 6 | chai.use(chaiAsPromised); 7 | var request = require('supertest'); 8 | var sinon = require("sinon"); 9 | var sinonChai = require("sinon-chai"); 10 | var mongoose = require('mongoose'); 11 | chai.use(sinonChai); 12 | var _ = require('lodash'); 13 | 14 | var response = require('../../services/response'); 15 | 16 | var express = require('express'); 17 | var app = express(); 18 | var bodyParser = require('body-parser'); 19 | var initRouter = require('../../routes/initialize'); 20 | var routers = require('../../routes'); 21 | 22 | var app = express(); 23 | 24 | app.use(bodyParser.urlencoded({ extended: false })); 25 | app.use(bodyParser.json()); 26 | app.use(response); 27 | app.use(routers); 28 | 29 | 30 | var agent = request.agent(app); 31 | var requestId; 32 | 33 | var res = {}; 34 | var req = {}; 35 | 36 | var nextChecker = false; 37 | var next = function(){ 38 | if(arguments.length > 0){ 39 | console.log(arguments[0]); 40 | }else{ 41 | nextChecker = true; 42 | } 43 | 44 | return nextChecker; 45 | }; 46 | res.json = function(data){ 47 | return res; 48 | }; 49 | 50 | res.badRequest = sinon.spy(); 51 | 52 | res.status = function(status){ 53 | return res; 54 | }; 55 | 56 | var header = {}; 57 | res.set = function(key, value){ 58 | header[key] = value; 59 | return header[key]; 60 | }; 61 | req.get = function(key){ 62 | return header[key]; 63 | }; 64 | 65 | header.set = function(data){ 66 | header.temp = data; 67 | return header.temp; 68 | }; 69 | 70 | req.method = ''; 71 | 72 | var tag; 73 | var objId1 = mongoose.Types.ObjectId('59abab38ead925031a71496d'); 74 | var objId2 = mongoose.Types.ObjectId('59abab38ead925031a71496e'); 75 | var objId3 = mongoose.Types.ObjectId('59abab38ead925031a71496c'); 76 | var last; 77 | var oneId; 78 | var oneId2; 79 | var from = new Date(new Date().setMinutes(new Date().getMinutes() - 3)).toISOString(); 80 | describe('/<%= object %>s', function(){ 81 | 82 | before(function(done){ /* jslint ignore:line */ 83 | var workers = require('../../services/queue/workers'); 84 | var workers2 = require('../../services/queue/workers'); 85 | var workers3 = require('../../services/queue/workers'); 86 | 87 | agent 88 | .get('/initialize') 89 | .then(function(resp){ 90 | 91 | tag = resp.body.data['x-tag']; 92 | done(); 93 | }) 94 | .catch(function(err){ 95 | done(err); 96 | }); 97 | }); 98 | 99 | it('should create a document', function(done){ 100 | agent 101 | .post('/<%= object %>s') 102 | .set('x-tag',tag) 103 | .send({name: 'femi2'}) 104 | .then(function(resp){ 105 | console.log(resp.body.data); 106 | oneId = resp.body.data._id; 107 | done(); 108 | }) 109 | .catch(function(err){ 110 | done(err); 111 | }); 112 | }); 113 | 114 | it('should create documents', function(done){ 115 | agent 116 | .post('/<%= object %>s') 117 | .set('x-tag',tag) 118 | .send([{name: 'femi',toPop: oneId},{name: 'tolu',toPop: oneId},{name: 'femi2',toPop: oneId},{name: 'bola',toPop: oneId}]) 119 | .then(function(resp){ 120 | oneId2 = resp.body.data[0]._id; 121 | resp.body.data.length.should.be.above(0); 122 | done(); 123 | }) 124 | .catch(function(err){ 125 | done(err); 126 | }); 127 | }); 128 | 129 | 130 | describe('Find', function(){ 131 | it('should search for matching documents for a given string', function(done){ 132 | agent 133 | .get('/<%= object %>s?search=femi') 134 | .set('x-tag',tag) 135 | .expect(200, done); 136 | }); 137 | it('should limit the number of returned documents',function(done){ 138 | agent 139 | .get('/<%= object %>s?limit=2') 140 | .set('x-tag',tag) 141 | .then(function(resp){ 142 | resp.body.data.length.should.equal(2); 143 | done(); 144 | }) 145 | .catch(function(err){ 146 | done(err); 147 | }); 148 | }); 149 | it('should contain count of total record for the query', function(done){ 150 | agent 151 | .get('/<%= object %>s') 152 | .set('x-tag',tag) 153 | .then(function(resp){ 154 | resp.body.total.should.exist; /* jslint ignore:line */ 155 | done(); 156 | }) 157 | .catch(function(err){ 158 | done(err); 159 | }); 160 | }); 161 | it('should return the last document Id in the array of documents returned from a query', function(done){ 162 | agent 163 | .get('/<%= object %>s?limit=2') 164 | .set('x-tag',tag) 165 | .then(function(resp){ 166 | console.log('>>>>>>>>>>>>>>>>',resp.body); 167 | last = resp.body.lastId; 168 | resp.body.lastId.should.exist; /* jslint ignore:line */ 169 | done(); 170 | }) 171 | .catch(function(err){ 172 | done(err); 173 | }); 174 | }); 175 | it('should sort documents in ascending order', function(done){ 176 | agent 177 | .get('/<%= object %>s?sort=name') 178 | .set('x-tag',tag) 179 | .expect(200,done); 180 | }); 181 | it('should sort documents in descending order', function(done){ 182 | agent 183 | .get('/<%= object %>s?sort=-name') 184 | .set('x-tag',tag) 185 | .expect(200,done); 186 | }); 187 | it('should select just few parameters from the documents', function(done){ 188 | agent 189 | .get('/<%= object %>s?select=name') 190 | .set('x-tag',tag) 191 | .expect(200,done); 192 | }); 193 | it('should populate data of a reference for multiple data', function(done){ 194 | agent 195 | .get('/<%= object %>s?populate=toPop') 196 | .set('x-tag',tag) 197 | .then(function(resp){ 198 | _.forEach(resp.body.data, function(value){ 199 | if(value._id === oneId2){ 200 | value.toPop.should.be.an('object'); 201 | } 202 | }); 203 | done(); 204 | }) 205 | .catch(function(err){ 206 | done(err); 207 | }); 208 | }); 209 | 210 | it('should populate data of a reference for single data', function(done){ 211 | agent 212 | .get('/<%= object %>s/'+oneId2+'?populate=toPop') 213 | .set('x-tag',tag) 214 | .then(function(resp){ 215 | resp.body.data.toPop.should.be.an('object'); 216 | done(); 217 | }) 218 | .catch(function(err){ 219 | done(err); 220 | }); 221 | }); 222 | 223 | it('should load next page for pagination', function(done){ 224 | agent 225 | .get('/<%= object %>s?lastId='+last+'') 226 | .set('x-tag',tag) 227 | .expect(200,done); 228 | }); 229 | it('should filter by date range', function(done){ 230 | agent 231 | .get('/<%= object %>s?from='+from+'&to='+new Date().toISOString()) 232 | .set('x-tag',tag) 233 | .then(function(resp){ 234 | resp.body.data.length.should.be.above(0); 235 | done(); 236 | }) 237 | .catch(function(err){ 238 | done(err); 239 | }); 240 | }); 241 | it('should filter by date range without setting the end date', function(done){ 242 | agent 243 | .get('/<%= object %>s?from='+from) 244 | .set('x-tag',tag) 245 | .then(function(resp){ 246 | resp.body.data.length.should.be.above(0); 247 | done(); 248 | }) 249 | .catch(function(err){ 250 | done(err); 251 | }); 252 | }); 253 | }); 254 | 255 | it('should find one document', function(done){ 256 | agent 257 | .get('/<%= object %>s/'+oneId+'') 258 | .set('x-tag',tag) 259 | .then(function(resp){ 260 | resp.body.data.should.be.an('object'); 261 | done(); 262 | }) 263 | .catch(function(err){ 264 | done(err); 265 | }); 266 | }); 267 | it('should update documents', function(done){ 268 | agent 269 | .put('/<%= object %>s?name=femi') 270 | .set('x-tag',tag) 271 | .send({name: 'Bukola'}) 272 | .then(function(resp){ 273 | resp.body.data.should.be.an('object'); 274 | done(); 275 | }) 276 | .catch(function(err){ 277 | done(err); 278 | }); 279 | }); 280 | it('should update a document', function(done){ 281 | agent 282 | .patch('/<%= object %>s/'+oneId+'') 283 | .set('x-tag',tag) 284 | .send({name: 'Bukola'}) 285 | .then(function(resp){ 286 | resp.body.data.should.be.an('object'); 287 | done(); 288 | }) 289 | .catch(function(err){ 290 | done(err); 291 | }); 292 | }); 293 | 294 | describe('Delete', function(){ 295 | 296 | it('should delete multiple data', function(done){ 297 | agent 298 | .delete('/<%= object %>s?name=femi2') 299 | .set('x-tag',tag) 300 | .then(function(resp){ 301 | 302 | resp.body.data.should.be.an('array'); 303 | done(); 304 | }) 305 | .catch(function(err){ 306 | done(err); 307 | }); 308 | }); 309 | 310 | it('should delete one data', function(done){ 311 | agent 312 | .delete('/<%= object %>s/'+oneId+'') 313 | .set('x-tag',tag) 314 | .then(function(resp){ 315 | resp.body.data.should.be.an('object'); 316 | done(); 317 | }) 318 | .catch(function(err){ 319 | done(err); 320 | }); 321 | }); 322 | }); 323 | 324 | describe('Restore', function(){ 325 | it('should restore a previously deleted data', function(done){ 326 | var Trash = require('../../models').Trash; 327 | var tid = oneId.toString(); 328 | setTimeout(function(){ 329 | Trash.findOne({'data._id': tid}) 330 | .then(function(resp){ 331 | return agent 332 | .post('/<%= object %>s/'+resp._id+'/restore') 333 | .set('x-tag',tag); 334 | }) 335 | .then(function(resp){ 336 | resp.body.data.should.be.an('object'); 337 | done(); 338 | }) 339 | .catch(function(err){ 340 | done(err); 341 | }); 342 | },5000); 343 | }); 344 | }); 345 | }); 346 | --------------------------------------------------------------------------------