diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md index feb511f..26896b8 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,10 @@ -![cf](https://i.imgur.com/7v5ASc8.png) 11: Single Resource Express API -====== +To use this API. -## Submission Instructions - * fork this repository & create a new branch for your work - * write all of your code in a directory named `lab-` + `` **e.g.** `lab-susan` - * 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 -## Learning Objectives -* students will be able to create a single resource API using the express framework -* students will be able to leverage 3rd party helper modules for debugging, logging, and handling errors +Open your terminal and run the server. (npm run start) -## Requirements +in a seperate pane, make a request, http :(port) / -#### Configuration -* `package.json` -* `.eslintrc` -* `.gitignore` -* `README.md` - * your `README.md` should include detailed instructions on how to use your API +That should get you a test entry. -#### Feature Tasks -* 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 - -#### Server Endpoints -* **`/api/simple-resource-name`** -* `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 - -#### 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 +to do a post, run the post style http server access to /api/hike. Same with put and delete. diff --git a/data/hike/18a57827-1eb2-47ef-8263-bdaa0b3403d8.json b/data/hike/18a57827-1eb2-47ef-8263-bdaa0b3403d8.json new file mode 100644 index 0000000..47bd27f --- /dev/null +++ b/data/hike/18a57827-1eb2-47ef-8263-bdaa0b3403d8.json @@ -0,0 +1 @@ +{"id":"18a57827-1eb2-47ef-8263-bdaa0b3403d8","name":"some cool hike","distance":"3.4 miles","difficulty":"medium"} \ No newline at end of file diff --git a/data/hike/258f0ab7-481a-4bf1-a3d1-2bd78911723e.json b/data/hike/258f0ab7-481a-4bf1-a3d1-2bd78911723e.json new file mode 100644 index 0000000..d72388c --- /dev/null +++ b/data/hike/258f0ab7-481a-4bf1-a3d1-2bd78911723e.json @@ -0,0 +1 @@ +{"id":"258f0ab7-481a-4bf1-a3d1-2bd78911723e","name":"some cool hike","distance":"3.4 miles","difficulty":"medium"} \ No newline at end of file diff --git a/data/hike/472db26c-18b5-4291-8684-7d397c269d7e.json b/data/hike/472db26c-18b5-4291-8684-7d397c269d7e.json new file mode 100644 index 0000000..8e79d16 --- /dev/null +++ b/data/hike/472db26c-18b5-4291-8684-7d397c269d7e.json @@ -0,0 +1 @@ +{"id":"472db26c-18b5-4291-8684-7d397c269d7e","name":"some cool hike","distance":"3.4 miles","difficulty":"medium"} \ No newline at end of file diff --git a/data/hike/57f40498-9319-4fca-8c08-f0301a930ab6.json b/data/hike/57f40498-9319-4fca-8c08-f0301a930ab6.json new file mode 100644 index 0000000..a23b9d2 --- /dev/null +++ b/data/hike/57f40498-9319-4fca-8c08-f0301a930ab6.json @@ -0,0 +1 @@ +{"id":"57f40498-9319-4fca-8c08-f0301a930ab6","name":"some cool hike","distance":"3.4 miles","difficulty":"medium"} \ No newline at end of file diff --git a/data/hike/5ea5a87c-96f9-4e31-9e77-acaa600f4c52.json b/data/hike/5ea5a87c-96f9-4e31-9e77-acaa600f4c52.json new file mode 100644 index 0000000..9f230d7 --- /dev/null +++ b/data/hike/5ea5a87c-96f9-4e31-9e77-acaa600f4c52.json @@ -0,0 +1 @@ +{"id":"5ea5a87c-96f9-4e31-9e77-acaa600f4c52","name":"some cool hike","distance":"3.4 miles","difficulty":"medium"} \ No newline at end of file diff --git a/data/hike/6069a76b-6e44-4535-8857-766848d6287c.json b/data/hike/6069a76b-6e44-4535-8857-766848d6287c.json new file mode 100644 index 0000000..57bae19 --- /dev/null +++ b/data/hike/6069a76b-6e44-4535-8857-766848d6287c.json @@ -0,0 +1 @@ +{"id":"6069a76b-6e44-4535-8857-766848d6287c","name":"some cool hike","distance":"3.4 miles","difficulty":"medium"} \ No newline at end of file diff --git a/data/hike/6cc4d2a4-73bb-40e4-bdc3-49c6306bf498.json b/data/hike/6cc4d2a4-73bb-40e4-bdc3-49c6306bf498.json new file mode 100644 index 0000000..7101fca --- /dev/null +++ b/data/hike/6cc4d2a4-73bb-40e4-bdc3-49c6306bf498.json @@ -0,0 +1 @@ +{"id":"6cc4d2a4-73bb-40e4-bdc3-49c6306bf498"} \ No newline at end of file diff --git a/data/hike/865cf4e6-13d0-440b-8489-811429ebeeb5.json b/data/hike/865cf4e6-13d0-440b-8489-811429ebeeb5.json new file mode 100644 index 0000000..0400f68 --- /dev/null +++ b/data/hike/865cf4e6-13d0-440b-8489-811429ebeeb5.json @@ -0,0 +1 @@ +{"id":"865cf4e6-13d0-440b-8489-811429ebeeb5","name":"some cool hike"} \ No newline at end of file diff --git a/data/hike/b1b26a34-6b89-4c80-846f-10a84c23aafc.json b/data/hike/b1b26a34-6b89-4c80-846f-10a84c23aafc.json new file mode 100644 index 0000000..b7515d3 --- /dev/null +++ b/data/hike/b1b26a34-6b89-4c80-846f-10a84c23aafc.json @@ -0,0 +1 @@ +{"id":"b1b26a34-6b89-4c80-846f-10a84c23aafc","name":"some cool hike"} \ No newline at end of file diff --git a/data/hike/c6b1f28c-5f65-4730-87e2-aa35bfd47284.json b/data/hike/c6b1f28c-5f65-4730-87e2-aa35bfd47284.json new file mode 100644 index 0000000..8c7a618 --- /dev/null +++ b/data/hike/c6b1f28c-5f65-4730-87e2-aa35bfd47284.json @@ -0,0 +1 @@ +{"id":"c6b1f28c-5f65-4730-87e2-aa35bfd47284","name":"some cool hike"} \ No newline at end of file diff --git a/data/hike/cb1a3bac-a6e8-4d5f-8627-9458b5f2e7c5.json b/data/hike/cb1a3bac-a6e8-4d5f-8627-9458b5f2e7c5.json new file mode 100644 index 0000000..2d1d07f --- /dev/null +++ b/data/hike/cb1a3bac-a6e8-4d5f-8627-9458b5f2e7c5.json @@ -0,0 +1 @@ +{"id":"cb1a3bac-a6e8-4d5f-8627-9458b5f2e7c5","name":"some cool hike","distance":"3.4 miles","difficulty":"medium"} \ No newline at end of file diff --git a/data/hike/ce5e9e53-9ce9-4970-b646-b6b94719574c.json b/data/hike/ce5e9e53-9ce9-4970-b646-b6b94719574c.json new file mode 100644 index 0000000..d33f801 --- /dev/null +++ b/data/hike/ce5e9e53-9ce9-4970-b646-b6b94719574c.json @@ -0,0 +1 @@ +{"id":"ce5e9e53-9ce9-4970-b646-b6b94719574c","name":"some cool hike"} \ No newline at end of file diff --git a/data/hike/de6172e8-fb67-4c1d-9dac-dfd1111ef564.json b/data/hike/de6172e8-fb67-4c1d-9dac-dfd1111ef564.json new file mode 100644 index 0000000..48af04f --- /dev/null +++ b/data/hike/de6172e8-fb67-4c1d-9dac-dfd1111ef564.json @@ -0,0 +1 @@ +{"id":"de6172e8-fb67-4c1d-9dac-dfd1111ef564","name":"some cool hike"} \ No newline at end of file diff --git a/data/hike/e389f25b-580e-462b-841d-c0e32b568ea5.json b/data/hike/e389f25b-580e-462b-841d-c0e32b568ea5.json new file mode 100644 index 0000000..0a31962 --- /dev/null +++ b/data/hike/e389f25b-580e-462b-841d-c0e32b568ea5.json @@ -0,0 +1 @@ +{"id":"e389f25b-580e-462b-841d-c0e32b568ea5","name":"some cool hike","distance":"3.4 miles","difficulty":"medium"} \ No newline at end of file diff --git a/data/hike/e9388170-1a54-4df2-a906-a5e1583f148e.json b/data/hike/e9388170-1a54-4df2-a906-a5e1583f148e.json new file mode 100644 index 0000000..abc9e71 --- /dev/null +++ b/data/hike/e9388170-1a54-4df2-a906-a5e1583f148e.json @@ -0,0 +1 @@ +{"id":"e9388170-1a54-4df2-a906-a5e1583f148e"} \ No newline at end of file diff --git a/data/hike/eb2d4107-a887-4901-ad70-a8b9c91f512d.json b/data/hike/eb2d4107-a887-4901-ad70-a8b9c91f512d.json new file mode 100644 index 0000000..9564c2d --- /dev/null +++ b/data/hike/eb2d4107-a887-4901-ad70-a8b9c91f512d.json @@ -0,0 +1 @@ +{"id":"eb2d4107-a887-4901-ad70-a8b9c91f512d","name":"some cool hike"} \ No newline at end of file diff --git a/data/hike/f07e49d4-7ac1-4f86-87e8-a811d61d4330.json b/data/hike/f07e49d4-7ac1-4f86-87e8-a811d61d4330.json new file mode 100644 index 0000000..41e2a60 --- /dev/null +++ b/data/hike/f07e49d4-7ac1-4f86-87e8-a811d61d4330.json @@ -0,0 +1 @@ +{"id":"f07e49d4-7ac1-4f86-87e8-a811d61d4330","name":"some cool hike","distance":"3.4 miles","difficulty":"medium"} \ No newline at end of file diff --git a/data/hike/f4be63b1-026a-4111-b128-401c4f5fd80a.json b/data/hike/f4be63b1-026a-4111-b128-401c4f5fd80a.json new file mode 100644 index 0000000..dfe3d5c --- /dev/null +++ b/data/hike/f4be63b1-026a-4111-b128-401c4f5fd80a.json @@ -0,0 +1 @@ +{"id":"f4be63b1-026a-4111-b128-401c4f5fd80a","name":"some cool hike","distance":"3.4 miles","difficulty":"medium"} \ No newline at end of file diff --git a/lib/storage.js b/lib/storage.js new file mode 100644 index 0000000..9e340da --- /dev/null +++ b/lib/storage.js @@ -0,0 +1,53 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), { suffix: 'Prom'}); +const createError = require('http-errors'); //create custom err0r and status code +const debug = require('debug')('hike:storage'); + +module.exports = exports = {}; + +exports.createItem = function(schemaName, item){ + debug('storage:create item'); + 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(err)); +}; + + +exports.fetchItem = function(schemaName, id){ + debug('fetch item'); + if(!schemaName) return Promise.reject(createError(400, 'expected schema name')); + if(!id) return Promise.reject(createError(400, 'expected id')); + + return fs.readFileProm(`${__dirname}/../${schemaName}/${id}.jsson`) + .then( data => { + try { + let item = JSON.parse(data.toString()); + return item; + } catch (err) { + return Promise.reject(err); + } + }) + .catch( err => Promise.reject()); +}; + +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 name')); + + return fs.unlinkProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .then( ()=> { + try { + console.log('delted that shit'); + return; + } catch (err) { + Promise.reject(err); + } + }).catch( err => Promise.reject(err)); +}; diff --git a/model/hike.js b/model/hike.js new file mode 100644 index 0000000..0229e6e --- /dev/null +++ b/model/hike.js @@ -0,0 +1,42 @@ +'use strict'; + +const uuidv4 = require('uuid/v4'); +const createError = require('http-errors'); +const debug = require('debug')('hike:hike'); +const storage = require('../lib/storage.js'); + +const Hike = module.exports = function(name, distance, difficulty) { + debug('hike constructor'); + + if(!name) throw new Error('expected name'); + if(!distance) throw new Error('expected distance'); + if(!difficulty) throw new Error('expected difficulty '); + + this.id = uuidv4(); + this.name = name; + this.distance = distance; + this.difficulty = difficulty; +}; + +Hike.createHike = function(_hike){ + debug('createHike'); + + try { + let hike = new Hike(_hike.name, _hike.distance, _hike.difficulty); + console.log(_hike.name, _hike.distance, _hike.difficulty); + console.log('constructed hike', hike); + return storage.createItem('hike', hike); + } catch (err){ + return Promise.reject(err); + } +}; + +Hike.fetchHike = function(id) { + debug('fetchHike'); + return storage.fetchItem('hike', id); +}; + +Hike.deleteHike = function(id){ + debug('deleteHike'); + return storage.deleteItem('hike', id); +}; diff --git a/npm-debug.log b/npm-debug.log new file mode 100644 index 0000000..d4cd9eb --- /dev/null +++ b/npm-debug.log @@ -0,0 +1,48 @@ +0 info it worked if it ends with ok +1 verbose cli [ '/Users/Andymartin/.nvm/versions/node/v6.11.1/bin/node', +1 verbose cli '/Users/Andymartin/.nvm/versions/node/v6.11.1/bin/npm', +1 verbose cli 'run', +1 verbose cli 'start' ] +2 info using npm@3.10.10 +3 info using node@v6.11.1 +4 verbose run-script [ 'prestart', 'start', 'poststart' ] +5 info lifecycle 11-express-api@1.0.0~prestart: 11-express-api@1.0.0 +6 silly lifecycle 11-express-api@1.0.0~prestart: no script for prestart, continuing +7 info lifecycle 11-express-api@1.0.0~start: 11-express-api@1.0.0 +8 verbose lifecycle 11-express-api@1.0.0~start: unsafe-perm in lifecycle true +9 verbose lifecycle 11-express-api@1.0.0~start: PATH: /Users/Andymartin/.nvm/versions/node/v6.11.1/lib/node_modules/npm/bin/node-gyp-bin:/Users/Andymartin/codefellows/401/labs/week-3/11-express-api/node_modules/.bin:/Users/Andymartin/.nvm/versions/node/v6.11.1/bin:/Users/Andymartin/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +10 verbose lifecycle 11-express-api@1.0.0~start: CWD: /Users/Andymartin/codefellows/401/labs/week-3/11-express-api +11 silly lifecycle 11-express-api@1.0.0~start: Args: [ '-c', 'DEBUG=\'hike*\' node server.js' ] +12 silly lifecycle 11-express-api@1.0.0~start: Returned: code: 1 signal: null +13 info lifecycle 11-express-api@1.0.0~start: Failed to exec start script +14 verbose stack Error: 11-express-api@1.0.0 start: `DEBUG='hike*' node server.js` +14 verbose stack Exit status 1 +14 verbose stack at EventEmitter. (/Users/Andymartin/.nvm/versions/node/v6.11.1/lib/node_modules/npm/lib/utils/lifecycle.js:255:16) +14 verbose stack at emitTwo (events.js:106:13) +14 verbose stack at EventEmitter.emit (events.js:191:7) +14 verbose stack at ChildProcess. (/Users/Andymartin/.nvm/versions/node/v6.11.1/lib/node_modules/npm/lib/utils/spawn.js:40:14) +14 verbose stack at emitTwo (events.js:106:13) +14 verbose stack at ChildProcess.emit (events.js:191:7) +14 verbose stack at maybeClose (internal/child_process.js:891:16) +14 verbose stack at Process.ChildProcess._handle.onexit (internal/child_process.js:226:5) +15 verbose pkgid 11-express-api@1.0.0 +16 verbose cwd /Users/Andymartin/codefellows/401/labs/week-3/11-express-api +17 error Darwin 15.6.0 +18 error argv "/Users/Andymartin/.nvm/versions/node/v6.11.1/bin/node" "/Users/Andymartin/.nvm/versions/node/v6.11.1/bin/npm" "run" "start" +19 error node v6.11.1 +20 error npm v3.10.10 +21 error code ELIFECYCLE +22 error 11-express-api@1.0.0 start: `DEBUG='hike*' node server.js` +22 error Exit status 1 +23 error Failed at the 11-express-api@1.0.0 start script 'DEBUG='hike*' node server.js'. +23 error Make sure you have the latest version of node.js and npm installed. +23 error If you do, this is most likely a problem with the 11-express-api package, +23 error not with npm itself. +23 error Tell the author that this fails on your system: +23 error DEBUG='hike*' node server.js +23 error You can get information on how to open an issue for this project with: +23 error npm bugs 11-express-api +23 error Or if that isn't available, you can get their info via: +23 error npm owner ls 11-express-api +23 error There is likely additional logging output above. +24 verbose exit [ 1, true ] diff --git a/package.json b/package.json new file mode 100644 index 0000000..f0c67f6 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "11-express-api", + "version": "1.0.0", + "description": "![cf](https://i.imgur.com/7v5ASc8.png) 11: Single Resource Express API ======", + "main": "server.js", + "scripts": { + "test": "mocha", + "start": "DEBUG='hike*' node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/andyfiveeleven/11-express-api.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/andyfiveeleven/11-express-api/issues" + }, + "homepage": "https://github.com/andyfiveeleven/11-express-api#readme", + "dependencies": { + "bluebird": "^3.5.0", + "body-parser": "^1.17.2", + "debug": "^2.6.8", + "express": "^4.15.3", + "http-errors": "^1.6.1", + "morgan": "^1.8.2", + "uuid": "^3.1.0" + }, + "devDependencies": { + "chai": "^4.1.0", + "mocha": "^3.5.0", + "superagent": "^3.5.2" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..ee0a9a1 --- /dev/null +++ b/server.js @@ -0,0 +1,62 @@ +'use strict'; + +const express = require('express'); +const morgan = require('morgan'); +const createError = require('http-errors'); +const jsonParser = require('body-parser').json(); +const debug = require('debug')('hike:server'); +const Hike = require('./model/hike.js'); + +const PORT = process.env.PORT || 3000 +const app = express(); + +app.use(morgan('dev')); + +app.get('/test', function(req, res) { + debug('GET: /test'); + res.json({ msg: 'hello from /test land'}); +}); + +app.post('/api/hike', jsonParser, function(req, res, next){ + debug('POST: /api/hike'); + + Hike.createHike(req.body) + .then( hike => res.json(hike)) + .catch( err => next(err)); +}); + +app.get('/api/hike', function(req, res, next){ + debug('GET: /api/hike'); + + Hike.fetchHike(req.query.id) + .then( hike => res.json(hike)) + .catch( err => next(err)); +}); + +app.delete('/api/hike', function(req, res, next){ + debug('DELETE: /api/hike'); + + Hike.deleteHike(req.query.id) + .then( () => { + createError(204, 'no content'); + return; + }) //i'm a little confused what happens here. This is questionable.... + .catch( err => next(err)); +}); + +app.use(function(err, req, res, next){ + debug('error middleware'); + console.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); +}); + +app.listen(PORT, () => { + debug('server up:', PORT); +}); diff --git a/test/hike-route-test.js b/test/hike-route-test.js new file mode 100644 index 0000000..31f0006 --- /dev/null +++ b/test/hike-route-test.js @@ -0,0 +1,68 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; +const debug = require('debug')('hike:test'); + +require('../server.js'); + +describe('hike routes', function(){ + var hike = null; + + describe('POST: /api/hike', function(){ + it('should return a hike', function(done){ + request.post('localhost:8000/api/hike') + .send({name:'some cool hike', distance:'3.4 miles', difficulty:'medium'}) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('some cool hike'); + expect(res.body.distance).to.equal('3.4 miles'); + expect(res.body.difficulty).to.equal('medium'); + hike = res.body; + done(); + }); + }); + + it('should return a bad request', function(done){ + request.post('localhost:8000/api/hike') + .send({name:'some cool hike', blue:'green'}) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.text).to.equal('bad request'); + done(); + }); + }); + }); + + describe('GET: /api/hike', function(){ + it('should return a hike', function(done){ + request.get(`localhost:8000/api/hike?id=${hike.id}`) + .send({name:'some cool hike', distance: '3.4 miles', difficulty: 'medium'}) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('some cool hike'); + expect(res.body.distance).to.equal('3.4 miles'); + expect(res.body.difficulty).to.equal('medium'); + console.log('get request hike: ', res.body); + done(); + }); + }); + it('should return a 404 error', function(done){ + request.get('localhost:8000/api/hike?id=123456') + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + + it('should return a 400 error', function(done){ + request.get('localhost:8000/api/hike?=12344567') + .end(function(err, res){ + expect(res.status).to.equal(400); + done(); + }); + }); + }); +});