├── .gitignore ├── common └── helpers.js ├── config.js ├── controllers ├── EmployeeController.js └── ProjectController.js ├── index.js ├── models ├── BaseModel.js ├── Employee.js └── Project.js ├── package-lock.json ├── package.json ├── readme.txt ├── router.js └── routes.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ -------------------------------------------------------------------------------- /common/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports.validationError = (res, error = 'Data provided is not valid') => { 2 | addHeaders(res); 3 | 4 | res.statusCode = 422; 5 | 6 | res.end(JSON.stringify({ 7 | status: 'fail', 8 | error 9 | }, null, 3)); 10 | }; 11 | 12 | module.exports.error = (res, error = 'An unknown error occurred', statusCode = 500) => { 13 | addHeaders(res); 14 | 15 | res.statusCode = statusCode; 16 | 17 | res.end(JSON.stringify({ 18 | status: 'fail', 19 | error 20 | }, null, 3)); 21 | }; 22 | 23 | module.exports.success = (res, data = null) => { 24 | addHeaders(res); 25 | 26 | res.statusCode = 200; 27 | 28 | res.end(JSON.stringify({ 29 | status: 'success', 30 | data 31 | }, null, 3)); 32 | }; 33 | 34 | const addHeaders = (res) => { 35 | return res.setHeader('Content-Type', 'application/json'); 36 | } -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 2 | // Set mongodb connection string here 3 | module.exports.MONGO_CONNECTION_STRING = 'mongodb://localhost:27017/wizni'; -------------------------------------------------------------------------------- /controllers/EmployeeController.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Employee = require('./../models/Employee'); 3 | const Project = require('./../models/Project'); 4 | const helpers = require('./../common/helpers'); 5 | 6 | class EmployeeController { 7 | // GET /employee 8 | async index (req, res) { 9 | try { 10 | const selectParams = { 11 | _id: 1, 12 | name: 1, 13 | email: 1 14 | }; 15 | 16 | const employees = await Employee.getAll({}, selectParams); 17 | 18 | return helpers.success(res, employees); 19 | } 20 | catch (error) { 21 | return helpers.error(res, error); 22 | } 23 | } 24 | 25 | // POST /employee 26 | async create (req, res, param, postData) { 27 | postData = JSON.parse(postData); 28 | let { name, email, isManager = false, managerId = null, peers = [] } = postData; 29 | 30 | try { 31 | let manageExists = await this.validateManager(managerId); 32 | 33 | if (!manageExists) { 34 | return helpers.validationError(res, 'managerId is invalid'); 35 | } 36 | 37 | if (managerId !== null) { 38 | managerId = mongoose.Types.ObjectId(managerId); 39 | } 40 | 41 | if (! (peers instanceof Array)) { 42 | peers = [peers]; 43 | } 44 | 45 | let peersExists = await this.validatePeers(peers, isManager); 46 | 47 | if (!peersExists) { 48 | return helpers.validationError(res, 'Peer(s) is invalid'); 49 | } 50 | 51 | if (peers.length > 0) { 52 | peers = peers.map((el) => { return mongoose.Types.ObjectId(el); }); 53 | } 54 | 55 | const employee = await Employee.create({ name, email, isManager, managerId, peers }); 56 | 57 | // set managerId of all peers 58 | if (employee.peers.length > 0) { 59 | const update = {$set: {managerId: mongoose.Types.ObjectId(employee._id)}}; 60 | await Employee.update({_id: {$in: employee.peers}}, update, {multi: true}); 61 | } 62 | 63 | return helpers.success(res, employee.toClient()); 64 | } 65 | catch (error) { 66 | if (error.name === 'ValidationError') { 67 | return helpers.validationError(res, error); 68 | } 69 | else if (error.message.indexOf('duplicate key error') !== -1) { 70 | return helpers.validationError(res, 'Email already exists'); 71 | } 72 | else { 73 | return helpers.error(res); 74 | } 75 | } 76 | } 77 | 78 | // GET /employee/:id 79 | async show (req, res, param) { 80 | try { 81 | const pipeline = [ 82 | { 83 | "$match" : { 84 | "_id" : mongoose.Types.ObjectId(param) 85 | } 86 | }, 87 | { 88 | "$project" : { 89 | "_id" : 1, 90 | "isManager" : 1, 91 | "name" : 1, 92 | "email" : 1, 93 | "managerId" : 1 94 | } 95 | }, 96 | { 97 | "$lookup" : { 98 | "from" : "employees", 99 | "localField" : "managerId", 100 | "foreignField" : "_id", 101 | "as" : "manager" 102 | } 103 | }, 104 | { 105 | "$project" : { 106 | "manager" : { 107 | "isManager" : 0, 108 | "peers" : 0, 109 | "managerid" : 0, 110 | "createdAt" : 0, 111 | "updatedAt" : 0, 112 | "__v" : 0 113 | }, 114 | "managerId" : 0 115 | } 116 | }, 117 | { 118 | "$lookup" : { 119 | "from" : "projects", 120 | "localField" : "_id", 121 | "foreignField" : "employeeIds", 122 | "as" : "projects" 123 | } 124 | }, 125 | { 126 | "$project" : { 127 | "projects" : { 128 | "employeeIds" : 0, 129 | "managerId" : 0, 130 | "createdAt" : 0, 131 | "updatedAt" : 0, 132 | "__v" : 0 133 | } 134 | } 135 | } 136 | ]; 137 | 138 | const employee = await Employee.aggregation(pipeline); 139 | 140 | return helpers.success(res, employee); 141 | } 142 | catch (error) { 143 | return helpers.error(res, error); 144 | } 145 | } 146 | 147 | // PUT /employee/:id 148 | async update (req, res, param, postData) { 149 | let employee; 150 | 151 | try { 152 | employee = await Employee.get({ _id: param }, { isManager: 1 }); 153 | } 154 | catch (e) { 155 | console.log(e); 156 | } 157 | 158 | if (!employee) { 159 | return helpers.error(res, 'Entity not found', 404); 160 | } 161 | 162 | postData = JSON.parse(postData); 163 | 164 | let updateData = { 165 | isManager: employee.isManager 166 | }; 167 | 168 | if (postData.name) { 169 | updateData.name = postData.name; 170 | } 171 | 172 | if (postData.email) { 173 | updateData.email = postData.email; 174 | } 175 | 176 | if (postData.isManager) { 177 | updateData.isManager = true; 178 | } 179 | 180 | let { managerId = null, peers = null } = postData; 181 | 182 | try { 183 | let manageExists = await this.validateManager(managerId); 184 | 185 | if (!manageExists) { 186 | return helpers.validationError(res, 'managerId is invalid'); 187 | } 188 | 189 | if (managerId !== null) { 190 | updateData.managerId = mongoose.Types.ObjectId(managerId); 191 | } 192 | 193 | if (peers !== null && ! (peers instanceof Array)) { 194 | peers = [peers]; 195 | } 196 | 197 | let peersExists = await this.validatePeers(peers, updateData.isManager); 198 | 199 | if (!peersExists) { 200 | return helpers.validationError(res, 'Peer(s) is invalid'); 201 | } 202 | 203 | if (peers && peers.length > 0) { 204 | peers = peers.map((el) => { return mongoose.Types.ObjectId(el); }); 205 | } 206 | 207 | if (peers !== null) { 208 | updateData.peers = peers; 209 | } 210 | 211 | const employee = await Employee.findOneAndUpdate({ _id: param }, { $set: updateData }, { new: true }); 212 | 213 | // set managerId of all peers 214 | if (employee.peers.length > 0) { 215 | const update = {$set: {managerId: mongoose.Types.ObjectId(employee._id)}}; 216 | await Employee.update({ _id: {$in: employee.peers} }, update, { multi: true }); 217 | } 218 | 219 | return helpers.success(res, employee.toClient()); 220 | } 221 | catch (error) { 222 | console.log(error); 223 | 224 | if (error.name === 'ValidationError') { 225 | return helpers.validationError(res, error); 226 | } 227 | else if (error.message.indexOf('duplicate key error') !== -1) { 228 | return helpers.validationError(res, 'Email already exists'); 229 | } 230 | else { 231 | return helpers.error(res); 232 | } 233 | } 234 | } 235 | 236 | // DELETE /employee/:id 237 | async delete (req, res, param) { 238 | let employee; 239 | try { 240 | employee = await Employee.get({ _id: param }, { isManager: 1 }); 241 | } 242 | catch (e) { 243 | console.log(e); 244 | } 245 | 246 | if (!employee) { 247 | return helpers.error(res, 'Entity not found', 404); 248 | } 249 | 250 | try { 251 | let update, conditions; 252 | 253 | // delete employee from project 254 | try { 255 | update = { $pull: { employeeIds: mongoose.Types.ObjectId(param) } }; 256 | await Project.update({}, update, {multi: true}); 257 | } 258 | catch (e) { 259 | console.log('Error in delete employee from project', e); 260 | } 261 | 262 | // delete managerId from project 263 | try { 264 | update = { $set: { managerId: null } }; 265 | await Project.update({managerId: mongoose.Types.ObjectId(param)}, update, {multi: true}); 266 | } 267 | catch (e) { 268 | console.log('Error in delete employee from project', e); 269 | } 270 | 271 | // delete peers 272 | try { 273 | update = { $pull: { peers: mongoose.Types.ObjectId(param) } }; 274 | await Employee.update({}, update, {multi: true}); 275 | } 276 | catch (e) { 277 | console.log('delete peers', e); 278 | } 279 | 280 | // set manager to null 281 | try { 282 | conditions = {managerId: mongoose.Types.ObjectId(param)}; 283 | update = { $set: { managerId: null } }; 284 | await Employee.update(conditions, update, {multi: true}); 285 | } 286 | catch (e) { 287 | console.log('set manager to null', e); 288 | } 289 | 290 | conditions = { _id: param }; 291 | await Employee.remove(conditions); 292 | 293 | return helpers.success(res); 294 | } 295 | catch (error) { 296 | return helpers.error(res, error); 297 | } 298 | } 299 | 300 | // Checks if a manager with given id exists 301 | async validateManager (managerId) { 302 | if (managerId === null) { 303 | return true; 304 | } 305 | 306 | try { 307 | const managerExists = await Employee.get({ _id: managerId, isManager: true }); 308 | return !!(managerExists); 309 | } 310 | catch (e) { 311 | return false; 312 | } 313 | } 314 | 315 | // Checks if all the peers exist in database 316 | async validatePeers (peers, isManager) { 317 | if (peers === null) { 318 | return true; 319 | } 320 | 321 | if (peers.length && !isManager) { 322 | return false; 323 | } 324 | 325 | try { 326 | const peersExists = await Employee.getAll({ _id: {$in: peers} }, { _id: 1 }); 327 | return (peersExists.length === peers.length) ; 328 | } 329 | catch (e) { 330 | return false; 331 | } 332 | } 333 | } 334 | 335 | module.exports = new EmployeeController(); 336 | -------------------------------------------------------------------------------- /controllers/ProjectController.js: -------------------------------------------------------------------------------- 1 | const Project = require('./../models/Project'); 2 | const helpers = require('./../common/helpers'); 3 | const Employee = require('./../models/Employee'); 4 | 5 | const mongoose = require('mongoose'); 6 | 7 | class ProjectController { 8 | // GET /project 9 | async index (req, res) { 10 | try { 11 | const selectParams = { 12 | _id: 1, 13 | name: 1, 14 | managerId: 1, 15 | employeeIds: 1 16 | }; 17 | 18 | const projects = await Project.getAll({}, selectParams); 19 | 20 | return helpers.success(res, projects); 21 | } 22 | catch (error) { 23 | return helpers.error(res, error); 24 | } 25 | } 26 | 27 | // POST /project 28 | async create (req, res, param, postData) { 29 | postData = JSON.parse(postData); 30 | 31 | let { name, employeeIds = [], managerId = null } = postData; 32 | 33 | if (! (employeeIds instanceof Array)) { 34 | employeeIds = [employeeIds]; 35 | } 36 | 37 | try { 38 | let manageExists = await this.validateManager(managerId); 39 | 40 | if (!manageExists) { 41 | return helpers.validationError(res, 'Manager is invalid'); 42 | } 43 | 44 | let employeesExists = await this.validateEmployees(employeeIds); 45 | 46 | if (!employeesExists) { 47 | return helpers.validationError(res, 'Employee(s) is invalid'); 48 | } 49 | 50 | employeeIds = employeeIds.map(element => { return mongoose.Types.ObjectId(element) }); 51 | 52 | if (managerId !== null) { 53 | managerId = mongoose.Types.ObjectId(managerId); 54 | } 55 | 56 | const project = await Project.create({ name, employeeIds, managerId }); 57 | 58 | return helpers.success(res, project); 59 | } 60 | catch (error) { 61 | if (error.name === 'ValidationError') { 62 | return helpers.validationError(res, error); 63 | } 64 | else { 65 | return helpers.error(res); 66 | } 67 | } 68 | } 69 | 70 | // GET /project/:id 71 | async show (req, res, param) { 72 | try { 73 | const aggPipeline = [ 74 | { 75 | "$match" : { 76 | "_id" : mongoose.Types.ObjectId(param) 77 | } 78 | }, 79 | { 80 | "$lookup" : { 81 | "from" : "employees", 82 | "localField" : "managerId", 83 | "foreignField" : "_id", 84 | "as" : "manager" 85 | } 86 | }, 87 | { 88 | "$lookup" : { 89 | "from" : "employees", 90 | "localField" : "employeeIds", 91 | "foreignField" : "_id", 92 | "as" : "employees" 93 | } 94 | }, 95 | { 96 | "$project" : { 97 | "_id" : 1, 98 | "name" : 1, 99 | "manager" : { 100 | "_id" : 1, 101 | "name" : 1 102 | }, 103 | "employees" : { 104 | "_id" : 1, 105 | "name" : 1 106 | } 107 | } 108 | } 109 | ]; 110 | 111 | const project = await Project.aggregation(aggPipeline); 112 | 113 | return helpers.success(res, project); 114 | } 115 | catch (error) { 116 | return helpers.error(res, error); 117 | } 118 | } 119 | 120 | // PUT /project/:id 121 | async update (req, res, param, postData) { 122 | param = mongoose.Types.ObjectId(param); 123 | 124 | let project; 125 | try { 126 | project = await Project.get({ _id: param }, { _id: 1 }); 127 | } 128 | catch (e) { 129 | console.log(e); 130 | } 131 | 132 | if (!project) { 133 | return helpers.error(res, 'Entity not found', 404); 134 | } 135 | 136 | let updateData = {}; 137 | postData = JSON.parse(postData); 138 | 139 | if (postData.name) { 140 | updateData.name = postData.name; 141 | } 142 | 143 | let { managerId = null, employeeIds = [] } = postData; 144 | 145 | if (! (employeeIds instanceof Array)) { 146 | employeeIds = [employeeIds]; 147 | } 148 | 149 | try { 150 | let manageExists = await this.validateManager(managerId); 151 | 152 | if (!manageExists) { 153 | return helpers.validationError(res, 'managerId is invalid'); 154 | } 155 | 156 | if (managerId !== null) { 157 | updateData.managerId = mongoose.Types.ObjectId(managerId); 158 | } 159 | 160 | let employeesExists = await this.validateEmployees(employeeIds); 161 | 162 | if (!employeesExists) { 163 | return helpers.validationError(res, 'EmployeeIds is invalid'); 164 | } 165 | 166 | employeeIds = employeeIds.map(element => { return mongoose.Types.ObjectId(element) }); 167 | 168 | if (employeeIds.length > 0) { 169 | updateData.employeeIds = employeeIds; 170 | } 171 | 172 | const options = { 173 | fields: { 174 | name: 1, 175 | employeeIds: 1, 176 | managerId: 1 177 | }, 178 | new: true 179 | }; 180 | 181 | const project = await Project.findOneAndUpdate({ _id: param }, {$set: updateData}, options); 182 | 183 | return helpers.success(res, project); 184 | } 185 | catch (error) { 186 | if (error.name === 'ValidationError') { 187 | return helpers.validationError(res, error); 188 | } 189 | else { 190 | console.log(error); 191 | return helpers.error(res); 192 | } 193 | } 194 | } 195 | 196 | // DELETE /employee/:id 197 | async delete (req, res, param) { 198 | param = mongoose.Types.ObjectId(param); 199 | 200 | let project; 201 | try { 202 | project = await Project.get({ _id: param }, { _id: 1 }); 203 | } 204 | catch (e) { 205 | console.log(e); 206 | } 207 | 208 | if (!project) { 209 | return helpers.error(res, 'Entity not found', 404); 210 | } 211 | 212 | try { 213 | let conditions = { _id: param }; 214 | 215 | await Project.remove(conditions); 216 | 217 | return helpers.success(res); 218 | } 219 | catch (error) { 220 | return helpers.error(res, error); 221 | } 222 | } 223 | 224 | // Checks if a manager with given id exists 225 | async validateManager (managerId) { 226 | if (managerId === null) { 227 | return true; 228 | } 229 | 230 | try { 231 | const managerExists = await Employee.get({ _id: managerId, isManager: true }); 232 | return !!(managerExists); 233 | } 234 | catch (e) { 235 | return false; 236 | } 237 | } 238 | 239 | // Checks if all the peers exist in database 240 | async validateEmployees (employeeIds) { 241 | if (! (employeeIds instanceof Array)) { 242 | employeeIds = [employeeIds]; 243 | } 244 | 245 | if (employeeIds.length === 0) { 246 | return true; 247 | } 248 | 249 | try { 250 | const employeesExists = await Employee.getAll({ _id: {$in: employeeIds} }, {_id: 1}); 251 | return (employeesExists.length === employeeIds.length) ; 252 | } 253 | catch (e) { 254 | return false; 255 | } 256 | } 257 | } 258 | 259 | module.exports = new ProjectController(); 260 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const routes = require('./routes'); 4 | const router = require('./router'); 5 | 6 | process.on('uncaughtException', function(err) { 7 | // handle the error safely 8 | console.log('uncaughtException'); 9 | console.error(err.stack); 10 | console.log(err); 11 | }); 12 | 13 | 14 | const server = http.createServer(async (req, res) => { 15 | await router(req, res, routes); 16 | }); 17 | 18 | server.listen(3000, () => { 19 | console.log('Server is listening on port 3000'); 20 | }); -------------------------------------------------------------------------------- /models/BaseModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const config = require('../config.js'); 3 | 4 | const connectionInstance = mongoose.createConnection(config.MONGO_CONNECTION_STRING); 5 | 6 | connectionInstance.on('error', (err) => { 7 | if (err) { 8 | throw err; 9 | } 10 | }); 11 | 12 | connectionInstance.once('open', () => { 13 | console.log(`MongoDb connected successfully, date is = ${new Date()}`); 14 | }); 15 | 16 | module.exports = connectionInstance; 17 | 18 | const logDebug = true; 19 | 20 | mongoose.set('debug', logDebug); -------------------------------------------------------------------------------- /models/Employee.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const BaseModel = require('./BaseModel'); 5 | 6 | const employeeSchema = new Schema({ 7 | // Name of the employee 8 | name: { 9 | type: String, 10 | required: true 11 | }, 12 | 13 | // Email of the employee 14 | email: { 15 | type: String, 16 | validate: { 17 | validator: function(v) { 18 | return /^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/.test(v); 19 | }, 20 | message: '{VALUE} is not a valid email!' 21 | }, 22 | required: true, 23 | unique: true 24 | }, 25 | 26 | // Indicates if the employee is a manager. We are managing 27 | // employees and managers in same collection 28 | isManager: { 29 | type: Boolean, 30 | default: false 31 | }, 32 | 33 | // List of employees who have this employee as manager 34 | peers: { 35 | type: Array, 36 | default: [] 37 | }, 38 | 39 | // Employee's manager 40 | managerId: { 41 | type: Schema.ObjectId 42 | } 43 | }, { timestamps: true }); 44 | 45 | employeeSchema.method('toClient', function () { 46 | const employee = this.toObject(); 47 | 48 | delete employee.__v; 49 | delete employee.deletedAt; 50 | delete employee.createdAt; 51 | delete employee.updatedAt; 52 | 53 | return employee; 54 | }); 55 | 56 | 57 | const employeeModel = BaseModel.model('employees', employeeSchema); 58 | 59 | class Employee { 60 | static create (data) { 61 | const newEmployee = employeeModel(data); 62 | 63 | return new Promise((resolve, reject) => { 64 | const error = newEmployee.validateSync(); 65 | if (error) { 66 | reject(error); 67 | } 68 | 69 | newEmployee.save((err, obj) => { 70 | if (obj) { 71 | resolve(obj); 72 | } 73 | else { 74 | reject(err); 75 | } 76 | }); 77 | }); 78 | } 79 | 80 | static getAll (conditions, selectParams) { 81 | return new Promise((resolve, reject) => { 82 | const query = employeeModel.find(conditions); 83 | 84 | if (selectParams) { 85 | query.select(selectParams); 86 | } 87 | 88 | query.lean().exec((err, docs) => { 89 | if (docs) { 90 | resolve(docs); 91 | } 92 | else { 93 | reject(err); 94 | } 95 | }); 96 | }); 97 | } 98 | 99 | static get (conditions, selectParams) { 100 | return new Promise((resolve, reject) => { 101 | const query = employeeModel.findOne(conditions); 102 | 103 | if (selectParams) { 104 | query.select(selectParams); 105 | } 106 | 107 | query.lean().exec((err, docs) => { 108 | if (docs) { 109 | resolve(docs); 110 | } 111 | else { 112 | reject(err); 113 | } 114 | }); 115 | }); 116 | } 117 | 118 | static remove (conditions) { 119 | return new Promise((resolve, reject) => { 120 | employeeModel.remove(conditions, (err, docs) => { 121 | if (docs) { 122 | resolve(docs); 123 | } 124 | else { 125 | reject(err); 126 | } 127 | }); 128 | }); 129 | } 130 | 131 | static findOneAndUpdate (conditions, updateData, options) { 132 | return new Promise((resolve, reject) => { 133 | employeeModel.findOneAndUpdate(conditions, updateData, options, (err, docs) => { 134 | if (docs) { 135 | resolve(docs); 136 | } 137 | else { 138 | reject(err); 139 | } 140 | }); 141 | }); 142 | } 143 | 144 | static update (conditions, updateData, options) { 145 | return new Promise((resolve, reject) => { 146 | employeeModel.update(conditions, updateData, options, (err, docs) => { 147 | if (docs) { 148 | resolve(docs); 149 | } 150 | else { 151 | reject(err); 152 | } 153 | }); 154 | }); 155 | } 156 | 157 | static aggregation (pipeline) { 158 | return new Promise((resolve, reject) => { 159 | employeeModel.aggregate(pipeline, (err, docs) => { 160 | if (err) { 161 | reject(err); 162 | } 163 | else { 164 | resolve(docs); 165 | } 166 | }); 167 | }); 168 | } 169 | } 170 | 171 | 172 | module.exports = Employee; -------------------------------------------------------------------------------- /models/Project.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const BaseModel = require('./BaseModel'); 5 | 6 | const projectSchema = new Schema({ 7 | // Name of the project 8 | name: { 9 | type: String, required: true 10 | }, 11 | 12 | // List of employees who are working on project 13 | employeeIds: { 14 | type: Array, default: [] 15 | }, 16 | 17 | // Project manager 18 | managerId: { 19 | type: Schema.ObjectId 20 | } 21 | }, { timestamps: true }); 22 | 23 | 24 | const projectModel = BaseModel.model('projects', projectSchema); 25 | 26 | class Project { 27 | static create (data) { 28 | const newProject = projectModel(data); 29 | 30 | return new Promise((resolve, reject) => { 31 | const error = newProject.validateSync(); 32 | if (error) { 33 | reject(error); 34 | } 35 | 36 | newProject.save((err, obj) => { 37 | if (obj) { 38 | resolve(obj); 39 | } 40 | else { 41 | reject(err); 42 | } 43 | }); 44 | }); 45 | } 46 | 47 | static getAll (conditions, selectParams) { 48 | return new Promise((resolve, reject) => { 49 | const query = projectModel.find(conditions); 50 | 51 | if (selectParams) { 52 | query.select(selectParams); 53 | } 54 | 55 | query.lean().exec((err, docs) => { 56 | if (docs) { 57 | resolve(docs); 58 | } 59 | else { 60 | reject(err); 61 | } 62 | }); 63 | }); 64 | } 65 | 66 | static get (conditions, selectParams) { 67 | return new Promise((resolve, reject) => { 68 | const query = projectModel.findOne(conditions); 69 | 70 | if (selectParams) { 71 | query.select(selectParams); 72 | } 73 | 74 | query.lean().exec((err, docs) => { 75 | if (docs) { 76 | resolve(docs); 77 | } 78 | else { 79 | reject(err); 80 | } 81 | }); 82 | }); 83 | } 84 | 85 | static remove (conditions) { 86 | return new Promise((resolve, reject) => { 87 | projectModel.remove(conditions, (err, docs) => { 88 | if (docs) { 89 | resolve(docs); 90 | } 91 | else { 92 | reject(err); 93 | } 94 | }); 95 | }); 96 | } 97 | 98 | static findOneAndUpdate (conditions, updateData, options) { 99 | return new Promise((resolve, reject) => { 100 | projectModel.findOneAndUpdate(conditions, updateData, options, (err, docs) => { 101 | if (docs) { 102 | resolve(docs); 103 | } 104 | else { 105 | reject(err); 106 | } 107 | }); 108 | }); 109 | } 110 | 111 | static aggregation (pipeline) { 112 | return new Promise((resolve, reject) => { 113 | projectModel.aggregate(pipeline, (err, docs) => { 114 | if (err) { 115 | reject(err); 116 | } 117 | else { 118 | resolve(docs); 119 | } 120 | }); 121 | }); 122 | } 123 | 124 | static update (conditions, updateData, options) { 125 | return new Promise((resolve, reject) => { 126 | projectModel.update(conditions, updateData, options, (err, docs) => { 127 | if (docs) { 128 | resolve(docs); 129 | } 130 | else { 131 | reject(err); 132 | } 133 | }); 134 | }); 135 | } 136 | } 137 | 138 | 139 | module.exports = Project; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wizni", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "async": { 8 | "version": "2.6.1", 9 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", 10 | "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", 11 | "requires": { 12 | "lodash": "^4.17.10" 13 | } 14 | }, 15 | "bluebird": { 16 | "version": "3.5.0", 17 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", 18 | "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" 19 | }, 20 | "bson": { 21 | "version": "1.0.9", 22 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.9.tgz", 23 | "integrity": "sha512-IQX9/h7WdMBIW/q/++tGd+emQr0XMdeZ6icnT/74Xk9fnabWn+gZgpE+9V+gujL3hhJOoNrnDVY7tWdzc7NUTg==" 24 | }, 25 | "debug": { 26 | "version": "2.6.9", 27 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 28 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 29 | "requires": { 30 | "ms": "2.0.0" 31 | } 32 | }, 33 | "kareem": { 34 | "version": "2.2.1", 35 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.2.1.tgz", 36 | "integrity": "sha512-xpDFy8OxkFM+vK6pXy6JmH92ibeEFUuDWzas5M9L7MzVmHW3jzwAHxodCPV/BYkf4A31bVDLyonrMfp9RXb/oA==" 37 | }, 38 | "lodash": { 39 | "version": "4.17.10", 40 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", 41 | "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" 42 | }, 43 | "lodash.get": { 44 | "version": "4.4.2", 45 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 46 | "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" 47 | }, 48 | "mongodb": { 49 | "version": "3.0.10", 50 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.0.10.tgz", 51 | "integrity": "sha512-jy9s4FgcM4rl8sHNETYHGeWcuRh9AlwQCUuMiTj041t/HD02HwyFgmm2VZdd9/mA9YNHaUJLqj0tzBx2QFivtg==", 52 | "requires": { 53 | "mongodb-core": "3.0.9" 54 | } 55 | }, 56 | "mongodb-core": { 57 | "version": "3.0.9", 58 | "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.0.9.tgz", 59 | "integrity": "sha512-buOWjdLLBlEqjHDeHYSXqXx173wHMVp7bafhdHxSjxWdB9V6Ri4myTqxjYZwL/eGFZxvd8oRQSuhwuIDbaaB+g==", 60 | "requires": { 61 | "bson": "~1.0.4", 62 | "require_optional": "^1.0.1" 63 | } 64 | }, 65 | "mongoose": { 66 | "version": "5.1.7", 67 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.1.7.tgz", 68 | "integrity": "sha512-9zus8yEovPqLo3S2iXz2Dg9YJAo8xG7m161abqJbUNIqZYjvpPwjmWAs6ChZnxEUpTYFOve//ljvyA5V5D9sNw==", 69 | "requires": { 70 | "async": "2.6.1", 71 | "bson": "~1.0.5", 72 | "kareem": "2.2.1", 73 | "lodash.get": "4.4.2", 74 | "mongodb": "3.0.10", 75 | "mongoose-legacy-pluralize": "1.0.2", 76 | "mpath": "0.4.1", 77 | "mquery": "3.0.0", 78 | "ms": "2.0.0", 79 | "regexp-clone": "0.0.1", 80 | "sliced": "1.0.1" 81 | } 82 | }, 83 | "mongoose-legacy-pluralize": { 84 | "version": "1.0.2", 85 | "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", 86 | "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" 87 | }, 88 | "mpath": { 89 | "version": "0.4.1", 90 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.4.1.tgz", 91 | "integrity": "sha512-NNY/MpBkALb9jJmjpBlIi6GRoLveLUM0pJzgbp9vY9F7IQEb/HREC/nxrixechcQwd1NevOhJnWWV8QQQRE+OA==" 92 | }, 93 | "mquery": { 94 | "version": "3.0.0", 95 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.0.0.tgz", 96 | "integrity": "sha512-WL1Lk8v4l8VFSSwN3yCzY9TXw+fKVYKn6f+w86TRzOLSE8k1yTgGaLBPUByJQi8VcLbOdnUneFV/y3Kv874pnQ==", 97 | "requires": { 98 | "bluebird": "3.5.0", 99 | "debug": "2.6.9", 100 | "regexp-clone": "0.0.1", 101 | "sliced": "0.0.5" 102 | }, 103 | "dependencies": { 104 | "sliced": { 105 | "version": "0.0.5", 106 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", 107 | "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" 108 | } 109 | } 110 | }, 111 | "ms": { 112 | "version": "2.0.0", 113 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 114 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 115 | }, 116 | "regexp-clone": { 117 | "version": "0.0.1", 118 | "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", 119 | "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" 120 | }, 121 | "require_optional": { 122 | "version": "1.0.1", 123 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", 124 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", 125 | "requires": { 126 | "resolve-from": "^2.0.0", 127 | "semver": "^5.1.0" 128 | } 129 | }, 130 | "resolve-from": { 131 | "version": "2.0.0", 132 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", 133 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" 134 | }, 135 | "semver": { 136 | "version": "5.5.0", 137 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", 138 | "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" 139 | }, 140 | "sliced": { 141 | "version": "1.0.1", 142 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", 143 | "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wizni", 3 | "version": "1.0.0", 4 | "description": "Programming Assignment", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "rest", 11 | "wizni", 12 | "challenge" 13 | ], 14 | "author": "Shalu Singhal", 15 | "license": "ISC", 16 | "dependencies": { 17 | "mongoose": "^5.1.7" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | Requirements 2 | ~~~~~~~~~~~~ 3 | 4 | 1. Node.js v10.5.0 5 | 2. MongoDB v3.6 6 | 7 | How to run 8 | ~~~~~~~~~~ 9 | 10 | 1. Make sure mongodb server is running. 11 | 2. Goto application code directory and run npm install. 12 | 3. Modify the connection string config.js 13 | 4. Run "node index.js" 14 | 15 | Project Structure 16 | ~~~~~~~~~~~~~~~~~ 17 | 18 | 1. index.js - Bootstraps the application 19 | 2. config.js - Database and other configurations 20 | 3. router.js - Custom router for routing endpoints 21 | 4. routes.js - List of routes (api endpoints in the project) 22 | 5. common - Some common functions/helpers that are used throught the project. 23 | 6. models - Project's models 24 | 7. controllers - Project's controllers 25 | 26 | Request Requirements 27 | ~~~~~~~~~~~~~~~~~~~~ 28 | 29 | 1. API is JSON based 30 | 2. All requests should have 'Content-Type: application/json' header set 31 | 3. Request body for POST and PUT should be in JSON format. 32 | 33 | Models 34 | ~~~~~~ 35 | (I have assumed manager and employee as same entity and distinguished them with 'isManager' property) 36 | Employee: 37 | { 38 | name: "" // Name of the employee, 39 | email: "" // Email of the employee 40 | isManager: true/false // If the employee is a manager or not 41 | peers: [] // Array of employee who have this employee as manager 42 | managerId: "" // If of this employee's manager 43 | } 44 | 45 | Project: 46 | { 47 | name: "" // Name of the project, 48 | employeeIds: [] // Array of employees who are part of this project 49 | managerId: "" // Id of this project's manager 50 | } 51 | 52 | Example Requests and Responses 53 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 54 | 55 | GET localhost:3000/employee 56 | Response: 57 | { 58 | "status": "success", 59 | "data": [ 60 | { 61 | "_id": "5b35af170177f90fd68fedd9", 62 | "name": "Shalu Singhal", 63 | "email": "shalu@gmail.com" 64 | }, 65 | { 66 | "_id": "5b366b7e38acd914fa966289", 67 | "name": "John Doe", 68 | "email": "john@example.com" 69 | }, 70 | { 71 | "_id": "5b366eb23259a415335f80c2", 72 | "name": "Ram Dev", 73 | "email": "ram@example.com" 74 | } 75 | ] 76 | } 77 | 78 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 79 | 80 | POST localhost:3000/employee 81 | Body: 82 | { 83 | "name": "Garry Tan", 84 | "email": "garry@example.com", 85 | "manager": "5b35af170177f90fd68fedd9", 86 | "peers": [ "5b370dfb1cf2c415cfe9c62a" ], 87 | "isManager": true 88 | } 89 | 90 | Response: 91 | { 92 | "status": "success", 93 | "data": { 94 | "_id": "5b3743f3afcb981bf0958e97", 95 | "isManager": false, 96 | "name": "Garry Tan", 97 | "email": "garry@example.com", 98 | "managerId": "5b35af170177f90fd68fedd9", 99 | "peers": [ "5b370dfb1cf2c415cfe9c62a" ] 100 | } 101 | } 102 | 103 | Note: I have kept the project simple so that you get idea of my programming capabilities. This just a proof of concept and is in no way 104 | a good ready to use REST API. -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | const { parse } = require('querystring'); 2 | const helpers = require('./common/helpers'); 3 | 4 | module.exports = async (req, res, routes) => { 5 | // Find a matching route 6 | const route = routes.find((route) => { 7 | const methodMatch = route.method === req.method; 8 | let pathMatch = false; 9 | 10 | if (typeof route.path === 'object') { 11 | // Path is a RegEx, we use RegEx matching 12 | pathMatch = req.url.match(route.path); 13 | } 14 | else { 15 | // Path is a string, we simply match with URL 16 | pathMatch = route.path === req.url; 17 | } 18 | 19 | return pathMatch && methodMatch; 20 | }); 21 | 22 | // Extract the "id" parameter from route and pass it to controller 23 | let param = null; 24 | 25 | if (route && typeof route.path === 'object') { 26 | param = req.url.match(route.path)[1]; 27 | } 28 | 29 | // Extract request body 30 | if (route) { 31 | let body = null; 32 | if (req.method === 'POST' || req.method === 'PUT') { 33 | body = await getPostData(req); 34 | } 35 | 36 | return route.handler(req, res, param, body); 37 | } 38 | else { 39 | return helpers.error(res, 'Endpoint not found', 404); 40 | } 41 | }; 42 | 43 | /** 44 | * Extract posted data from request body 45 | * @param req 46 | * @returns {Promise} 47 | */ 48 | function getPostData(req) { 49 | return new Promise((resolve, reject) => { 50 | try { 51 | let body = ''; 52 | req.on('data', chunk => { 53 | body += chunk.toString(); // convert Buffer to string 54 | }); 55 | 56 | req.on('end', () => { 57 | //resolve(parse(body)); 58 | resolve(body); 59 | }); 60 | } 61 | catch (e) { 62 | reject(e); 63 | } 64 | }); 65 | } -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * We define all our routes in this file. Routes are matched using `path`. 3 | * 1. If "path" is a string, then we simply match with url 4 | * 2. If "path is a object, then we assume it is a RegEx and use RegEx matching 5 | */ 6 | 7 | const employeeController = require('./controllers/EmployeeController'); 8 | const projectController = require('./controllers/ProjectController'); 9 | 10 | const routes = [ 11 | { 12 | method: 'GET', 13 | path: '/employee', 14 | handler: employeeController.index.bind(employeeController) 15 | }, 16 | { 17 | method: 'GET', 18 | path: /\/employee\/([0-9a-z]+)/, 19 | handler: employeeController.show.bind(employeeController) 20 | }, 21 | { 22 | method: 'POST', 23 | path: '/employee', 24 | handler: employeeController.create.bind(employeeController) 25 | }, 26 | { 27 | method: 'PUT', 28 | path: /\/employee\/([0-9a-z]+)/, 29 | handler: employeeController.update.bind(employeeController) 30 | }, 31 | { 32 | method: 'DELETE', 33 | path: /\/employee\/([0-9a-z]+)/, 34 | handler: employeeController.delete.bind(employeeController) 35 | }, 36 | { 37 | method: 'POST', 38 | path: '/project', 39 | handler: projectController.create.bind(projectController) 40 | }, 41 | { 42 | method: 'GET', 43 | path: '/project', 44 | handler: projectController.index.bind(projectController) 45 | }, 46 | { 47 | method: 'GET', 48 | path: /\/project\/([0-9a-z]+)/, 49 | handler: projectController.show.bind(projectController) 50 | }, 51 | { 52 | method: 'PUT', 53 | path: /\/project\/([0-9a-z]+)/, 54 | handler: projectController.update.bind(projectController) 55 | }, 56 | { 57 | method: 'DELETE', 58 | path: /\/project\/([0-9a-z]+)/, 59 | handler: projectController.delete.bind(projectController) 60 | }, 61 | ]; 62 | 63 | module.exports = routes; --------------------------------------------------------------------------------