diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..8dc6807 --- /dev/null +++ b/.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/.gitignore b/.gitignore new file mode 100644 index 0000000..d675f5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,91 @@ +# Created by https://www.gitignore.io/api/node,macos,vim,git + +### Node ### +# Logs +logs +*.log +npm-debug.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 + +# 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 + +### 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 + + +### Vim ### +# swap +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + + +### Git ### +*.orig diff --git a/README.md b/README.md new file mode 100644 index 0000000..00cda5d --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +## Express Single Resource API + +This is a fun project where we created refactored our Vanilla REST API to use Express.js! Woo! + +### Get the Project Running + +To get this project running, type the following in your command line: + +1. `git clone https://github.com/brittdawn/11-express_single_resource_api.git` +2. `cd 11-express_single_resource_api.git` +3. `npm i` +4. `brew install httpie` +5. `node server.js` + +You will now see the phrase "server is up: 3000" if you have not already specified a port number. + +### Test the Vanilla REST API (POST) + +1. Open a new terminal located at the root of this project and type `http POST localhost:3000/api/song title="meow" description="meow meow"` +2. You should get a JSON response with a description, id, and title with a `200` status code, like this example: + +``` javascript +HTTP/1.1 200 OK +Connection: keep-alive +Content-Length: 86 +Content-Type: application/json; charset=utf-8 +Date: Tue, 20 Dec 2016 03:49:14 GMT +ETag: W/"56-eWDnWC1unrCb1RCpC08DRQ" +X-Powered-By: Express + +{ + "description": "meow meow", + "id": "465251f0-c667-11e6-979e-15e9bd7f18fe", + "title": "meow" +} +``` + +### Test the Vanilla REST API (GET) + +After making a POST, you can make a GET request. + +1. Copy the id from your POST request above. Add it as a querystring to your GET request, like this example: `http localhost:3000/api/song?id=465251f0-c667-11e6-979e-15e9bd7f18fe` + +2. You should get a JSON response with a description, id, and title with a `200` status code, like this example: + +``` javascript +HTTP/1.1 200 OK +Connection: keep-alive +Content-Length: 86 +Content-Type: application/json; charset=utf-8 +Date: Tue, 20 Dec 2016 03:49:38 GMT +ETag: W/"56-eWDnWC1unrCb1RCpC08DRQ" +X-Powered-By: Express + +{ + "description": "meow meow", + "id": "465251f0-c667-11e6-979e-15e9bd7f18fe", + "title": "meow" +} +``` + +### Test the Vanilla REST API (DELETE) + +After making a GET or a POST, you can make a DELETE request. + +1. Copy the id from your POST/GET request above. Add it as a querystring to your DELETE request, like this example: `http DELETE localhost:3000/api/song?id=465251f0-c667-11e6-979e-15e9bd7f18fe` + +2. You should get a JSON response with a description, id, and title, like this example: + +``` javascript +HTTP/1.1 204 +Connection: keep-alive +Content-Length: 0 +Content-Type: application/json +Date: Tue, 20 Dec 2016 03:49:50 GMT +X-Powered-By: Express +``` + +3. If you try a GET request now for the item you deleted, it should not be found. For example, with the item above: `http DELETE localhost:3000/api/song?id=465251f0-c667-11e6-979e-15e9bd7f18fe`. Now if you will get this `404` (not found) response, because you deleted the item, yo: + +``` javascript +HTTP/1.1 404 Not Found +Connection: keep-alive +Content-Length: 13 +Content-Type: text/html; charset=utf-8 +Date: Tue, 20 Dec 2016 03:49:53 GMT +ETag: W/"d-8ImJlDOBcq5A9PkBq5sbQw" +X-Powered-By: Express + +NotFoundError +``` diff --git a/data/song/2b1d6ef0-c663-11e6-8151-6362d73a0c82.json b/data/song/2b1d6ef0-c663-11e6-8151-6362d73a0c82.json new file mode 100644 index 0000000..dbbf6af --- /dev/null +++ b/data/song/2b1d6ef0-c663-11e6-8151-6362d73a0c82.json @@ -0,0 +1 @@ +{"id":"2b1d6ef0-c663-11e6-8151-6362d73a0c82","title":"meow","description":"meow meow"} \ No newline at end of file diff --git a/data/song/35631ab0-c666-11e6-a436-95d49b074ebc.json b/data/song/35631ab0-c666-11e6-a436-95d49b074ebc.json new file mode 100644 index 0000000..1c26492 --- /dev/null +++ b/data/song/35631ab0-c666-11e6-a436-95d49b074ebc.json @@ -0,0 +1 @@ +{"id":"35631ab0-c666-11e6-a436-95d49b074ebc","title":"meow","description":"meow meow"} \ No newline at end of file diff --git a/data/song/49774dc0-c669-11e6-ad46-89d8a403c76b.json b/data/song/49774dc0-c669-11e6-ad46-89d8a403c76b.json new file mode 100644 index 0000000..33eb683 --- /dev/null +++ b/data/song/49774dc0-c669-11e6-ad46-89d8a403c76b.json @@ -0,0 +1 @@ +{"id":"49774dc0-c669-11e6-ad46-89d8a403c76b","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/67471600-c664-11e6-87b9-896cdb3073c3.json b/data/song/67471600-c664-11e6-87b9-896cdb3073c3.json new file mode 100644 index 0000000..61edfc1 --- /dev/null +++ b/data/song/67471600-c664-11e6-87b9-896cdb3073c3.json @@ -0,0 +1 @@ +{"id":"67471600-c664-11e6-87b9-896cdb3073c3","title":"meow","description":"meow meow"} \ No newline at end of file diff --git a/data/song/69f309e0-c669-11e6-90f6-2d65db8c1575.json b/data/song/69f309e0-c669-11e6-90f6-2d65db8c1575.json new file mode 100644 index 0000000..532efcc --- /dev/null +++ b/data/song/69f309e0-c669-11e6-90f6-2d65db8c1575.json @@ -0,0 +1 @@ +{"id":"69f309e0-c669-11e6-90f6-2d65db8c1575","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/7233c540-c669-11e6-82ff-ad0cf937bc7a.json b/data/song/7233c540-c669-11e6-82ff-ad0cf937bc7a.json new file mode 100644 index 0000000..0040b6f --- /dev/null +++ b/data/song/7233c540-c669-11e6-82ff-ad0cf937bc7a.json @@ -0,0 +1 @@ +{"id":"7233c540-c669-11e6-82ff-ad0cf937bc7a","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/7d419fd0-c668-11e6-b91c-f7697a602373.json b/data/song/7d419fd0-c668-11e6-b91c-f7697a602373.json new file mode 100644 index 0000000..075e6b7 --- /dev/null +++ b/data/song/7d419fd0-c668-11e6-b91c-f7697a602373.json @@ -0,0 +1 @@ +{"id":"7d419fd0-c668-11e6-b91c-f7697a602373","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/82a92e10-c669-11e6-9ac3-8731107568fe.json b/data/song/82a92e10-c669-11e6-9ac3-8731107568fe.json new file mode 100644 index 0000000..373b1e9 --- /dev/null +++ b/data/song/82a92e10-c669-11e6-9ac3-8731107568fe.json @@ -0,0 +1 @@ +{"id":"82a92e10-c669-11e6-9ac3-8731107568fe","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/908eeb50-c669-11e6-bbbb-1b1c4bcc18fd.json b/data/song/908eeb50-c669-11e6-bbbb-1b1c4bcc18fd.json new file mode 100644 index 0000000..0169b63 --- /dev/null +++ b/data/song/908eeb50-c669-11e6-bbbb-1b1c4bcc18fd.json @@ -0,0 +1 @@ +{"id":"908eeb50-c669-11e6-bbbb-1b1c4bcc18fd","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/9e84f1b0-c668-11e6-aa3d-8da8f38cd954.json b/data/song/9e84f1b0-c668-11e6-aa3d-8da8f38cd954.json new file mode 100644 index 0000000..89c04bc --- /dev/null +++ b/data/song/9e84f1b0-c668-11e6-aa3d-8da8f38cd954.json @@ -0,0 +1 @@ +{"id":"9e84f1b0-c668-11e6-aa3d-8da8f38cd954","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/ac6c4020-c669-11e6-9b97-7b290993bf82.json b/data/song/ac6c4020-c669-11e6-9b97-7b290993bf82.json new file mode 100644 index 0000000..772bcc7 --- /dev/null +++ b/data/song/ac6c4020-c669-11e6-9b97-7b290993bf82.json @@ -0,0 +1 @@ +{"id":"ac6c4020-c669-11e6-9b97-7b290993bf82","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/bbe40f10-c669-11e6-bafd-6b4f864ffd1a.json b/data/song/bbe40f10-c669-11e6-bafd-6b4f864ffd1a.json new file mode 100644 index 0000000..130d863 --- /dev/null +++ b/data/song/bbe40f10-c669-11e6-bafd-6b4f864ffd1a.json @@ -0,0 +1 @@ +{"id":"bbe40f10-c669-11e6-bafd-6b4f864ffd1a","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/d13a1c10-c6dc-11e6-baaf-571a317fb498.json b/data/song/d13a1c10-c6dc-11e6-baaf-571a317fb498.json new file mode 100644 index 0000000..4c52a8a --- /dev/null +++ b/data/song/d13a1c10-c6dc-11e6-baaf-571a317fb498.json @@ -0,0 +1 @@ +{"id":"d13a1c10-c6dc-11e6-baaf-571a317fb498","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/d4e9b2d0-c669-11e6-a4b3-2ded427aa76c.json b/data/song/d4e9b2d0-c669-11e6-a4b3-2ded427aa76c.json new file mode 100644 index 0000000..000679d --- /dev/null +++ b/data/song/d4e9b2d0-c669-11e6-a4b3-2ded427aa76c.json @@ -0,0 +1 @@ +{"id":"d4e9b2d0-c669-11e6-a4b3-2ded427aa76c","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/e9a15930-c669-11e6-aab5-151730593493.json b/data/song/e9a15930-c669-11e6-aab5-151730593493.json new file mode 100644 index 0000000..623460b --- /dev/null +++ b/data/song/e9a15930-c669-11e6-aab5-151730593493.json @@ -0,0 +1 @@ +{"id":"e9a15930-c669-11e6-aab5-151730593493","title":"test title","description":"test description"} \ No newline at end of file diff --git a/data/song/ee6f34f0-c669-11e6-bd41-55f016c71ca6.json b/data/song/ee6f34f0-c669-11e6-bd41-55f016c71ca6.json new file mode 100644 index 0000000..f517917 --- /dev/null +++ b/data/song/ee6f34f0-c669-11e6-bd41-55f016c71ca6.json @@ -0,0 +1 @@ +{"id":"ee6f34f0-c669-11e6-bd41-55f016c71ca6","title":"test title","description":"test description"} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..6db050d --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,37 @@ +'use strict'; + +const gulp = require('gulp'); +const eslint = require('gulp-eslint'); +const mocha = require('gulp-mocha'); +const cache = require('gulp-cache'); +const istanbul = require('gulp-istanbul'); + +gulp.task('pre-test', function() { + return gulp.src(['./lib/*.js', './model/*.js', '!node_modules/**']) + .pipe(istanbul()) + .pipe(istanbul.hookRequire()); +}); + +gulp.task('test', ['pre-test'], function() { + gulp.src('./test/*-test.js', { read: false}) + .pipe(mocha({ report: 'spec'})) + .pipe(istanbul.writeReports()) + .pipe(istanbul.enforceThresholds({thresholds: {global: 90}})); +}); + +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']); + +gulp.task('clear', function (done) { + return cache.clearAll(done); +}); diff --git a/lib/storage.js b/lib/storage.js new file mode 100644 index 0000000..51f8355 --- /dev/null +++ b/lib/storage.js @@ -0,0 +1,48 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), {suffix: 'Prom'}); +const createError = require('http-errors'); +const debug = require('debug')('song:storage'); + +module.exports = exports = {}; + +exports.createItem = function(schemaName, item){ + debug('createItem'); + + if (!schemaName) return Promise.reject(createError(400, 'expected the schema name')); + if (!item) return Promise.reject(createError(400, 'expected an 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 the schema name')); + if (!id) return Promise.reject(createError(400, 'expected an 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 the schema name')); + if (!id) return Promise.reject(createError(400, 'expected an id')); + + return fs.unlinkProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .catch( err => Promise.reject(createError(500, err.message))); +}; diff --git a/model/song.js b/model/song.js new file mode 100644 index 0000000..addb501 --- /dev/null +++ b/model/song.js @@ -0,0 +1,38 @@ +'use strict'; + +const uuid = require('node-uuid'); +const createError = require('http-errors'); +const debug = require('debug')('song:song'); +const storage = require('../lib/storage.js'); + +const Song = module.exports = function(title, description) { + debug('song constructor'); + + if (!title) throw createError(400, 'expected title'); + if (!description) throw createError(400, 'expected description'); + + this.id = uuid.v1(); + this.title = title; + this.description = description; +}; + +Song.createSong = function(_song) { + debug('createSong'); + + try { + let song = new Song(_song.title, _song.description); + return storage.createItem('song', song); + } catch (err) { + return Promise.reject(err); + } +}; + +Song.fetchSong = function(id) { + debug('fetchSong'); + return storage.fetchItem('song', id); +}; + +Song.deleteSong = function(id) { + debug('deleteSong'); + return storage.deleteItem('song', id); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..38345a9 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "express", + "version": "1.0.0", + "description": "This is a project using Express.", + "main": "index.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "DEBUG='song*' node server.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "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", + "eslint": "^3.12.2", + "gulp": "^3.9.1", + "gulp-cache": "^0.4.5", + "gulp-eslint": "^3.0.1", + "gulp-istanbul": "^1.1.1", + "gulp-mocha": "^3.0.1", + "mocha": "^3.2.0", + "superagent": "^3.3.0" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..6e8b24f --- /dev/null +++ b/server.js @@ -0,0 +1,56 @@ +'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')('song:server'); + +const Song = require('./model/song'); + +const PORT = process.env.PORT || 3000; +const app = express(); + +app.use(morgan('dev')); + +app.post('/api/song', jsonParser, function(req, res, next) { + debug('POST: /api/song'); + + Song.createSong(req.body) + .then( song => res.json(song)) + .catch( err => next(err)); +}); + +app.get('/api/song', function(req, res, next) { + debug('GET: /api/song'); + + Song.fetchSong(req.query.id) + .then( song => res.json(song)) + .catch( err => next(err)); +}); + +app.delete('/api/song', (req, res, next) => { + debug('DELETE: /api/song'); + + Song.deleteSong(req.query.id) + .then( song => res.json(song)) + .catch( err => next(err)); +}); + +// eslint-disable-next-line +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/song-route-test.js b/test/song-route-test.js new file mode 100644 index 0000000..2e43e1a --- /dev/null +++ b/test/song-route-test.js @@ -0,0 +1,85 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; + +require('../server.js'); + +describe('Song Routes', function() { + var song = null; + + describe('POST: /api/song', function() { + it('should return a 400 bad request if no body is given', function(done) { + request.post('localhost:3000/api/song') + .send({}) + .end((err, res) => { + expect(err).to.be.an('error'); + expect(res.text).to.equal('BadRequestError'); + expect(res.body.id).to.equal(undefined); + done(); + }); + }); + it('should return a 400 bad request an invalid body is requested', function(done) { + request.post('localhost:3000/api/song') + .send({ meow: 'meow meow meow', robot: 'beep beep boop' }) + .end((err, res) => { + expect(res.text).to.equal('BadRequestError'); + expect(res.body.id).to.equal(undefined); + done(); + }); + }); + it('should return a song', function(done) { + request.post('localhost:3000/api/song') + .send({ title: 'test title', description: 'test description' }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.title).to.equal('test title'); + expect(res.body.description).to.equal('test description'); + song = res.body; + done(); + }); + }); + }); + + describe('GET: /api/song', function() { + it('should return a song', function(done) { + request.get(`localhost:3000/api/song?id=${song.id}`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.title).to.equal('test title'); + expect(res.body.description).to.equal('test description'); + done(); + }); + }); + it('should return a 404 not found for a valid request but an invalid id', function(done) { + request.get('localhost:3000/api/song?id=44abc123') + .end((err, res) => { + expect(res.status).to.equal(404); + expect(res.text).to.equal('NotFoundError'); + done(); + }); + }); + it('should return a 400 bad request if id is not provided', function(done) { + request.get('localhost:3000/api/song') + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.text).to.equal('BadRequestError'); + expect(res.body.id).to.equal(undefined); + done(); + }); + }); + }); + + describe('testing for routes not registered', function() { + it('should return a status code of 404', function(done) { + request.get('localhost:3000/api/movie') + .end((err, res) => { + expect(res.status).to.equal(404); + expect(res.text).to.equal('Cannot GET /api/movie\n'); + done(); + }); + }); + }); +});