diff --git a/lab-megan/.eslintrc b/lab-megan/.eslintrc new file mode 100644 index 0000000..8dc6807 --- /dev/null +++ b/lab-megan/.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-megan/.gitignore b/lab-megan/.gitignore new file mode 100644 index 0000000..04d4cc4 --- /dev/null +++ b/lab-megan/.gitignore @@ -0,0 +1,147 @@ +Skip to content +This repository +Search +Pull requests +Issues +Gist + @meganreardon + Watch 5 + Star 1 + Fork 3 codefellows/seattle-javascript-401d12 + Code Issues 0 Pull requests 0 Projects 0 Wiki Pulse Graphs +Branch: master Find file Copy pathseattle-javascript-401d12/02-build_automation_and_dependency_management/lecture/demo/hello-world-gulp/.gitignore +b2d8bee 41 minutes ago +@bnates bnates added lecture 2 material +1 contributor +RawBlameHistory +128 lines (94 sloc) 1.85 KB +# Created by https://www.gitignore.io/api/node,vim,macos,linux,windows + +node_modules/ + +### Node ### +# Logs +logs +*.log +npm-debug.log* +data + +# 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 + + +### 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 + + +### 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* + + +### 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 +Contact GitHub API Training Shop Blog About +© 2016 GitHub, Inc. Terms Privacy Security Status Help diff --git a/lab-megan/README.md b/lab-megan/README.md new file mode 100644 index 0000000..cd20c68 --- /dev/null +++ b/lab-megan/README.md @@ -0,0 +1,44 @@ +### ABOUT THIS PROJECT + +This is a simple Express router built as part of the Code Fellows 401 JavaScript class. It uses Express to handle middleware and routing, in this case I'm keeping track of hats with a color and style. + +### HOW TO GET THE API RUNNING + +Clone this repository. +```JavaScript +cd lab-megan + +npm i +``` +To get needed Node dependencies. + +In one terminal window: +```JavaScript +nmp run start +``` + +### HOW TO USE THE API + +With this API you can create, view and delete the records of various hat. To do so get the server running in a terminal window and open a second terminal window and do the following + +- To create a hat +`http POST localhost:3000/api/hat color='' style=''` +This will return your color, style and a unique id. + +Note: If your server shows it is running on a different port please use that one instead. + + +- To view the record of a hat +`http localhost:3000/api/hat?id=` +This will return the color and style of the requested id. + +- To delete the record of a hat +`http DELETE localchost:3000/api/hat?id=` +This will return a 204 message to confirm any record of the had has been deleted. + +### HOW TO INCLUDE IN YOUR PROJECT + +```JavaScript +npm i -D chai mocha superagent +npm run test +``` diff --git a/lab-megan/gulpfile.js b/lab-megan/gulpfile.js new file mode 100644 index 0000000..93ef0cd --- /dev/null +++ b/lab-megan/gulpfile.js @@ -0,0 +1,24 @@ + +'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-megan/lib/cors-middleware.js b/lab-megan/lib/cors-middleware.js new file mode 100644 index 0000000..6661797 --- /dev/null +++ b/lab-megan/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(); +}; diff --git a/lab-megan/lib/error-middleware.js b/lab-megan/lib/error-middleware.js new file mode 100644 index 0000000..7efa009 --- /dev/null +++ b/lab-megan/lib/error-middleware.js @@ -0,0 +1,21 @@ +'use strict'; + +const createError = require('http-errors'); +const debug = require('debug')('hat: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'); + err = createError(500, err.message); + res.status(err.status).send(err.name); + next(); +}; diff --git a/lab-megan/lib/storage.js b/lab-megan/lib/storage.js new file mode 100644 index 0000000..a29eaa5 --- /dev/null +++ b/lab-megan/lib/storage.js @@ -0,0 +1,58 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), {suffix: 'Prom'}); +const createError = require('http-errors'); +const debug = require('debug')('hat:storage'); + +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) + .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.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))); +}; + +exports.availIDs = function(schemaName) { + debug('availIDs'); // NOTE: I added this line + + if (!schemaName) return Promise.reject(createError(400, 'expected schema name')); // NOTE: I added this line + + return fs.readdirProm(`${__dirname}/../data/${schemaName}`) + .then ( files => files.map(color => color.split('.json')[0])) + .catch( err => Promise.reject(createError(404, err.message))); +}; diff --git a/lab-megan/model/hat.js b/lab-megan/model/hat.js new file mode 100644 index 0000000..50d5f0a --- /dev/null +++ b/lab-megan/model/hat.js @@ -0,0 +1,57 @@ +'use strict'; + +const uuid = require('node-uuid'); +const createError = require('http-errors'); +const debug = require('debug')('hat:hat'); +const storage = require('../lib/storage.js'); + +const Hat = module.exports = function(color, style) { + debug('hat constructor'); + + if (!color) throw createError(400, 'expected color'); + if (!style) throw createError(400, 'expected style'); + + this.id = uuid.v1(); + this.color = color; + this.style = style; +}; + +Hat.createHat = function(_hat) { + debug('createHat'); + + try { + let hat = new Hat(_hat.color, _hat.style); + return storage.createItem('hat', hat); + } catch (err) { + return Promise.reject(createError(400, 'err.message')); + } +}; + +Hat.fetchHat = function(id) { + debug('fetchHat'); + return storage.fetchItem('hat', id); +}; + +Hat.updateHat = function(id, _hat) { + debug('updateHat'); + + return storage.fetchItem('hat', id) + .catch( err => Promise.reject(createError(404, err.message))) + .then( hat => { + for (var prop in hat) { + if (prop === 'id') continue; + if (_hat[prop]) hat[prop] = _hat[prop]; + } + return storage.createItem('hat', hat); + }); +}; + +Hat.deleteHat = function(id) { + debug('deleteHat'); + return storage.deleteItem('hat', id); +}; + +Hat.fetchIDs = function() { + debug('fetchIDs'); + return storage.availIDs('hat'); +}; diff --git a/lab-megan/package.json b/lab-megan/package.json new file mode 100644 index 0000000..7285805 --- /dev/null +++ b/lab-megan/package.json @@ -0,0 +1,30 @@ +{ + "name": "lab-megan", + "version": "1.0.0", + "description": "", + "main": "gulpfile.js", + "directories": { + "test": "test" + }, + "scripts": { + "start": "DEBUG='hat*' node server.js", + "test": "DEBUG='hat*' mocha" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "bluebird": "^3.4.6", + "body-parser": "^1.15.2", + "debug": "^2.4.5", + "express": "^4.14.0", + "http-errors": "^1.5.1", + "morgan": "^1.7.0", + "node-uuid": "^1.4.7" + }, + "devDependencies": { + "chai": "^3.5.0", + "mocha": "^3.2.0", + "superagent": "^3.3.1" + } +} diff --git a/lab-megan/route/hat-router.js b/lab-megan/route/hat-router.js new file mode 100644 index 0000000..ab3ad92 --- /dev/null +++ b/lab-megan/route/hat-router.js @@ -0,0 +1,41 @@ +'use strict'; + +const Router = require('express').Router; +const jsonParser = require('body-parser').json(); +const debug = require('debug')('hat:hat-router'); +const Hat = require('../model/hat.js'); +const hatRouter = new Router(); + +hatRouter.post('/api/hat', jsonParser, function(req, res, next) { + debug('POST: /api/hat'); + + Hat.createHat(req.body) + .then( hat => res.json(hat)) + .catch( err => next(err)); +}); + +hatRouter.get('/api/hat/:id', function(req, res, next) { + debug('GET: /api/hat/:id'); + + Hat.fetchHat(req.params.id) + .then( hat => res.json(hat)) + .catch( err => next(err)); +}); + +hatRouter.get('/api/hat', function(req, res, next) { + debug('GET: /api/hat'); + + Hat.fetchIDs() + .then( ids => res.json(ids)) + .catch(next); +}); + +hatRouter.put('/api/hat', jsonParser, function(req, res, next) { + debug('PUT: /api/hat'); + + Hat.updateHat(req.query.id, req.body) + .then( hat => res.json(hat)) + .catch(next); +}); + +module.exports = hatRouter; diff --git a/lab-megan/server.js b/lab-megan/server.js new file mode 100644 index 0000000..e8ab634 --- /dev/null +++ b/lab-megan/server.js @@ -0,0 +1,21 @@ +'use strict'; + +const morgan = require('morgan'); +const express = require('express'); +const createError = require('http-errors'); +const cors = require('./lib/cors-middleware.js'); +const errors = require('./lib/error-middleware.js'); +const debug = require('debug')('hat:server'); +const hatRouter = require('./route/hat-router.js'); + +const PORT = process.env.PORT || 3000; +const app = express(); + +app.use(morgan('dev')); +app.use(cors); +app.use(hatRouter); +app.use(errors); + +app.listen(PORT, () => { + console.log(`server up at: ${PORT}`); +}); diff --git a/lab-megan/test/hat-route-test.js b/lab-megan/test/hat-route-test.js new file mode 100644 index 0000000..b61412a --- /dev/null +++ b/lab-megan/test/hat-route-test.js @@ -0,0 +1,189 @@ +'use strict'; + +const expect = require('chai').expect; +const request = require('superagent'); +const Hat = require('../model/hat.js'); +const url = 'http://localhost:3000'; + +require('../server.js'); + +const exampleHat = { + color: 'example color', + style: 'example style' +}; + +describe('Hat Routes', function() { + +// --------- +// GET tests +// --------- + + describe('GET: /api/hat', function() { + describe('with a valid id', function() { + before( done => { + Hat.createHat(exampleHat) + .then(hat => { + this.tempHat = hat; + done(); + }) + .catch( err => done(err)); + }); + + after( done => { + Hat.deleteHat(this.tempHat.id) + .then( ()=> done()) + .catch( err => done(err)); + }); + + it('should return a hat', done => { + request.get(`${url}/api/hat/${this.tempHat.id}`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.id).to.equal(this.tempHat.id); + expect(res.body.color).to.equal(this.tempHat.color); + expect(res.body.style).to.equal(this.tempHat.style); + done(); + }); + }); + + describe('with an invalid id', function() { + it('should respond with a 404 status code', done => { + request.get(`${url}/api/hat/123456789`) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with an invalid path', function() { + it('should respond with a 404 status code', done => { + request.get(`${url}/api/boots/123456789`) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + }); + }); + +// ---------- +// POST tests +// ---------- + + describe('POST: /api/hat', function() { + describe('with a valid body', function() { + after( done => { + if (this.tempHat) { + Hat.deleteHat(this.tempHat.id) + .then( ()=> done()) + .catch( err => done(err)); + } + }); + + it('should return a hat', done => { + request.post(`${url}/api/hat`) + .send(exampleHat) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.color).to.equal(exampleHat.color); + expect(res.body.style).to.equal(exampleHat.style); + this.tempHat = res.body; + done(); + }); + }); + + describe('with no content', function() { + it('should respond with a 400 status code', done => { + request.post(`${url}/api/hat`) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.text).to.include('BadRequestError'); + done(); + }); + }); + }); + + describe('with an invalid path', function() { + it('should respond with 404 error code', done => { + request.post(`${url}/api/boots`) + .send(exampleHat) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + }); + }); + +// --------- +// PUT tests +// --------- + + describe('PUT: /api/hat', function() { + describe('with a valid id and body', function() { + before( done => { + Hat.createHat(exampleHat) + .then( hat => { + this.tempHat = hat; + done(); + }) + .catch( err => done(err)); + }); + + after( done => { + if (this.tempHat) { + Hat.deleteHat(this.tempHat.id) + .then( ()=> done()) + .catch(done); + } + }); + + it('should return a hat', done => { + let updateHat = { color: 'new color', style: 'new style' }; + request.put(`${url}/api/hat?id=${this.tempHat.id}`) + .send(updateHat) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.id).to.equal(this.tempHat.id); + for (var prop in updateHat) { + expect(res.body[prop]).to.equal(updateHat[prop]); + } + done(); + }); + }); + + describe('with an invalid id', function() { + it('should respond with a 404 status code', done => { + let updateHat = { color: 'new color', style: 'new style' }; + request.put(`${url}/api/hat?id=123456789`) + .send(updateHat) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with an invalid path', function() { + it('should respond with 404 error code', done => { + let updateHat = { color: 'new color', style: 'new style' }; + request.put(`${url}/api/boots`) + .send(updateHat) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + }); + }); +});