diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..c4c483c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,21 @@ +{ + "rules": { + "no-console": "off", + "indent": [ "error", 2 ], + "quotes": [ "error", "single" ], + "semi": ["error", "always"] + # "linebreak-style": [ "error", "unix" ] + }, + "env": { + "es6": true, + "node": true, + "mocha": true, + "jasmine": true + }, + "ecmaFeatures": { + "modules": true, + "experimentalObjectRestSpread": true, + "impliedStrict": true + }, + "extends": "eslint:recommended" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96bb76c --- /dev/null +++ b/.gitignore @@ -0,0 +1,127 @@ +Created by https://www.gitignore.io/api/macos,node,vim,windows,linux + +out.bmp +### macOS ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon +# Thumbnails +._* +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Node ### +node_modules +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + + + +### Vim ### +# swap +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c20e54 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# REST API USING Express + +This REST-API provides necessary backend support functionality create, read, update and delete. Currently it is saving data on data file using fs-node module which provides a little persistence. + +### Dependencies: + +- bluebird +- body-parser +- debug +- express +- http-error +- morgan +- node-uuid +- superagent +- mocha +- chai + +This API is structured on a Model View Controller(MVC) architecture pattern. The base technologies are node.js express. + + +POST: +topic is required parameter as request body. +example: http POST localhost:3000/api/simple_resource +data: topic='some topic' book='some book'. + +result: + +```` +http POST localhost:3000/api/simple_resource topic='some topic' book='some book' +HTTP/1.1 200 OK +Connection: keep-alive +Content-Length: 85 +Content-Type: application/json; charset=utf-8 +Date: Tue, 20 Dec 2016 19:29:22 GMT +ETag: W/"55-rGlt9UK1EtZHiKpS3mGyxw" +X-Powered-By: Express + +{ + "book": "some book", + "id": "9c5aebb0-c6ea-11e6-865c-d5e13121e327", + "topic": "some topic" +} +```` + +GET: +id is required as query string. +example: http GET localhost:3000/api/simple_resource?id="xxxxxxxxxxxxxxxx" + + +```` +http GET localhost:3000/api/simple_resource id=="9c5aebb0-c6ea-11e6-865c-d5e13121e327" + + +HTTP/1.1 200 OK +Connection: keep-alive +Content-Length: 85 +Content-Type: application/json; charset=utf-8 +Date: Tue, 20 Dec 2016 19:30:40 GMT +ETag: W/"55-rGlt9UK1EtZHiKpS3mGyxw" +X-Powered-By: Express + +{ + "book": "some book", + "id": "9c5aebb0-c6ea-11e6-865c-d5e13121e327", + "topic": "some topic" +} + +```` + +PUT: +id is required parameter as request body. +example: http PUT localhost:3000/api/simple_resource id="xxxxxxxxxxxxxxxx" book='xxx' topic='xxx' instructor='xxxxx' + +```` +http PUT localhost:3000/api/simple_resource id="9c5aebb0-c6ea-11e6-865c-d5e13121e327" book='you donot know js' topic='javascript' instructor='Brian' + +HTTP/1.1 200 OK +Connection: keep-alive +Content-Length: 114 +Content-Type: application/json; charset=utf-8 +Date: Tue, 20 Dec 2016 19:33:50 GMT +ETag: W/"72-CHOLh28sVJNLFGAmtoAvCg" +X-Powered-By: Express + +{ + "book": "you donot know js", + "id": "9c5aebb0-c6ea-11e6-865c-d5e13121e327", + "instructor": "Brian", + "topic": "javascript" +} +```` + +DELETE: +id is required parameter as request query string. +example: http DELETE localhost:3000/api/simple_resource id="xxxxxxxxxxxxxxxx" +```` +http DELETE localhost:3000/api/simple_resource id=="9c5aebb0-c6ea-11e6-865c-d5e13121e327" + +HTTP/1.1 200 OK +Connection: keep-alive +Content-Length: 15 +Content-Type: application/json; charset=utf-8 +Date: Tue, 20 Dec 2016 19:39:16 GMT +ETag: W/"f-ylZ9HW13eVLeeOl4qKFjDA" +X-Powered-By: Express + +"file deleted!" +```` diff --git a/lib/storage.js b/lib/storage.js new file mode 100644 index 0000000..51eb32e --- /dev/null +++ b/lib/storage.js @@ -0,0 +1,79 @@ +'use strict'; + +const createError = require('http-errors'); +const debug = require('debug')('simple_resource:storage'); +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), {suffix: 'Prom'}); + +module.exports = exports = {}; + +exports.generate = function(schemaName, item){ + debug('storage:generate'); + if(!schemaName) return Promise.reject(createError(400, 'schemaName required!')); + if(!item) return Promise.reject(createError(400, 'item required')); + + //check if dir schemaName exist? if not create one. + var create = function(){ + var dir = fs.readdirSync('./data').filter(function(value){ + return value == schemaName; + })[0]; + + if(!dir) { + fs.mkdirSync(`./data/${schemaName}`); + } + return Promise.resolve(dir); + }; + + return create().then(() => { + var json = JSON.stringify(item); + return fs.writeFileProm(`${__dirname}/../data/${schemaName}/${item.id}.json`, json) + .then( () => item ) + .catch( err => Promise.reject(createError(500, err.message))); + + }).catch( err => Promise.reject(createError(500, err.message))); + +}; + +exports.fetch = function(schemaName, id){ + debug('storage:fetch'); + if(!id) Promise.reject(createError(400, 'id expected')); + if(!schemaName) return Promise.reject(createError(400, 'schemaName required!')); + + + return fs.readFileProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .then(data => { + if(!data) return Promise.reject(createError(404, 'file empty')); + return Promise.resolve(data.toString('utf8')); + }) + .catch(() => Promise.reject(createError(404, 'not found'))); +}; + +exports.put = function(schemaName, item){ + debug('storage:put'); + if(!item.id) Promise.reject(createError(400, 'id expected as request body')); + if(!schemaName) Promise.reject(createError(400, 'schemaName expected as request body')); + console.log('\nitem: ', item); + + return fs.readFileProm(`${__dirname}/../data/${schemaName}/${item.id}.json`) + .then(resource => { + resource = JSON.parse(resource.toString('utf-8')); + if(item.topic) resource.topic = item.topic; + if(item.book) resource.book = item.book; + if(item.instructor) resource.instructor = item.instructor; + + return fs.writeFileProm(`${__dirname}/../data/${schemaName}/${item.id}.json`, JSON.stringify( resource)) + .then(() => item) + .catch( err => Promise.reject(createError(500, err.message))); + }) + .catch( err => Promise.reject(createError(500, err.message))); +}; + +exports.del = function(schemaName, id){ + debug('storage:del'); + if(!id) Promise.reject(createError(400, 'id expected')); + if(!schemaName) return Promise.reject(createError(400, 'schemaName required!')); + + return fs.unlinkProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .then( () => Promise.resolve('file deleted!')) + .catch(err => Promise.reject(createError(500, err.message))); +}; diff --git a/model/resource.js b/model/resource.js new file mode 100644 index 0000000..c0800ae --- /dev/null +++ b/model/resource.js @@ -0,0 +1,25 @@ +'use strict'; +const uuid = require('node-uuid'); +const storage = require('../lib/storage.js'); +const debug = require('debug')('simple_resource:resource'); +const createError = require('http-errors'); + + +module.exports = exports = function(topic, book, instructor){ + if(!topic) return Promise.reject(createError(400, 'topic required!')); + this.id = uuid.v1(); + this.topic = topic; + this.book = book; + this.instructor = instructor; +}; + +exports.create = function(_Simple_resource){ + debug('resource:create'); + try{ + var resource = new exports(_Simple_resource.topic, _Simple_resource.book, _Simple_resource.instructor); + return storage.generate('simple_resource', resource); + }catch(err){ + return Promise.reject(createError(500, err.message)); + } + +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..76c3a46 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "11-express_single_resource_api", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "test": "set DEBUG=simple_resource* && mocha", + "start": "set DEBUG=simple_resource* && node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/bhavyaab/11-express_single_resource_api.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/bhavyaab/11-express_single_resource_api/issues" + }, + "homepage": "https://github.com/bhavyaab/11-express_single_resource_api#readme", + "devDependencies": { + "bluebird": "^3.4.6", + "body-parser": "^1.15.2", + "chai": "^3.5.0", + "debug": "^2.4.5", + "express": "^4.14.0", + "http-error": "0.0.6", + "mocha": "^3.2.0", + "morgan": "^1.7.0", + "node-uuid": "^1.4.7", + "superagent": "^3.3.1" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..866583b --- /dev/null +++ b/server.js @@ -0,0 +1,54 @@ +'use strict'; +console.log(process.env.DEBUG); +const express = require('express'); +const morgan = require('morgan'); +const createError = require('http-errors'); +const debug = require('debug')('simple_resource:server'); +const jsonParser = require('body-parser').json(); + +const resource = require('./model/resource.js'); +const storage = require('./lib/storage.js'); + +const app = express(); +const PORT = 3000; + +app.use(morgan('dev')); + +app.post('/api/simple_resource', jsonParser, function(req, res, next){ + debug('POST /api/simple_resource'); + + resource.create(req.body) + .then(resource => res.json(resource)) + .catch(err => next(err)); +}); + +app.get('/api/simple_resource', function(req, res, next){ + debug('GET /api/simple_resource'); + + storage.fetch('simple_resource', req.query.id) + .then(resource => res.json(resource)) + .catch(err => next(err)); +}); + +app.put('/api/simple_resource', jsonParser, function(req, res, next){ + debug('PUT /api/simple_resource'); + + storage.put('simple_resource', req.body) + .then(resource => res.json(resource)) + .catch( err => next(err)); +}); + +app.delete('/api/simple_resource', function(req, res, next){ + debug('DELETE /api/simple_resource'); + + + storage.del('simple_resource', req.query.id) + .then(msg =>{ + res.json(msg); + }) + .catch( err => next(err)); +}); + +app.listen(PORT, () => { + console.log(`surver PORT: ${PORT} on!!`); +}); diff --git a/test/route-test.js b/test/route-test.js new file mode 100644 index 0000000..1cf2177 --- /dev/null +++ b/test/route-test.js @@ -0,0 +1,66 @@ +'use strict'; + +const expect = require('chai').expect; +const request = require('superagent'); +const url = 'localhost:3000/api/simple_resource'; + +require('../server.js'); + +describe('route test /api/simple_resource', function(){ + var simple_resource; + describe('POST', function(){ + it('should return a valid simple_resource', function(done){ + request.post(`${url}`) + .send({topic:'javascript', book:'you donot know js'}) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.topic).to.equal('javascript'); + expect(res.body.book).to.equal('you donot know js'); + simple_resource = res.body; + done(); + }); + }); + }); + describe('GET', function(){ + it('should return a valid simple_resource', function(done){ + request.get(`${url}?id=${simple_resource.id}`) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + res.body = JSON.parse(res.body); + expect(res.body.id).to.equal(`${simple_resource.id}`); + expect(res.body.topic).to.equal('javascript'); + expect(res.body.book).to.equal('you donot know js'); + expect(res.body.instructor).to.equal(undefined); + done(); + }); + }); + }); + describe('PUT', function(){ + it('should update a note of simple_resource', function(done){ + console.log(simple_resource); + request.put(`${url}`) + .send({id:`${simple_resource.id}`, topic:'javascript', book:'Node Beginner Book'}) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.id).to.equal(`${simple_resource.id}`); + expect(res.body.topic).to.equal('javascript'); + expect(res.body.book).to.equal('Node Beginner Book'); + done(); + }); + }); + }); + describe('DELETE', function(){ + it('should delete a note of simple_resource', function(done){ + request.delete(`${url}?id=${simple_resource.id}`) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.text).to.equal('"file deleted!"'); + done(); + }); + }); + }); +});