diff --git a/README.md b/README.md index 27b130b..15a3e89 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,11 @@ -![CF](https://camo.githubusercontent.com/70edab54bba80edb7493cad3135e9606781cbb6b/687474703a2f2f692e696d6775722e636f6d2f377635415363382e706e67) Lab 11: Single Resource Express API +Single Resource Express API === -## To Submit this Assignment - * Fork this repository - * Write all of your code in a directory named `lab-` + `` **e.g.** `lab-brian` - * Push to your repository - * Submit a pull request to this repository - * Submit a link to your PR in canvas - * Write a question and observation on canvas - -## Include - * `package.json` - * `.eslintrc` - * `gulpfile.js` - * `.gitignore` - * `README.md` - * your `README.md` should include detailed instructions on how to use your API - ## Description - * Create an HTTP server using `express` - * Create a object constructor that creates a _simple resource_ with at least 3 properties - * it can **not** have the same properties as the in-class sample code (other than the `id`) - * a unique `id` property should be included *(node-uuid)* - * include two additional properties of your choice - * Use the JSON parser included with the `body-parser` module as a middleware component to parse the request body on `POST` and `PUT` routes - * Use the npm `debug` module to log the methods in your application - * Create an `npm` script to automate the `debug` process and start the server - * Persist your API data using the storage module and file system persistence + * Utilizes an `express` server, one resource, and various third party tools to route and handle requests and allow data to persist on the file system. ## Server Endpoints - * **`/api/simple-resource-name`** + * **`/api/neighbor`** * `POST` request * pass data as stringifed JSON in the body of a **POST** request to create a new resource * `GET` request @@ -37,12 +13,3 @@ * `DELETE` request * pass `?id=` in the query string to **DELETE** a specific resource * this should return a 204 status code with no content in the body - -## Tests - * write a test to ensure that your api returns a status code of 404 for routes that have not been registered - * write tests to ensure the `/api/simple-resource-name` endpoint responds as described for each condition below: - * `GET`: test 404, it should respond with 'not found' for valid requests made with an id that was not found - * `GET`: test 400, it should respond with 'bad request' if no id was provided in the request - * `GET`: test 200, it should contain a response body for a request made with a valid id - * `POST`: test 400, it should respond with 'bad request' if no request body was provided or the body was invalid - * `POST`: test 200, it should respond with the body content for a post request with a valid body diff --git a/lab-regan/.eslintrc b/lab-regan/.eslintrc new file mode 100644 index 0000000..f4ed946 --- /dev/null +++ b/lab-regan/.eslintrc @@ -0,0 +1,19 @@ +{ + "rules": { + "no-console": "off", + "indent": [ "error", 2 ], + "quotes": [ "error", "single" ], + "semi": ["error", "always"], + "linebreak-style": [ "error", "unix" ] + }, + "env": { + "es6": true, + "node": true, + }, + "ecmaFeatures": { + "modules": true, + "experimentalObjectRestSpread": true, + "impliedStrict": true + }, + "extends": "eslint:recommended" +} diff --git a/lab-regan/.gitignore b/lab-regan/.gitignore new file mode 100644 index 0000000..3b9f04a --- /dev/null +++ b/lab-regan/.gitignore @@ -0,0 +1,132 @@ + +# Created by https://www.gitignore.io/api/osx,node,linux,windows + +### 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* + +###Local Files### +data/ + +### 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/osx,node,linux,windows diff --git a/lab-regan/README.md b/lab-regan/README.md new file mode 100644 index 0000000..15a3e89 --- /dev/null +++ b/lab-regan/README.md @@ -0,0 +1,15 @@ +Single Resource Express API +=== + +## Description + * Utilizes an `express` server, one resource, and various third party tools to route and handle requests and allow data to persist on the file system. + +## Server Endpoints + * **`/api/neighbor`** + * `POST` request + * pass data as stringifed JSON in the body of a **POST** request to create a new resource + * `GET` request + * pass `?id=` as a query string parameter to retrieve a specific resource (as JSON) + * `DELETE` request + * pass `?id=` in the query string to **DELETE** a specific resource + * this should return a 204 status code with no content in the body diff --git a/lab-regan/gulpfile.js b/lab-regan/gulpfile.js new file mode 100644 index 0000000..c36afa2 --- /dev/null +++ b/lab-regan/gulpfile.js @@ -0,0 +1,13 @@ +'use strict'; + +const gulp = require('gulp'); +const eslint = require('gulp-eslint'); + +gulp.task('lint', function(){ + return gulp.src(['**/*.js', '!node_modules']) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +gulp.task('default', ['lint']); diff --git a/lab-regan/lib/storage.js b/lab-regan/lib/storage.js new file mode 100644 index 0000000..d1aaa81 --- /dev/null +++ b/lab-regan/lib/storage.js @@ -0,0 +1,45 @@ +'use strict'; + +const Promise = require('bluebird'); +const createError = require('http-errors'); +const debug = require('debug')('neighbors:storage'); +const fs = Promise.promisifyAll(require('fs'), {suffix:'Prom'}); + +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 object')); + + 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))); +};//end createItem + + +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`) + .then( () => thang ) + .catch( err => Promise.reject(createError(500, err.message))); +};//end deleteItem + + +exports.fetchItem = function(schemaName, id){ + if(!schemaName) return Promise.reject(createError(400, 'expected schema')); + if(!id) return Promise.reject(createError(400, 'expected id')); + return fs.readFileProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .then( thing => { + try { + let item = JSON.parse(thing.toString()); + return item; + } catch (err) { + return Promise.reject(createError(500, err.message)); + } + }) + .catch( err => Promise.reject(createError(404, err.message))); +};//end fetchItem diff --git a/lab-regan/model/neighbor.js b/lab-regan/model/neighbor.js new file mode 100644 index 0000000..2337523 --- /dev/null +++ b/lab-regan/model/neighbor.js @@ -0,0 +1,40 @@ +'use strict'; + +const uuid = require('node-uuid'); +const createError = require('http-errors'); +const debug = require('debug')('neighbor:neighbor'); +const storage = require('../lib/storage.js'); + +const Neighbor = module.exports = function(name, age, friendly){ + debug('neighbor constructor'); + + if(!name) throw createError(400, 'expected name'); + if(!age) throw createError(400, 'expected age'); + if(!friendly) throw createError(400, 'expected friendly'); + + this.id = uuid.v1(); + this.name = name; + this.age = age; + this.friendly = friendly; +}; + +Neighbor.createNeighbor = function(_neighbor){ + debug('createNeighbor'); + try { + let neighbor = new Neighbor(_neighbor.name, _neighbor.age, _neighbor.friendly); + return storage.createItem('neighbor', neighbor); + } catch(err) { + return Promise.reject(err); + }; +};// end createNeighbor + +Neighbor.fetchNeighbor = function(id){ + debug('fetchNeighbor'); + return storage.fetchItem('neighbor', id); +};//end fetchNeighbor + + +Neighbor.deleteNeighbor = function(id){ + debug('deleteNeighbor'); + return storage.deleteItem('neighbor', id); +};//end deleteNeighbor diff --git a/lab-regan/package.json b/lab-regan/package.json new file mode 100644 index 0000000..3e4f9b0 --- /dev/null +++ b/lab-regan/package.json @@ -0,0 +1,25 @@ +{ + "name": "lab-regan", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "DEBUG='neighbor*' node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "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", + "node-uuid": "^1.4.7" + }, + "devDependencies": { + "chai": "^3.5.0", + "superagent": "^3.5.0" + } +} diff --git a/lab-regan/server.js b/lab-regan/server.js new file mode 100644 index 0000000..73ae844 --- /dev/null +++ b/lab-regan/server.js @@ -0,0 +1,73 @@ +'use strict'; + +const express = require('express'); +const morgan = require('morgan'); +const createError = require('http-errors'); +const debug = require('debug')('neighbor:server'); +const jsonParser = require('body-parser').json(); + +const PORT = process.env.PORT || 3000; +const app = express(); + +const storage = require('./lib/storage.js'); +const Neighbor = require('./model/neighbor.js'); + +app.use(morgan('dev')); + +app.get('/test', function(req,res){ + debug('GET: /test'); + res.json({msg: 'test route works'}); +});//end get test + +app.get('/api/neighbor/', function(req,res, next){ + debug('/api/neighbor/'); + storage.fetchItem(); + res.sendStatus(400); +}); + +app.get('/api/neighbor/:id', function(req,res, next) +{ + debug('GET: /api/neighbor/:id'); + Neighbor.fetchNeighbor(req.params.id) +.then( thing => res.json(thing)) +.catch( err => next(err)); +});//end app.get /api/neighbor + +app.post('/api/neighbor', jsonParser, function(req, res, next){ + debug('POST: /api/neighbor'); + Neighbor.createNeighbor(req.body) + .then( thing => res.json(thing)) + .catch( err => { + next(err) + }); +});//end app.post /api/neighbor + + +app.delete('/api/neighbor/:id', function(req, res, next) + { + debug('DELETE: /api/neighbor/:id'); + Neighbor.deleteNeighbor(req.params.id) + .then(thing => { + debug('then block of delete in server.js'); + res.sendStatus(204); + // res.end(); + }) + .catch(err => next(err)); +} +); + +app.use(function(err, req, res, next){ + debug('error middleware'); + console.error('you caused an error:', err.message); + + if(err.status){ + res.status(err.status).send(err.name); + return; + } + err = createError(500, err.message); + res.status(err.status).send(err.name); +});//end app.use error handling + +app.listen(PORT, () => { + debug(`Server is up on port: ${PORT}`); +}); diff --git a/lab-regan/test/neighbor-route-test.js b/lab-regan/test/neighbor-route-test.js new file mode 100644 index 0000000..0abf4d2 --- /dev/null +++ b/lab-regan/test/neighbor-route-test.js @@ -0,0 +1,71 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; + +require('../server.js'); + +describe('Neighbor Routes', function() { + + var neighbor = null; + describe('POST: /api/neighbor', function() { + it('should return a neighbor and 200', function(done) { + request.post('localhost:8000/api/neighbor') + .send({ name: 'test name', age: 'test age', friendly: 'yes' }) + .end((err, res) => { + if (err) console.log('nope'); + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('test name'); + expect(res.body.age).to.equal('test age'); + expect(res.body.friendly).to.equal('yes'); + neighbor = res.body; + done(); + }); + }); + }); + + describe('POST: 400 Bad Request', function() { + it('should return a 400', function(done) { + request.post('localhost:8000/api/neighbor') + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('GET: /api/neighbor', function() { + it('should return a neighbor', function(done) { + request.get(`localhost:8000/api/neighbor/${neighbor.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.age).to.equal('test age'); + expect(res.body.friendly).to.equal('yes'); + done(); + }); + }); + }); + + describe('GET: Not Found', function() { + it('should return a 404', function(done) { + request.get(`localhost:8000/api/neighbor/22edf8b0-fd79-11e6-a8de-zzbzz0e9316d`) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('GET: Bad Request', function() { + it('should return a 400', function(done) { + request.get(`localhost:8000/api/neighbor`) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); +});