├── .gitignore ├── app ├── validations │ ├── get_birthdates-user.js │ ├── create_birthdates.js │ └── create_user.js ├── configs │ ├── configs.js │ ├── database.js │ └── di.js ├── controllers │ ├── user.js │ └── birthdates.js ├── models │ └── Users.js ├── lib │ ├── error_handler.js │ ├── logger.js │ ├── jsend.js │ ├── service_locator.js │ └── validator.js ├── services │ ├── user.js │ └── birthdates.js └── routes │ └── routes.js ├── .editorconfig ├── .env ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | *.iml 4 | -------------------------------------------------------------------------------- /app/validations/get_birthdates-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const joi = require('joi'); 4 | 5 | module.exports = { 6 | username: joi.string().alphanum().min(4).max(30).required() 7 | }; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /app/validations/create_birthdates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const joi = require('joi'); 4 | 5 | module.exports = joi.object().keys({ 6 | fullname: joi.string().min(5).max(60).required(), 7 | birthdate: joi.date() 8 | }).required(); 9 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_PATH=. 2 | APPLICATION_ENV=development 3 | 4 | APP_NAME=birthdate-api 5 | APP_PORT=5000 6 | LOG_PATH=logs/birthdate-api.log 7 | LOG_ENABLE_CONSOLE=true 8 | 9 | DB_PORT=27017 10 | DB_HOST=localhost 11 | DB_NAME=birthdates 12 | -------------------------------------------------------------------------------- /app/validations/create_user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const joi = require('joi'); 4 | 5 | module.exports = joi.object().keys({ 6 | username: joi.string().alphanum().min(4).max(15).required(), 7 | birthdate: joi.date().required() 8 | }).required(); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "birthdates-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "dotenv": "^5.0.1", 13 | "http-status": "^1.1.0", 14 | "joi": "^13.3.0", 15 | "mongoose": "^5.1.0", 16 | "restify": "^7.1.1", 17 | "restify-errors": "^6.0.0", 18 | "restify-url-semver": "^1.1.1", 19 | "winston": "^3.0.0-rc5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/configs/configs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => ({ 4 | app: { 5 | name: process.env.APP_NAME, 6 | port: process.env.APP_PORT || 8000, 7 | environment: process.env.APPLICATION_ENV, 8 | logpath: process.env.LOG_PATH, 9 | }, 10 | mongo: { 11 | port: process.env.DB_PORT, 12 | host: process.env.DB_HOST, 13 | name: process.env.DB_NAME 14 | }, 15 | application_logging: { 16 | file: process.env.LOG_PATH, 17 | level: process.env.LOG_LEVEL || 'info', 18 | console: process.env.LOG_ENABLE_CONSOLE || true 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /app/controllers/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class UserController { 4 | constructor(log, userService, httpSatus) { 5 | this.log = log; 6 | this.userService = userService; 7 | this.httpSatus = httpSatus; 8 | } 9 | 10 | async create(req, res) { 11 | try { 12 | const { body } = req; 13 | const result = await this.userService.createUser(body); 14 | 15 | res.send(result); 16 | } catch (err) { 17 | this.log.error(err.message); 18 | res.send(err); 19 | } 20 | } 21 | 22 | async get(req, res) { 23 | try{ 24 | const { username } = req.params; 25 | const result = await this.userService.getUser(username); 26 | 27 | res.send(result); 28 | } catch (err) { 29 | this.log.error(err.message); 30 | res.send(err); 31 | } 32 | } 33 | 34 | } 35 | 36 | module.exports = UserController; 37 | -------------------------------------------------------------------------------- /app/models/Users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = require('../configs/configs'); 4 | const serviceLocator = require('../lib/service_locator'); 5 | const mongoose = serviceLocator.get('mongoose'); 6 | 7 | const birthdatesSchema = new mongoose.Schema({ 8 | fullname: { 9 | type: String, 10 | trim: true, 11 | required: true 12 | }, 13 | birthdate: { 14 | type: Date, 15 | required: true 16 | } 17 | }); 18 | 19 | const userSchema = new mongoose.Schema({ 20 | username: { 21 | type: String, 22 | trim: true, 23 | required: true, 24 | unique: true, 25 | lowercase: true 26 | }, 27 | birthdate: { 28 | type: Date, 29 | required: true 30 | }, 31 | birthdates: [birthdatesSchema] 32 | }, 33 | { 34 | timestamps: true 35 | } 36 | ); 37 | 38 | module.exports = mongoose.model('Users', userSchema); 39 | -------------------------------------------------------------------------------- /app/lib/error_handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.register = (server) => { 4 | var httpStatusCodes = require('http-status'); 5 | 6 | server.on('NotFound', (req, res) => { 7 | res.send( 8 | httpStatusCodes.NOT_FOUND, 9 | new Error('Method not Implemented', 'METHOD_NOT_IMPLEMENTED') 10 | ); 11 | }); 12 | 13 | server.on('VersionNotAllowed', (req, res) => { 14 | res.send( 15 | httpStatusCodes.NOT_FOUND, 16 | new Error('Unsupported API version requested', 'INVALID_VERSION') 17 | ); 18 | }); 19 | 20 | server.on('InvalidVersion', (req, res) => { 21 | res.send( 22 | httpStatusCodes.NOT_FOUND, 23 | new Error('Unsupported API version requested', 'INVALID_VERSION') 24 | ); 25 | }); 26 | 27 | server.on('MethodNotAllowed', (req, res) => { 28 | res.send( 29 | httpStatusCodes.METHOD_NOT_ALLOWED, 30 | new Error('Method not Implemented', 'METHOD_NOT_ALLOWED') 31 | ); 32 | }); 33 | 34 | server.on('restifyError', (req, res) => { 35 | res.send(httpStatusCodes.INTERNAL_SERVER_ERROR, err); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /app/lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {createLogger, format, transports} = require('winston'); 4 | const {combine, timestamp, label, prettyPrint} = format; 5 | 6 | const createTransports = function (config) { 7 | const customTransports = []; 8 | 9 | // setup the file transport 10 | if (config.file) { 11 | 12 | // setup the log transport 13 | customTransports.push( 14 | new transports.File({ 15 | filename: config.file, 16 | level: config.level 17 | }) 18 | ); 19 | } 20 | 21 | // if config.console is set to true, a console logger will be included. 22 | if (config.console) { 23 | customTransports.push( 24 | new transports.Console({ 25 | level: config.level 26 | }) 27 | ); 28 | } 29 | 30 | return customTransports; 31 | }; 32 | 33 | module.exports = { 34 | create: function (config) { 35 | return new createLogger({ 36 | transports: createTransports(config), 37 | format: combine( 38 | label({label: 'Birthdates API'}), 39 | timestamp(), 40 | prettyPrint() 41 | ) 42 | }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /app/controllers/birthdates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const serviceLocator = require('../lib/service_locator'); 4 | 5 | class BirthdateController { 6 | constructor(log, birthdateService, httpSatus) { 7 | this.log = log; 8 | this.birthdateService = birthdateService; 9 | this.httpSatus = httpSatus; 10 | } 11 | 12 | async create(req, res) { 13 | try { 14 | const {body} = req; 15 | const {username} = req.params; 16 | const result = await this.birthdateService.createBirthdate( 17 | username, 18 | body 19 | ); 20 | if (result instanceof Error) 21 | res.send(result); 22 | else res.send(`${body.fullname}'s birthdate saved successfully!`) 23 | } catch (err) { 24 | this.log.error(err.message); 25 | res.send(err); 26 | } 27 | } 28 | 29 | async listAll(req, res) { 30 | try { 31 | const {username} = req.params; 32 | const result = await this.birthdateService.getBirthdates(username); 33 | res.send(result); 34 | } catch (err) { 35 | this.log.error(err.message); 36 | res.send(err); 37 | } 38 | } 39 | } 40 | 41 | module.exports = BirthdateController; 42 | -------------------------------------------------------------------------------- /app/configs/database.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const serviceLocator = require('../lib/service_locator'); 4 | const logger = serviceLocator.get('logger'); 5 | 6 | class Database { 7 | constructor(port, host, name) { 8 | this.mongoose = serviceLocator.get('mongoose'); 9 | this._connect(port, host, name); 10 | } 11 | 12 | _connect(port, host, name) { 13 | this.mongoose.Promise = global.Promise; 14 | this.mongoose.connect(`mongodb://${host}:${port}/${name}`); 15 | const {connection} = this.mongoose; 16 | connection.on('connected', () => 17 | logger.info('Database Connection was Successful') 18 | ); 19 | connection.on('error', (err) => 20 | logger.info('Database Connection Failed' + err) 21 | ); 22 | connection.on('disconnected', () => 23 | logger.info('Database Connection Disconnected') 24 | ); 25 | process.on('SIGINT', () => { 26 | connection.close(); 27 | logger.info( 28 | 'Database Connection closed due to NodeJs process termination' 29 | ); 30 | process.exit(0); 31 | }); 32 | 33 | // initialize Model 34 | require('../models/Users'); 35 | } 36 | } 37 | 38 | module.exports = Database; 39 | -------------------------------------------------------------------------------- /app/services/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class UserService { 4 | constructor(log, mongoose, httpStatus, errs) { 5 | this.log = log; 6 | this.mongoose = mongoose; 7 | this.httpStatus = httpStatus; 8 | this.errs = errs; 9 | } 10 | 11 | async createUser(body) { 12 | const Users = this.mongoose.model('Users'); 13 | const {username} = body; 14 | const user = await Users.findOne({username}); 15 | 16 | if (user) { 17 | const err = new this.errs.InvalidArgumentError( 18 | 'User with username already exists' 19 | ); 20 | return err; 21 | } 22 | 23 | let newUser = new Users(body); 24 | newUser.birthdate = new Date(body.birthdate); 25 | newUser = await newUser.save(); 26 | 27 | this.log.info('User Created Successfully'); 28 | return newUser; 29 | } 30 | 31 | async getUser(username) { 32 | const Users = this.mongoose.model('Users'); 33 | const user = await Users.findOne({username}); 34 | 35 | if (!user) { 36 | const err = new this.errs.NotFoundError( 37 | `User with username - ${username} does not exists` 38 | ); 39 | return err; 40 | } 41 | 42 | this.log.info('User fetched Successfully'); 43 | return user; 44 | } 45 | } 46 | 47 | module.exports = UserService; 48 | -------------------------------------------------------------------------------- /app/services/birthdates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class BirthdateService { 4 | constructor(log, mongoose, httpStatus, errs) { 5 | this.log = log; 6 | this.mongoose = mongoose; 7 | this.httpStatus = httpStatus; 8 | this.errs = errs; 9 | } 10 | 11 | async createBirthdate(username, body) { 12 | const Users = this.mongoose.model('Users'); 13 | const user = await Users.findOne({username}); 14 | const {birthdate, fullname} = body; 15 | 16 | if (!user) { 17 | const err = new this.errs.NotFoundError( 18 | `User with username - ${username} does not exists` 19 | ); 20 | return err; 21 | } 22 | 23 | user.birthdates.push({ 24 | birthdate: this.formatBirthdate(birthdate), 25 | fullname 26 | }); 27 | 28 | return user.save(); 29 | } 30 | 31 | formatBirthdate(date) { 32 | return new Date(date); 33 | } 34 | 35 | async getBirthdates(username) { 36 | const Users = this.mongoose.model('Users'); 37 | const user = await Users.findOne({username}); 38 | 39 | if (!user) { 40 | const err = new this.errs.NotFoundError( 41 | `User with username - ${username} does not exists` 42 | ); 43 | return err; 44 | } 45 | 46 | return user.birthdates; 47 | } 48 | } 49 | 50 | module.exports = BirthdateService; 51 | -------------------------------------------------------------------------------- /app/lib/jsend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function formatJSend(req, res, body) { 4 | function formatError(res, body) { 5 | const isClientError = res.statusCode >= 400 && res.statusCode < 500; 6 | if (isClientError) { 7 | return { 8 | status: 'error', 9 | message: body.message, 10 | code: body.code 11 | }; 12 | } else { 13 | const inDebugMode = process.env.NODE_ENV === 'development'; 14 | 15 | return { 16 | status: 'error', 17 | message: inDebugMode ? body.message : 'Internal Server Error', 18 | code: inDebugMode ? body.code : 'INTERNAL_SERVER_ERROR', 19 | data: inDebugMode ? body.stack : undefined 20 | }; 21 | } 22 | } 23 | 24 | function formatSuccess(res, body) { 25 | if (body.data && body.pagination) { 26 | return { 27 | status: 'success', 28 | data: body.data, 29 | pagination: body.pagination, 30 | }; 31 | } 32 | 33 | return { 34 | status: 'success', 35 | data: body 36 | }; 37 | } 38 | 39 | let response; 40 | if (body instanceof Error) { 41 | response = formatError(res, body); 42 | } else { 43 | response = formatSuccess(res, body); 44 | } 45 | 46 | response = JSON.stringify(response); 47 | res.header('Content-Length', Buffer.byteLength(response)); 48 | res.header('Content-Type', 'application/json'); 49 | 50 | return response; 51 | } 52 | 53 | module.exports = formatJSend; 54 | -------------------------------------------------------------------------------- /app/lib/service_locator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ServiceLocator() { 4 | this.dependencyMap = {}; 5 | this.dependencyCache = {}; 6 | } 7 | 8 | ServiceLocator.prototype.register = function (dependencyName, constructor) { 9 | if (typeof constructor !== 'function') { 10 | throw new Error(dependencyName + ': Dependency constructor is not a function'); 11 | } 12 | 13 | if (!dependencyName) { 14 | throw new Error('Invalid depdendency name provided'); 15 | } 16 | 17 | this.dependencyMap[dependencyName] = constructor; 18 | }; 19 | 20 | ServiceLocator.prototype.get = function (dependencyName) { 21 | if (this.dependencyMap[dependencyName] === undefined) { 22 | throw new Error(dependencyName + ': Attempting to retrieve unknown dependency'); 23 | } 24 | 25 | if (typeof this.dependencyMap[dependencyName] !== 'function') { 26 | throw new Error(dependencyName + ': Dependency constructor is not a function'); 27 | } 28 | 29 | if (this.dependencyCache[dependencyName] === undefined) { 30 | const dependencyConstructor = this.dependencyMap[dependencyName]; 31 | const dependency = dependencyConstructor(this); 32 | if (dependency) { 33 | this.dependencyCache[dependencyName] = dependency; 34 | } 35 | } 36 | 37 | return this.dependencyCache[dependencyName]; 38 | }; 39 | 40 | ServiceLocator.prototype.clear = function () { 41 | this.dependencyCache = {}; 42 | this.dependencyMap = {}; 43 | }; 44 | 45 | module.exports = new ServiceLocator(); 46 | -------------------------------------------------------------------------------- /app/routes/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.register = (server, serviceLocator) => { 4 | 5 | server.post( 6 | { 7 | path: '/users', 8 | name: 'Create User', 9 | version: '1.0.0', 10 | validation: { 11 | body: require('../validations/create_user') 12 | } 13 | }, 14 | (req, res, next) => 15 | serviceLocator.get('userController').create(req, res, next) 16 | ); 17 | 18 | server.get( 19 | { 20 | path: '/users/:username', 21 | name: 'Get User', 22 | version: '1.0.0', 23 | validation: { 24 | params: require('../validations/get_birthdates-user.js') 25 | } 26 | }, 27 | (req, res, next) => 28 | serviceLocator.get('userController').get(req, res, next) 29 | ); 30 | 31 | server.get( 32 | { 33 | path: '/birthdates/:username', 34 | name: 'Get Birthdates', 35 | version: '1.0.0', 36 | validation: { 37 | params: require('../validations/get_birthdates-user.js') 38 | } 39 | }, 40 | (req, res, next) => 41 | serviceLocator.get('birthdateController').listAll(req, res, next) 42 | ); 43 | 44 | server.post( 45 | { 46 | path: '/birthdates/:username', 47 | name: 'Create Birthdate', 48 | version: '1.0.0', 49 | validation: { 50 | body: require('../validations/create_birthdates') 51 | } 52 | }, 53 | (req, res, next) => 54 | serviceLocator.get('birthdateController').create(req, res, next) 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').config(); 4 | const config = require('./app/configs/configs')(); 5 | const restify = require('restify'); 6 | const versioning = require('restify-url-semver'); 7 | const joi = require('joi'); 8 | 9 | // Require DI 10 | const serviceLocator = require('./app/configs/di'); 11 | const validator = require('./app/lib/validator'); 12 | const handler = require('./app/lib/error_handler'); 13 | const routes = require('./app/routes/routes'); 14 | const logger = serviceLocator.get('logger'); 15 | const server = restify.createServer({ 16 | name: config.app.name, 17 | versions: ['1.0.0'], 18 | formatters: { 19 | 'application/json': require('./app/lib/jsend') 20 | } 21 | }); 22 | 23 | // Initialize the database 24 | const Database = require('./app/configs/database'); 25 | new Database(config.mongo.port, config.mongo.host, config.mongo.name); 26 | 27 | // Set API versioning and allow trailing slashes 28 | server.pre(restify.pre.sanitizePath()); 29 | server.pre(versioning({ prefix: '/' })); 30 | 31 | // Set request handling and parsing 32 | server.use(restify.plugins.acceptParser(server.acceptable)); 33 | server.use(restify.plugins.queryParser()); 34 | server.use( 35 | restify.plugins.bodyParser({ 36 | mapParams: false 37 | }) 38 | ); 39 | 40 | // initialize validator for all requests 41 | server.use(validator.paramValidation(logger, joi)); 42 | 43 | // Setup Error Event Handling 44 | handler.register(server); 45 | 46 | // Setup route Handling 47 | routes.register(server, serviceLocator); 48 | 49 | // start server 50 | server.listen(config.app.port, () => { 51 | console.log(`${config.app.name} Server is running on port - 52 | ${config.app.port}`); 53 | }); 54 | -------------------------------------------------------------------------------- /app/lib/validator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let httpStatus = require('http-status'); 4 | let errors = require('restify-errors'); 5 | 6 | module.exports.paramValidation = function (log, joi) { 7 | 8 | return function (req, res, next) { 9 | 10 | // always allow validation to allow unknown fields by default. 11 | let options = { 12 | allowUnknown: true 13 | }; 14 | 15 | let validation = req.route.spec.validation; //validation object in route 16 | if (!validation) { 17 | return next(); // skip validation if not set 18 | } 19 | 20 | let validProperties = ['body', 'query', 'params']; 21 | 22 | for (let i in validation) { 23 | if (validProperties.indexOf(i) < 0) { 24 | log.debug('Route contains unsupported validation key'); 25 | throw new Error('An unsupported validation key was set in route'); 26 | 27 | } else { 28 | if (req[i] === undefined) { 29 | log.debug('Empty request ' + i + ' was sent'); 30 | 31 | res.send( 32 | httpStatus.BAD_REQUEST, 33 | new errors.InvalidArgumentError('Missing request ' + i) 34 | ); 35 | return; 36 | } 37 | 38 | let result = joi.validate(req[i], validation[i], options); 39 | 40 | if (result.error) { 41 | log.debug('validation error - %s', result.error.message); 42 | 43 | res.send( 44 | httpStatus.BAD_REQUEST, 45 | new errors.InvalidArgumentError( 46 | 'Invalid request ' + i + ' - ' + result.error.details[0].message 47 | ) 48 | ); 49 | return; 50 | 51 | } else { 52 | log.info('successfully validated request parameters'); 53 | } 54 | } 55 | } 56 | 57 | next(); 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /app/configs/di.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const serviceLocator = require('../lib/service_locator'); 4 | const config = require('./configs')(); 5 | 6 | serviceLocator.register('logger', () => { 7 | return require('../lib/logger').create(config.application_logging); 8 | }); 9 | 10 | serviceLocator.register('httpStatus', () => { 11 | return require('http-status'); 12 | }); 13 | 14 | serviceLocator.register('mongoose', () => { 15 | return require('mongoose'); 16 | }); 17 | 18 | serviceLocator.register('errs', () => { 19 | return require('restify-errors'); 20 | }); 21 | 22 | serviceLocator.register('birthdateService', (serviceLocator) => { 23 | const log = serviceLocator.get('logger'); 24 | const mongoose = serviceLocator.get('mongoose'); 25 | const httpStatus = serviceLocator.get('httpStatus'); 26 | const errs = serviceLocator.get('errs'); 27 | const BirthdateService = require('../services/birthdates'); 28 | 29 | return new BirthdateService(log, mongoose, httpStatus, errs); 30 | }); 31 | 32 | serviceLocator.register('userService', (serviceLocator) => { 33 | const log = serviceLocator.get('logger'); 34 | const mongoose = serviceLocator.get('mongoose'); 35 | const httpStatus = serviceLocator.get('httpStatus'); 36 | const errs = serviceLocator.get('errs'); 37 | const UserService = require('../services/user'); 38 | 39 | return new UserService(log, mongoose, httpStatus, errs); 40 | }); 41 | 42 | serviceLocator.register('birthdateController', (serviceLocator) => { 43 | const log = serviceLocator.get('logger'); 44 | const httpStatus = serviceLocator.get('httpStatus'); 45 | const birthdateService = serviceLocator.get('birthdateService'); 46 | const BirthdateController = require('../controllers/birthdates'); 47 | 48 | return new BirthdateController(log, birthdateService, httpStatus); 49 | }); 50 | 51 | serviceLocator.register('userController', (serviceLocator) => { 52 | const log = serviceLocator.get('logger'); 53 | const httpStatus = serviceLocator.get('httpStatus'); 54 | const userService = serviceLocator.get('userService'); 55 | const UserController = require('../controllers/user'); 56 | 57 | return new UserController(log, userService, httpStatus); 58 | }); 59 | 60 | module.exports = serviceLocator; 61 | --------------------------------------------------------------------------------