diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..345130c --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# Created by https://www.gitignore.io/api/osx,vim,node,macos,windows + +### 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 ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.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 + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# 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 + +# dotenv environment variables file +.env + + +### OSX ### + +# Icon must end with two \r + +# Thumbnails + +# Files that might appear in the root of a volume + +# Directories potentially created on remote AFP share + +### Vim ### +# swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/osx,vim,node,macos,windows diff --git a/data/car/4c35a891-9915-4814-9f36-581832d882ff.json b/data/car/4c35a891-9915-4814-9f36-581832d882ff.json new file mode 100644 index 0000000..df58bba --- /dev/null +++ b/data/car/4c35a891-9915-4814-9f36-581832d882ff.json @@ -0,0 +1 @@ +{"id":"4c35a891-9915-4814-9f36-581832d882ff","make":"test make","model":"test model","year":"test year"} \ No newline at end of file diff --git a/data/car/635c7d19-c818-4f17-86f5-7302c3ef1a34.json b/data/car/635c7d19-c818-4f17-86f5-7302c3ef1a34.json new file mode 100644 index 0000000..4d84975 --- /dev/null +++ b/data/car/635c7d19-c818-4f17-86f5-7302c3ef1a34.json @@ -0,0 +1 @@ +{"id":"635c7d19-c818-4f17-86f5-7302c3ef1a34","make":"test make","model":"test model","year":"test year"} \ No newline at end of file diff --git a/data/car/6446d98d-6be4-47cb-b4d8-e3c7c839232a.json b/data/car/6446d98d-6be4-47cb-b4d8-e3c7c839232a.json new file mode 100644 index 0000000..7f5dae7 --- /dev/null +++ b/data/car/6446d98d-6be4-47cb-b4d8-e3c7c839232a.json @@ -0,0 +1 @@ +{"id":"6446d98d-6be4-47cb-b4d8-e3c7c839232a","make":"test make","model":"test model","year":"test year"} \ No newline at end of file diff --git a/data/car/71235e3c-6d98-4402-89e6-2b7dff711e54.json b/data/car/71235e3c-6d98-4402-89e6-2b7dff711e54.json new file mode 100644 index 0000000..d501514 --- /dev/null +++ b/data/car/71235e3c-6d98-4402-89e6-2b7dff711e54.json @@ -0,0 +1 @@ +{"id":"71235e3c-6d98-4402-89e6-2b7dff711e54","make":"test make","model":"test model","year":"test year"} \ No newline at end of file diff --git a/lib/parse-json.js b/lib/parse-json.js new file mode 100644 index 0000000..6f1e176 --- /dev/null +++ b/lib/parse-json.js @@ -0,0 +1,30 @@ +'use strict'; + +module.exports = function(req) { + return new Promise((resolve, reject) => { + if (req.method === 'POST' || req.method === 'PUT') { + var body = ''; + + req.on('data', data => { + body += data.toString(); + }); + + req.on('end', () => { + try { + req.body = JSON.parse(body); + resolve(req); + } catch (err) { + console.error(err); + reject(err); + } + }); + + req.on('error', err => { + console.error(err); + reject(err); + }); + return; + } + resolve(); + }); +}; diff --git a/lib/parse-url.js b/lib/parse-url.js new file mode 100644 index 0000000..efe7507 --- /dev/null +++ b/lib/parse-url.js @@ -0,0 +1,10 @@ +'use strict'; + +const parseUrl = require('url').parse +const parseQuery = require('querystring').parse; + +module.exports = function(req) { + req.url = parseUrl(req.url); + req.url.query = parseQuery(req.url.query); + return Promise.resolve(req); +} diff --git a/lib/response.js b/lib/response.js new file mode 100644 index 0000000..5997583 --- /dev/null +++ b/lib/response.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = exports = {}; + +exports.sendJSON = function(res, status, data) { + res.writeHead(status, { + 'Content-type': 'application/json' + }); + + res.write(JSON.stringify(data)); + res.end(); +} + +exports.sendText = function(res, status, msg) { + res.writeHead(status, { + 'Content-type': 'text/plain' + }); + + res.write(msg); + res.end(); +}; diff --git a/lib/router.js b/lib/router.js new file mode 100644 index 0000000..dd3b78a --- /dev/null +++ b/lib/router.js @@ -0,0 +1,63 @@ +'use strict'; + +const parseUrl = require('./parse-url.js'); +const parseJSON = require('./parse-json.js'); + +const Router = module.exports = function() { + this.routes = { + GET: {}, + POST: {}, + PUT: {}, + DELETE: {} + }; +}; + +Router.prototype.get = function(endpoint, callback) { + this.routes.GET[endpoint] = callback; +}; + +Router.prototype.post = function(endpoint, callback) { + this.routes.POST[endpoint] = callback; +}; + +Router.prototype.put = function(endpoint,callback) { + this.routes.PUT[endpoint] = callback; +}; + +Router.prototype.delete = function(endpoint, callback) { + this.routes.DELETE[endpoint] = callback; +}; + +Router.prototype.route = function () { + return (req, res) => { + Promise.all([ + parseUrl(req), + parseJSON(req) + ]) + .then( () => { + if (typeof this.routes[req.method][req.url.pathname] === 'function') { + this.routes[req.method][req.url.pathname](req,res); + return; + } + + console.error('route not found'); + + res.writeHead(404, { + 'Content-Type' : 'text/plain' + }); + + res.write('route not found'); + res.end(); + }) + .catch(err => { + console.error(err); + + res.writeHead(400, { + 'Content-Type' : 'text/plain' + }); + + res.write('bad request'); + res.end(); + }); + }; +}; diff --git a/lib/storage.js b/lib/storage.js new file mode 100644 index 0000000..a4920b7 --- /dev/null +++ b/lib/storage.js @@ -0,0 +1,42 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), { suffix: 'Prom'}); + +module.exports = exports = {}; + +exports.createItem = function(schemaName, item) { + if (!schemaName) return Promise.reject(new Error('expected schema name')); + if (!item) return Promise.reject(new Error('expected item')); + + let json = JSON.stringify(item); + return fs.writeFileProm(`${__dirname}/../data/${schemaName}/${item.id}.json`,json) + .then( () => item) + .catch( err => Promise.reject(err)) +} + +exports.fetchItem = function(schemaName, id) { + if (!schemaName) return Promise.reject(new Error('expected schema name')); + if (!id) return Promise.reject(new Error('expected id')); + + return fs.readFileProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .then( data => { + try { + let item = JSON.parse(data.toString()); + return item; + } catch(err) { + return Promise.reject(err); + } + }) + .catch( err => Promise.reject(err)); +} + +exports.deleteItem = function(schemaName, id) { + if (!schemaName) return Promise.reject(new Error('expected schema name')); + if (!id) return Promise.reject(new Error('expected id')) + + let json = JSON.stringify(id); + return fs.writeFileProm(`${__dirname}/../data/${schemaName}/${item.id}.json`, json) + .then( () => item) + .catch( err => Promise.reject(err)) +} diff --git a/model/car.js b/model/car.js new file mode 100644 index 0000000..1a95534 --- /dev/null +++ b/model/car.js @@ -0,0 +1,14 @@ +'use strict'; + +const uuidv4 = require('uuid/v4'); + +module.exports = function(make, model, year) { + if (!make) throw new Error('expected make'); + if (!model) throw new Error('expected model'); + if (! year) throw new Error('expected year'); + + this.id = uuidv4(); + this.make = make; + this.model = model; + this.year = year; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..0d51926 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "09-vanilla_api_persistence", + "version": "1.0.0", + "description": "![cf](https://i.imgur.com/7v5ASc8.png) Lab 09: Vanilla REST API w/ Persistence ======", + "main": "server.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/sharmarkei/09-vanilla_api_persistence.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/sharmarkei/09-vanilla_api_persistence/issues" + }, + "homepage": "https://github.com/sharmarkei/09-vanilla_api_persistence#readme", + "devDependencies": { + "chai": "^4.1.0", + "mocha": "^3.4.2", + "superagent": "^3.5.2" + }, + "dependencies": { + "bluebird": "^3.5.0", + "uuid": "^3.1.0" + } +} diff --git a/route/car-route.js b/route/car-route.js new file mode 100644 index 0000000..dd478a7 --- /dev/null +++ b/route/car-route.js @@ -0,0 +1,46 @@ +'use strict'; + +const storage = require('../lib/storage.js'); +const response = require('../lib/response.js'); +const Car = require('../model/car.js'); + +module.exports = function(router) { + router.get('/api/car', function(req, res) { + if (req.url.query.id) { + storage.fetchItem('car', req.url.query.id) + .then( car => { + response.sendJSON(res, 200, car); + }) + .catch( err => { + response.sendText(res, 404, 'not found'); + }); + + return; + } + response.sendText(res, 400, 'bad request'); + }); + + router.post('/api/car', function(req, res) { + try { + var car = new Car(req.body.make, req.body.model, req.body.year); + storage.createItem('car', car); + response.sendJSON(res, 200, car); + } catch (err) { + console.error(err); + response.sendText(res, 400, 'bad request'); + } + }); + + router.delete('api/car', function(req, res) { + if(req.url.query.id) { + storage.deleteItem('car', req.url.query.id) + .then( car => { + response.sendJSON(res, 204, car); + }) + .catch( err => { + response.sendText(res, 404, 'car not found'); + }); + return; + } + }); +}; diff --git a/server.js b/server.js new file mode 100644 index 0000000..4f89eaa --- /dev/null +++ b/server.js @@ -0,0 +1,14 @@ +'use strict'; + +const http = require('http'); +const Router = require('./lib/router.js'); +const PORT = process.env.PORT || 3000; +const router = new Router(); + +require('./route/car-route.js')(router) + +const server = http.createServer(router.route()); + +server.listen(PORT, () => { +console.log('server up:', PORT); +}); diff --git a/test/car-route-test.js b/test/car-route-test.js new file mode 100644 index 0000000..06312a7 --- /dev/null +++ b/test/car-route-test.js @@ -0,0 +1,78 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; + +require('../server.js'); + +describe('Car Routes', function() { + var car = null; + + describe('POST: /api/car', function() { + it('should return a car', function(done) { + request.post('localhost:8000/api/car') + .send({make: 'test make', model: 'test model', year: 'test year'}) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.make).to.equal('test make'); + expect(res.body.model).to.equal('test model'); + expect(res.body.year).to.equal('test year'); + car = res.body; + console.log('post 200'); + done(); + }); + }); + it('should return 400', function(done) { + request.post('localhost:8000/api/car') + .send({}) + .end((err, res) => { + expect(res.status).to.equal(400); + console.log('post 400'); + done(); + }); + }); + }); + + describe('GET: /api/car', function() { + it('should return a car', function(done) { + request.get(`localhost:8000/api/car?id=${car.id}`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.make).to.equal('test make'); + expect(res.body.model).to.equal('test model'); + expect(res.body.year).to.equal('test year'); + console.log('inside'); + done(); + }); + }); + it('should return 404', function(done) { + request.get('localhost:8000/api/car?id=5432') + .end((err, res) => { + expect(res.status).to.equal(404); + console.log('get 404'); + done(); + }); + }); + it('should return 400', function(done) { + request.get('localhost:8000/api/car') + .end((err, res) => { + expect(res.status).to.equal(400); + console.log('get 400'); + done(); + }); + }); + }); + // describe('DELETE: /api/car', function() { + // it('should return 204', function(done) { + // request.delete(`localhost:8000/api/car?id=${car.id}`) + // .end((err, res) => { + // expect(res.status).to.equal(204); + // console.log('delete 204'); + // + // done(); + // }); + // }); + // }); +});