From 0451fa83bf61399b729309ed0f7a167f68a4534d Mon Sep 17 00:00:00 2001 From: Zachary Crumbo Date: Thu, 23 Feb 2017 20:59:02 -0800 Subject: [PATCH 1/3] all files except for readme --- lab-zachary/.eslintrc | 21 +++++ lab-zachary/.gitignore | 129 ++++++++++++++++++++++++++++ lab-zachary/README.md | 0 lab-zachary/gulpfile.js | 23 +++++ lab-zachary/lib/parse-body.js | 29 +++++++ lab-zachary/lib/parse-url.js | 9 ++ lab-zachary/lib/response.js | 19 ++++ lab-zachary/lib/router.js | 51 +++++++++++ lab-zachary/lib/storage.js | 46 ++++++++++ lab-zachary/model/bike.js | 12 +++ lab-zachary/package.json | 24 ++++++ lab-zachary/routes/bike-route.js | 48 +++++++++++ lab-zachary/server.js | 15 ++++ lab-zachary/test/bike-route-test.js | 73 ++++++++++++++++ 14 files changed, 499 insertions(+) create mode 100644 lab-zachary/.eslintrc create mode 100644 lab-zachary/.gitignore create mode 100644 lab-zachary/README.md create mode 100644 lab-zachary/gulpfile.js create mode 100644 lab-zachary/lib/parse-body.js create mode 100644 lab-zachary/lib/parse-url.js create mode 100644 lab-zachary/lib/response.js create mode 100644 lab-zachary/lib/router.js create mode 100644 lab-zachary/lib/storage.js create mode 100644 lab-zachary/model/bike.js create mode 100644 lab-zachary/package.json create mode 100644 lab-zachary/routes/bike-route.js create mode 100644 lab-zachary/server.js create mode 100644 lab-zachary/test/bike-route-test.js diff --git a/lab-zachary/.eslintrc b/lab-zachary/.eslintrc new file mode 100644 index 0000000..8dc6807 --- /dev/null +++ b/lab-zachary/.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/lab-zachary/.gitignore b/lab-zachary/.gitignore new file mode 100644 index 0000000..89ab13a --- /dev/null +++ b/lab-zachary/.gitignore @@ -0,0 +1,129 @@ + +# Created by https://www.gitignore.io/api/node,osx,windows,linux + +### 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* + +### 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 ### +*.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 + +### 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/node,osx,windows,linux \ No newline at end of file diff --git a/lab-zachary/README.md b/lab-zachary/README.md new file mode 100644 index 0000000..e69de29 diff --git a/lab-zachary/gulpfile.js b/lab-zachary/gulpfile.js new file mode 100644 index 0000000..612786c --- /dev/null +++ b/lab-zachary/gulpfile.js @@ -0,0 +1,23 @@ +'use strict'; + +const gulp = require('gulp'); +const eslint = require('gulp-eslint'); +const mocha = require ('gulp-mocha'); + +gulp.task('test', function(){ + gulp.src('./test/*-test.js', { read: false}) + .pipe(mocha({ reporter: 'spec'})); +}); + +gulp.task('lint', function() { + return gulp.src(['**/*.js', '!node_modules']) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +gulp.task('dev', function(){ + gulp.watch(['**/*.js', '!node_modules/**'], ['lint']); +}); + +gulp.task('default', ['dev']); diff --git a/lab-zachary/lib/parse-body.js b/lab-zachary/lib/parse-body.js new file mode 100644 index 0000000..ad541d2 --- /dev/null +++ b/lab-zachary/lib/parse-body.js @@ -0,0 +1,29 @@ +'use strict'; + +module.exports = function(req){ + return new Promise((resolve, reject) => { + if (req.method === 'POST' || req.method == 'PUT') { + var body = ''; + req.on('data', function(data){ + body += data.toString(); + }); + + req.on('end', function() { + 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(); + }); +}; \ No newline at end of file diff --git a/lab-zachary/lib/parse-url.js b/lab-zachary/lib/parse-url.js new file mode 100644 index 0000000..bedabed --- /dev/null +++ b/lab-zachary/lib/parse-url.js @@ -0,0 +1,9 @@ +'use strict'; + +const parse = require('url').parse; + +module.exports = function(req){ + //return object with url, query string, parameters etc + req.url = parse(req.url, true); //true parameter returns query string as an object + return Promise.resolve(req); +}; \ No newline at end of file diff --git a/lab-zachary/lib/response.js b/lab-zachary/lib/response.js new file mode 100644 index 0000000..f1032dd --- /dev/null +++ b/lab-zachary/lib/response.js @@ -0,0 +1,19 @@ +'use strict'; + +exports = module.exports = {}; + +exports.sendText = function(res, status, message){ + res.writeHead(status, { + 'Content-Type' : 'text/plain' + }); + res.write(message); + res.end(); +}; + +exports.sendJSON = function (res, status, data){ + res.writeHead(status, { + 'Content-Type' : 'application/json' + }); + res.write(JSON.stringify(data)); + res.end(); +}; \ No newline at end of file diff --git a/lab-zachary/lib/router.js b/lab-zachary/lib/router.js new file mode 100644 index 0000000..0a1b27e --- /dev/null +++ b/lab-zachary/lib/router.js @@ -0,0 +1,51 @@ +'use strict'; + +const Promise = require('bluebird'); +const parseUrl = require('./parse-url.js'); +const parseBody = require('./parse-body.js'); +const writeResponse = require('./response.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), + parseBody(req), + ]) + .then( () => { + //request is valid, check if route is registered + if (typeof this.routes[req.method][req.url.pathname] === 'function'){ + this.routes[req.method][req.url.pathname](req, res); + return; + } + //endpoint not found/route not registered. return 404 + writeResponse.sendText(res, 404, 'not found (router.js)'); + res.end(); + }) + .catch( err => { //promise.all fails, url or post body malformed + console.error(err); + writeResponse.sendText(res, 400, 'bad request'); + }); + }; +}; \ No newline at end of file diff --git a/lab-zachary/lib/storage.js b/lab-zachary/lib/storage.js new file mode 100644 index 0000000..f5174b3 --- /dev/null +++ b/lab-zachary/lib/storage.js @@ -0,0 +1,46 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), {suffix: 'Prom'}); + +//create, fetch delete + +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) //whuck? look into this format. return item implied with function. + .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 item')); + + 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')); + + return fs.unlinkProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .then( () => id) + .catch( err => Promise.reject(err)); +}; + + + diff --git a/lab-zachary/model/bike.js b/lab-zachary/model/bike.js new file mode 100644 index 0000000..30dd00e --- /dev/null +++ b/lab-zachary/model/bike.js @@ -0,0 +1,12 @@ +'use strict'; + +const uuid = require('node-uuid'); + +module.exports = function(name, content){ + if (!name) throw new Error('name expected!'); + if (!content) throw new Error('content expected!'); + + this.id = uuid.v4(); + this.name = name; + this.content = content; +}; \ No newline at end of file diff --git a/lab-zachary/package.json b/lab-zachary/package.json new file mode 100644 index 0000000..05f1aef --- /dev/null +++ b/lab-zachary/package.json @@ -0,0 +1,24 @@ +{ + "name": "lab-zachary", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "chai": "^3.5.0", + "gulp": "^3.9.1", + "gulp-eslint": "^3.0.1", + "gulp-mocha": "^4.0.1", + "superagent": "^3.5.0" + }, + "dependencies": { + "bluebird": "^3.4.7", + "node-uuid": "^1.4.7" + } +} diff --git a/lab-zachary/routes/bike-route.js b/lab-zachary/routes/bike-route.js new file mode 100644 index 0000000..9001705 --- /dev/null +++ b/lab-zachary/routes/bike-route.js @@ -0,0 +1,48 @@ +'use strict'; + +const Bike = require('../model/bike.js'); +const storage = require('../lib/storage.js'); +const writeResponse = require('../lib/response.js'); + +const itemType = 'bike'; + +module.exports = function (router){ + router.get(`/api/${itemType}`, function(req, res){ + if(req.url.query.id){ + storage.fetchItem('bike', req.url.query.id)//promise + .then( bike => { + writeResponse.sendJSON(res, 200, bike); + }).catch( err => { + console.error(err); + writeResponse.sendText(res, 404, 'not found'); + }); + return; + } + writeResponse.sendText(res, 400, 'bad request'); + }); + + router.post(`/api/${itemType}`, function(req, res){ + try{ + var bike = new Bike(req.body.name, req.body.content, 'bike'); + storage.createItem('bike', bike); + writeResponse.sendJSON(res, 200, bike); //<-- look into this bike parameter being passed + } catch (err) { + console.error(err); + writeResponse.sendText(res, 400, 'bad request'); + } + }); + router.delete(`/api/${itemType}`, function(req, res){ + if(req.url.query.id){ + storage.deleteItem('bike', req.url.query.id) + .then( bike => { + writeResponse.sendJSON(res, 204, bike); + }).catch( err => { + console.error(err); + writeResponse.sendText(res, 404, 'not found'); + }); + return; + } + writeResponse.sendText(res, 400, 'bad request'); + }); + +}; diff --git a/lab-zachary/server.js b/lab-zachary/server.js new file mode 100644 index 0000000..402a8d3 --- /dev/null +++ b/lab-zachary/server.js @@ -0,0 +1,15 @@ +'use strict'; + +const http = require('http'); +const Router = require('./lib/router.js'); + +const PORT = process.env.PORT || 3000; +const router = new Router(); + +require('./routes/bike-route.js')(router); + +const server = http.createServer(router.route()); + +server.listen(PORT, () =>{ + console.log('server up:', PORT); +}); \ No newline at end of file diff --git a/lab-zachary/test/bike-route-test.js b/lab-zachary/test/bike-route-test.js new file mode 100644 index 0000000..3fe90f0 --- /dev/null +++ b/lab-zachary/test/bike-route-test.js @@ -0,0 +1,73 @@ +'use strict'; + +const request = require ('superagent'); +const expect = require('chai').expect; +const open = require('fs').openSync; + +require('../server.js') + +describe('Bike Route Test', function(){ + var bike = ''; + describe('POST route test', function(){ + it('Should return success with properly formatted request', function(done){ + request.post(':8000/api/bike') + .send({'name':'test name', 'content':'test body content'}) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.content).to.equal('test body content'); + expect(res.body.name).to.equal('test name'); + bike=res; + done(); + }); + }); + it('should resond with \'bad request\'if body content was not provided or was invalid', function(done){ + request.post('localhost:8000/api/bike') + .send({invalid: 'content', willNot: 'work'}) + .end((err, res) => { + expect(err.message).to.equal('Bad Request'); + expect(res.status).to.equal(400); + done(); + }); + }); + }); + describe('GET route test', function(){ + it('should return a 200 on request w/proper query string', function(done){ + request.get(`:8000/api/bike?id=${bike.body.id}`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('test name'); + expect(res.body.content).to.equal('test body content'); + done(); + }); + }); + it('should respond w/ \'not found\' for valid request w/id that was not found', function(done){ + request.get('localhost:8000/api/bike?id=not-an-id') + .end((err, res) => { + expect(err.message).to.equal('Not Found'); + expect(res.status).to.equal(404); + done(); + }); + }); + it('should respond w/ \'bad request\' if no id provided', function(done){ + request.get('localhost:8000/api/bike?id=') + .end((err, res) => { + expect(err.message).to.equal('Bad Request'); + expect(res.status).to.equal(400); + done(); + }); + }); + }); + describe('DELETE route test', function(){ + it('should delete the bike record', function(done){ + request.delete(`:8000/api/bike?id=${bike.body.id}`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(204); + expect(open.bind(open, `${__dirname}/../data/bike/${bike.body.id}.json`, 'r')).to.throw(Error); + done(); + }); + }); + }); +}); From 8e3d1678f2fbc820273804eb5696d037f55808d4 Mon Sep 17 00:00:00 2001 From: Zachary Crumbo Date: Thu, 23 Feb 2017 21:03:26 -0800 Subject: [PATCH 2/3] all files complete --- lab-zachary/README.md | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lab-zachary/README.md b/lab-zachary/README.md index e69de29..4016658 100644 --- a/lab-zachary/README.md +++ b/lab-zachary/README.md @@ -0,0 +1,45 @@ +# Vanilla Javascript API router w/File Server persistance + +This app creates an HTTP server that handles GET, POST, and DELETE to a server-level persistance layer. + +# System Requirements + + - Terminal.app on macOS or equivalent + - node.js and npm package manager installed + + +### Installation + +Clone the repository to your local server +```sh +https://github.com/zcrumbo/09-vanilla_rest_api_persistence.git +``` + +Install the dependencies - + +```sh +$ npm i +``` +[HTTPie](https://httpie.org/) will be required to run the HTTP requests from your terminal window. You will need to install this with [Homebrew][1] on macOS. It is also easier to see the results of all operations by running mocha tests with the command +```sh +$ mocha +``` + +Start the server + +```sh +$ node server.js +``` + + +### Connecting + +If you are using HTTPie, in your terminal window, type the following commands, where '3000' would be replaced with your local environment PORT variable, if configured. Commands can only be sent to the api/bike endpoint +```sh +$ http POST :3000/api/bike name='test name' content='test content' #creates a new bike object and writes it to the fileserver, and returns a unique id +$ http GET localhost:8000/api/bike?id=sample-id #returns the name and content of a stored bike object +$ DELETE localhost:8000/api/bike?id=sample-id #deletes the bike file from server storage +``` + +[1]:https://brew.sh/ + From 5aacbabb8c6f081c2b945de167de73097b534e5d Mon Sep 17 00:00:00 2001 From: Zachary Crumbo Date: Sun, 26 Feb 2017 16:46:13 -0800 Subject: [PATCH 3/3] added bonus functionality to retrieve all ids when bike endpoint is hit, but no id is provided, and updated test to confirm --- .../1b8d9b7e-cafa-4add-9d5e-5a95ba0bec31.json | 1 + .../ba3e0125-7652-4b19-8a3f-957789138f5b.json | 1 + .../d6109482-cd7e-45ea-9eae-44343753edc1.json | 1 + lab-zachary/lib/storage.js | 17 +++++++++++++++++ lab-zachary/routes/bike-route.js | 8 +++++++- lab-zachary/test/bike-route-test.js | 9 +++++---- 6 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 lab-zachary/data/bike/1b8d9b7e-cafa-4add-9d5e-5a95ba0bec31.json create mode 100644 lab-zachary/data/bike/ba3e0125-7652-4b19-8a3f-957789138f5b.json create mode 100644 lab-zachary/data/bike/d6109482-cd7e-45ea-9eae-44343753edc1.json diff --git a/lab-zachary/data/bike/1b8d9b7e-cafa-4add-9d5e-5a95ba0bec31.json b/lab-zachary/data/bike/1b8d9b7e-cafa-4add-9d5e-5a95ba0bec31.json new file mode 100644 index 0000000..c43eecc --- /dev/null +++ b/lab-zachary/data/bike/1b8d9b7e-cafa-4add-9d5e-5a95ba0bec31.json @@ -0,0 +1 @@ +{"id":"1b8d9b7e-cafa-4add-9d5e-5a95ba0bec31","name":"test name","content":"test content"} \ No newline at end of file diff --git a/lab-zachary/data/bike/ba3e0125-7652-4b19-8a3f-957789138f5b.json b/lab-zachary/data/bike/ba3e0125-7652-4b19-8a3f-957789138f5b.json new file mode 100644 index 0000000..5c4f367 --- /dev/null +++ b/lab-zachary/data/bike/ba3e0125-7652-4b19-8a3f-957789138f5b.json @@ -0,0 +1 @@ +{"id":"ba3e0125-7652-4b19-8a3f-957789138f5b","name":"test name","content":"test content"} \ No newline at end of file diff --git a/lab-zachary/data/bike/d6109482-cd7e-45ea-9eae-44343753edc1.json b/lab-zachary/data/bike/d6109482-cd7e-45ea-9eae-44343753edc1.json new file mode 100644 index 0000000..7e05384 --- /dev/null +++ b/lab-zachary/data/bike/d6109482-cd7e-45ea-9eae-44343753edc1.json @@ -0,0 +1 @@ +{"id":"d6109482-cd7e-45ea-9eae-44343753edc1","name":"test name","content":"test content"} \ No newline at end of file diff --git a/lab-zachary/lib/storage.js b/lab-zachary/lib/storage.js index f5174b3..ebcd486 100644 --- a/lab-zachary/lib/storage.js +++ b/lab-zachary/lib/storage.js @@ -33,6 +33,23 @@ exports.fetchItem = function(schemaName, id){ .catch (err => Promise.reject(err)); }; +exports.fetchAllItems = function(schemaName){ + if(!schemaName) return Promise.reject(new Error('expected schema name')); + + return fs.readdirProm(`${__dirname}/../data/bike`) + .then( data => { + try{ + let items = {}; + for (var key of data.keys()) { + items[key] = data[key]; + } + return data; + } 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')); diff --git a/lab-zachary/routes/bike-route.js b/lab-zachary/routes/bike-route.js index 9001705..30a9a8d 100644 --- a/lab-zachary/routes/bike-route.js +++ b/lab-zachary/routes/bike-route.js @@ -18,7 +18,13 @@ module.exports = function (router){ }); return; } - writeResponse.sendText(res, 400, 'bad request'); + storage.fetchAllItems('bike') + .then(items => { + writeResponse.sendJSON(res, 200, items); + }).catch( err => { + console.error(err); + writeResponse.sendText(res, 404, 'not found'); + }); }); router.post(`/api/${itemType}`, function(req, res){ diff --git a/lab-zachary/test/bike-route-test.js b/lab-zachary/test/bike-route-test.js index 3fe90f0..41e7340 100644 --- a/lab-zachary/test/bike-route-test.js +++ b/lab-zachary/test/bike-route-test.js @@ -4,7 +4,7 @@ const request = require ('superagent'); const expect = require('chai').expect; const open = require('fs').openSync; -require('../server.js') +require('../server.js'); describe('Bike Route Test', function(){ var bike = ''; @@ -50,11 +50,12 @@ describe('Bike Route Test', function(){ done(); }); }); - it('should respond w/ \'bad request\' if no id provided', function(done){ + it('should respond w/ all ids if no id provided', function(done){ request.get('localhost:8000/api/bike?id=') .end((err, res) => { - expect(err.message).to.equal('Bad Request'); - expect(res.status).to.equal(400); + if (err) return done(err); + expect(res.body.some(e => e ===`${bike.body.id}.json`)).to.equal(true); + expect(res.status).to.equal(200); done(); }); });