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..de8c691 --- /dev/null +++ b/lab-zachary/README.md @@ -0,0 +1,58 @@ +# Express Single Resource API router w/File Server persistance + +This app creates an HTTP server that handles GET, POST, and DELETE to a filesystem-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/11-single_resource_express_api/tree/lab-zachary +``` + +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 +``` +If you want to use the debug and nodemon modules, run the npm script: +``` +npm start +``` + +### 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/sample-id #returns the name and content of a stored bike object +$ DELETE localhost:8000/api/bike/sample-id #deletes the bike file from server storage +``` + +Sending the following requests to the server will have the results below: + + * `GET`: 404 response with 'not found' for valid requests made with an id that was not found + * `GET`: 200 response with an array of all ids if no id was provided in the request + * `GET`: 200 response with a response body for a request made with a valid id + * `POST`: 400 response with 'bad request' if no request body was provided or the body was invalid + * `POST`: 200 response with the body content for a post request with a valid body + * `PUT`: 200 response with body content if a put request was made with an valid id and properly formatted body content + * `PUT`: 400 response with 'bad request' if missing or malformed body data was passed. +[1]:https://brew.sh/ + diff --git a/lab-zachary/gulpfile.js b/lab-zachary/gulpfile.js new file mode 100644 index 0000000..d66519f --- /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', 'test']); +}); + +gulp.task('default', ['dev']); diff --git a/lab-zachary/lib/cors-middleware.js b/lab-zachary/lib/cors-middleware.js new file mode 100644 index 0000000..dd328d7 --- /dev/null +++ b/lab-zachary/lib/cors-middleware.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = function(req, res, next){ + res.append('Access-Control-Allow-Origin', '*'); + res.append('Access-Control-Allow-Headers', '*'); + next(); +}; \ No newline at end of file diff --git a/lab-zachary/lib/error-middleware.js b/lab-zachary/lib/error-middleware.js new file mode 100644 index 0000000..a560650 --- /dev/null +++ b/lab-zachary/lib/error-middleware.js @@ -0,0 +1,21 @@ +'use strict'; + +const createError = require('http-errors'); +const debug = require('debug')('bike:error-middleware'); + +module.exports = function(err, req, res, next){ + console.error(err.message); + + if (err.status){ + debug('user error'); + + res.status(err.status).send(err.name); + next(); + return; + } + debug('server error'); //last error handler in app - set generic 500 + + err = createError(500, err.message); + res.status(err.status).send(err.name); + next(); +}; \ 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..e4ad06b --- /dev/null +++ b/lab-zachary/lib/storage.js @@ -0,0 +1,73 @@ +'use strict'; + +const Promise = require('bluebird'); +const createError = require('http-errors'); +const debug = require('debug')('bike:storage'); +const fs = Promise.promisifyAll(require('fs'), {suffix: 'Prom'}); + +//create, fetch delete + +module.exports = exports = {}; + +exports.createItem = function(schemaName, item){ + debug('createItem'); + + if (!schemaName) return Promise.reject(createError(400, 'expected schema name')); + if (!item) return Promise.reject(createError(400, '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(createError(500, err.message))); +}; + +exports.fetchItem = function(schemaName, id){ + debug('fetchItem'); + + if(!schemaName) return Promise.reject(createError(400, 'expected schema name')); + if(!id) return Promise.reject(createError(400, '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(createError(500, err.message)); + } + }) + .catch (err => Promise.reject(createError(404, err.message))); +}; + +exports.fetchAllItems = function(schemaName){ + if(!schemaName) return Promise.reject(new Error('expected schema name')); + + return fs.readdirProm(`${__dirname}/../data/${schemaName}`) + .then( data => {return data;}) + .catch (err => Promise.reject(createError(404, err.message))); +}; +exports.deleteItem1 = function(schemaName, id){ + if(!schemaName) return Promise.reject(createError(400,'expected schema name')); + if (!id) return Promise.reject(createError(400, 'expected id')); + + return fs.unlinkProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .then( () => id) + .catch( err => Promise.reject(err)); +}; +exports.deleteItem = function(schemaName, id){ + debug ('deleteItem'); + + if(!schemaName) return Promise.reject(createError(400, 'expected schema name')); + if(!id) return Promise.reject(createError(400, 'expected id')); + + return fs.unlinkProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .catch( err => Promise.reject(createError(404, err.message)));//then not necessary +}; + +exports.availableIDs = function(schemaName) { + return fs.readdirProm(`${__dirname}/../data/${schemaName}`) + .then( files => files.map (name => name.split('.json')[0])) + .catch( err => Promise.reject(createError(404, err.message))); +}; + + diff --git a/lab-zachary/model/bike.js b/lab-zachary/model/bike.js new file mode 100644 index 0000000..5109f32 --- /dev/null +++ b/lab-zachary/model/bike.js @@ -0,0 +1,59 @@ +'use strict'; + +const uuid = require('node-uuid'); +const createError = require('http-errors'); +const debug = require('debug')('bike:bike-model'); + +const storage = require('../lib/storage.js'); + + +const Bike = module.exports = function(name, content) { + debug('Bike constructor'); + + if(!name) throw createError(400, 'name required'); + if(!content) throw createError(400, 'description required'); + + this.id = uuid.v4(); + this.name = name; + this.description = content; +}; + +Bike.createBike = function(_bike){ + debug ('createBike'); + + try { + let bike = new Bike(_bike.name, _bike.description); + return storage.createItem('bike', bike); + } catch (err) { + return Promise.reject(err); + } +}; + +Bike.fetchBike = function(id){ + debug ('fetchBike'); + + return storage.fetchItem('bike', id); +}; + +Bike.deleteBike = function(id){ + debug ('deleteBike'); + + return storage.deleteItem('bike', id); +}; + +Bike.fetchAllBikes = function(){ + debug ('fetchAllBikes'); + + return storage.availableIDs('bike'); +}; + +Bike.updateBike = function(id, _bike) { + debug('updateBike'); + + return storage.fetchItem('bike', id) + .then( bike =>{ + if(_bike.name) bike.name = _bike.name; + if(_bike.description) bike.description = _bike.description; + return storage.createItem('bike', bike); + }).catch( err => Promise.reject(createError(400, err.message))); +}; \ No newline at end of file diff --git a/lab-zachary/package.json b/lab-zachary/package.json new file mode 100644 index 0000000..804ffdf --- /dev/null +++ b/lab-zachary/package.json @@ -0,0 +1,31 @@ +{ + "name": "lab-zachary", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": { + "bluebird": "^3.4.7", + "body-parser": "^1.16.1", + "debug": "^2.6.1", + "express": "^4.14.1", + "http-errors": "^1.6.1", + "morgan": "^1.8.1", + "mkdirp": "^0.5.1", + "node-uuid": "^1.4.7" + }, + "devDependencies": { + "chai": "^3.5.0", + "gulp": "^3.9.1", + "gulp-eslint": "^3.0.1", + "gulp-mocha": "^4.0.1", + "mocha": "^3.2.0", + "superagent": "^3.5.0" + }, + "scripts": { + "test": "DEBUG='bike*' mocha", + "start": "DEBUG='bike*' nodemon server.js" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/lab-zachary/route/bike-route.js b/lab-zachary/route/bike-route.js new file mode 100644 index 0000000..40866ad --- /dev/null +++ b/lab-zachary/route/bike-route.js @@ -0,0 +1,50 @@ +'use strict'; + +const Router = require('express').Router; +const jsonParser = require('body-parser').json(); +const debug = require('debug')('bike:bike-route'); +const Bike = require('../model/bike.js'); + +const bikeRouter = new Router(); + +bikeRouter.post('/api/bike', jsonParser, function(req, res, next){ + debug('bikeRouter POST route', req.body); + + Bike.createBike(req.body) + .then( bike => res.json(bike)) + .catch( err => next(err)); +}); + +bikeRouter.get('/api/bike/:bikeId', function(req, res, next){ + debug('bikeRouter GET route'); + + Bike.fetchBike(req.params.bikeId) + .then( bike => res.json(bike)) + .catch( err => next(err)); +}); + +bikeRouter.delete('/api/bike/:bikeId', function( req, res, next){ + debug( 'bikeRouter DELETE route'); + + Bike.deleteBike(req.params.bikeId) + .then( () => res.sendStatus(204)) + .catch( err => next(err)); +}); + +bikeRouter.get('/api/bike(/)?', function(req, res, next){ + debug( 'bikeRouter GET all route'); + + Bike.fetchAllBikes() + .then( bikes => res.json(bikes)) + .catch( err => next(err)); +}); + +bikeRouter.put('/api/bike/:bikeId', jsonParser, function(req, res, next){ + debug ('bikeRouter PUT route'); + + Bike.updateBike(req.params.bikeId, req.body) + .then( bike => res.json(bike)) + .catch( err => next(err)); +}); + +module.exports = bikeRouter; diff --git a/lab-zachary/server.js b/lab-zachary/server.js new file mode 100644 index 0000000..201dcc2 --- /dev/null +++ b/lab-zachary/server.js @@ -0,0 +1,22 @@ +'use strict'; + +const express = require('express'); +const morgan = require('morgan'); +const debug = require('debug')('bike:server'); + +const cors = require('./lib/cors-middleware.js'); +const errorMiddleware = require('./lib/error-middleware.js'); +const bikeRouter = require('./route/bike-route.js'); + +const app = express(); + +const PORT = process.env.PORT || 3000; + +app.use(morgan('dev')); +app.use(cors); +app.use(bikeRouter); +app.use(errorMiddleware); + +app.listen(PORT, () => { + debug('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..095a3ae --- /dev/null +++ b/lab-zachary/test/bike-route-test.js @@ -0,0 +1,135 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; +const Bike = require('../model/bike.js'); + +const url = 'http://localhost:8000/api/bike'; +require ('../server.js'); + +const sampleBike = { + 'name': 'sample name', + 'description': 'sample description' +}; + +describe('Bike Routes', function(){ + describe('get api/bike', function(){ + before( done => { + Bike.createBike(sampleBike) + .then( bike => { + this.tempBike = bike; + done(); + }) + .catch( err => done(err)); + }); + + after( done => { + Bike.deleteBike(this.tempBike.id) + .then( () => done()) + .catch( err => done(err)); + }); + it('should return with a 200 response if proper id is passed', done => { + request.get(`${url}/${this.tempBike.id}`) + .end((err, res) =>{ + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal(sampleBike.name); + expect(res.body.description).to.equal(sampleBike.description); + done(); + }); + }); + it('should return all ids if no id param is passed', done => { + request.get(`${url}/`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body).to.contain(this.tempBike.id); + done(); + }); + }); + it('should return a 404 error if a bad id name is passed', done => { + request.get(`${url}/thisisabadidname`) + .end((err, res) => { + expect(res.status).to.equal(404); + expect(err.message).to.equal('Not Found'); + done(); + }); + }); + }); + describe('PUT api/bike', function(){ + var newBikeContent = {'name':'updated name', 'description':'updated description'}; + before( done => { + Bike.createBike(sampleBike) + .then( bike => { + this.tempBike = bike; + done(); + }) + .catch( err => done(err)); + }); + + after( done => { + Bike.deleteBike(this.tempBike.id) + .then( () => done()) + .catch( err => done(err)); + }); + it('should update note content with proper request', done => { + request.put(`${url}/${this.tempBike.id}`) + .send(newBikeContent) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal(newBikeContent.name); + expect(res.body.description).to.equal(newBikeContent.description); + done(); + }); + }); + it('should return a 400 with an incorrect id', done => { + request.put(`${url}/badIdPetey`) + .send(newBikeContent) + .end((err, res) => { + expect(err.message).to.equal('Bad Request'); + expect(res.status).to.equal(400); + done(); + }); + }); + it('should return a 404 with a missing id', done => { + request.put(`${url}/`) + .send(newBikeContent) + .end((err, res) => { + expect(err.message).to.equal('Not Found'); + expect(res.status).to.equal(404); + done(); + }); + }); + }); + describe('POST route test', function() { + after( done => { + if (this.tempBike) { + Bike.deleteBike(this.tempBike.id) + .then( () => done()) + .catch( err => done(err)); + } + }); + it('should create a new note', done => { + request.post(url) + .send(sampleBike) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal(sampleBike.name); + expect(res.body.description).to.equal(sampleBike.description); + this.tempBike = res.body; + done(); + }); + }); + it('should respond with a 400 error if malformed body content is passed', done => { + request.post(url) + .send('bad data') + .end((err, res) => { + expect(res.status).to.equal(400); + expect(err.message).to.equal('Bad Request'); + done(); + }); + }); + }); +});