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();
+ });
+ });
+ });
+
+ });
+ });
+});