diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..05b1cf3 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +**/node_modules/* +**/vendor/* +**/*.min.js +**/coverage/* +**/build/* 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..345130c --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# Created by https://www.gitignore.io/api/osx,vim,node,macos,windows + +### 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 + +### 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 ### + +# Icon must end with two \r + +# Thumbnails + +# Files that might appear in the root of a volume + +# Directories potentially created on remote AFP share + +### Vim ### +# swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + +### 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,vim,node,macos,windows diff --git a/data/note/89f1c6e2-0aea-4b37-8bbd-8c39ae5298a7.json b/data/note/89f1c6e2-0aea-4b37-8bbd-8c39ae5298a7.json new file mode 100644 index 0000000..4affd00 --- /dev/null +++ b/data/note/89f1c6e2-0aea-4b37-8bbd-8c39ae5298a7.json @@ -0,0 +1 @@ +{"id":"89f1c6e2-0aea-4b37-8bbd-8c39ae5298a7","name":"test name","content":"test content"} \ No newline at end of file diff --git a/data/note/a018ec08-28ef-4ba0-886b-35e2aba351b4.json b/data/note/a018ec08-28ef-4ba0-886b-35e2aba351b4.json new file mode 100644 index 0000000..be653bd --- /dev/null +++ b/data/note/a018ec08-28ef-4ba0-886b-35e2aba351b4.json @@ -0,0 +1 @@ +{"id":"a018ec08-28ef-4ba0-886b-35e2aba351b4","name":"test name","content":"test content"} \ No newline at end of file diff --git a/data/note/eaa13f81-44c5-4fe1-b079-e4934b91fd23.json b/data/note/eaa13f81-44c5-4fe1-b079-e4934b91fd23.json new file mode 100644 index 0000000..4a0d02d --- /dev/null +++ b/data/note/eaa13f81-44c5-4fe1-b079-e4934b91fd23.json @@ -0,0 +1 @@ +{"id":"eaa13f81-44c5-4fe1-b079-e4934b91fd23","name":"test name","content":"test content"} \ No newline at end of file diff --git a/data/note/ff715613-6ad2-45d7-bae8-a01048fa495d.json b/data/note/ff715613-6ad2-45d7-bae8-a01048fa495d.json new file mode 100644 index 0000000..cedd9d5 --- /dev/null +++ b/data/note/ff715613-6ad2-45d7-bae8-a01048fa495d.json @@ -0,0 +1 @@ +{"id":"ff715613-6ad2-45d7-bae8-a01048fa495d","name":"test name","content":"test content"} \ No newline at end of file diff --git a/lib/parse-json.js b/lib/parse-json.js new file mode 100644 index 0000000..b5a2c3d --- /dev/null +++ b/lib/parse-json.js @@ -0,0 +1,30 @@ +'use strict'; + +module.exports = (req) => { + return new Promise((resolve, reject) => { + if(req.method === 'POST' || req.method === 'PUT'){ + let body = ''; + + req.on('data', (data) => { + body += data.toString(); + }); + + req.on('end', () => { + try{ + req.body = JSON.parse(body); + resolve(req); + }catch(err){ + console.error(err); + reject(err); + } + }); + + req.on('error', (err) => { + console.error(err); + reject(err); + }); + return; + } + resolve(); + }); +}; diff --git a/lib/parse-url.js b/lib/parse-url.js new file mode 100644 index 0000000..c961fed --- /dev/null +++ b/lib/parse-url.js @@ -0,0 +1,10 @@ +'use strict'; + +const parseUrl = require('url').parse; +const parseQuery = require('querystring').parse; + +module.exports = (req) => { + req.url = parseUrl(req.url); + req.url.query = parseQuery(req.url.query); + return Promise.resolve(req); +}; diff --git a/lib/response.js b/lib/response.js new file mode 100644 index 0000000..af427e7 --- /dev/null +++ b/lib/response.js @@ -0,0 +1,22 @@ +'use strict'; +'use strict'; + +module.exports = exports = {}; + +exports.sendJson = (res, status, data) => { + res.writeHead(status, { + 'Content-Type': 'application/json' + }); + + res.write(JSON.stringify(data)); + res.end(); +}; + +exports.sendText = (res, status, msg) => { + res.writeHead(status, { + 'Content-Type': 'text/plain' + }); + + res.write(msg); + res.end(); +}; diff --git a/lib/router.js b/lib/router.js new file mode 100644 index 0000000..860c5ef --- /dev/null +++ b/lib/router.js @@ -0,0 +1,51 @@ +'use strict'; + +const parseUrl = require('./parse-url.js'); +const parseJSON = require('./parse-json.js'); +const returnStatus = require('./response.js'); + +const Router = module.exports = function(){ + this.routes = { + GET: {}, + POST: {}, + PUT: {}, + DELETE: {} + }; +}; + +Router.prototype.get = function(endpoint, callback){ + this.routes.GET[endpoint] = callback; +}; + +Router.prototype.post = function(endpoint, callback){ + this.routes.POST[endpoint] = callback; +}; + +Router.prototype.put = function(endpoint, callback){ + this.routes.PUT[endpoint] = callback; +}; + +Router.prototype.delete = function(endpoint, callback){ + this.routes.DELETE[endpoint] = callback; +}; + +Router.prototype.route = function(){ + return(req, res) => { + Promise.all([ + parseUrl(req), + parseJSON(req) + ]) + .then(() => { + if(typeof this.routes[req.method][req.url.pathname] === 'function'){ + this.routes[req.method][req.url.pathname](req, res); + return; + } + console.error('route not found'); + returnStatus.sendText(res, 404, 'route not found'); + }) + .catch((err) => { + console.error(err); + returnStatus.sendText(res, 400, 'bad request'); + }); + }; +}; diff --git a/lib/storage.js b/lib/storage.js new file mode 100644 index 0000000..839c498 --- /dev/null +++ b/lib/storage.js @@ -0,0 +1,49 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), {suffix: 'Pr'}); + +module.exports = exports = {}; + +exports.createItem = (schemaName, item) => { + if(!schemaName) return Promise.reject(new Error('no schemaName given')); + if(!item) return Promise.reject(new Error('no item given')); + + return fs.writeFilePr(`${__dirname}/../data/${schemaName}/${item.id}.json`, JSON.stringify(item)) + .then(() => item) + .catch((err) => Promise.reject(err)); +}; + +exports.fetchItem = (schemaName, id) => { + if(!schemaName) return Promise.reject(new Error('expected schemaName')); + if(!id) return Promise.reject(new Error('expected id')); + + return fs.readFilePr(`${__dirname}/../data/${schemaName}/${id}.json`) + .then((data) => { + try{ + return JSON.parse(data.toString()); + }catch(err){ + return Promise.reject(err); + } + }) + .catch((err) => Promise.reject(err)); +}; + +exports.removeItem = (schemaName, id) => { + if(!schemaName) return Promise.reject(new Error('no schemaName given')); + if(!id) return Promise.reject(new Error('no id given')); + + return fs.unlinkPr(`${__dirname}/../data/${schemaName}/${id}.json`) + .catch((err) => Promise.reject(err)); +}; + +exports.updateItem = (schemaName, id, input) => { + if(!schemaName) return Promise.reject(new Error('no schemaName given')); + if(!id) return Promise.reject(new Error('no id given')); + if(!input) return Promise.reject(new Error('no content given')); + console.log(id); + console.log(input); + return fs.writeFilePr(`${__dirname}/../data/${schemaName}/${id}.json`, JSON.stringify(input)) + .then(() => input) + .catch((err) => Promise.reject(err)); +}; diff --git a/model/note.js b/model/note.js new file mode 100644 index 0000000..7ed4d7f --- /dev/null +++ b/model/note.js @@ -0,0 +1,12 @@ +'use strict'; + +const uuidv4 = require('uuid/v4'); + +module.exports = function(name, content){ + if(!name) throw new Error('expected name'); + if(!content) throw new Error('expected content'); + + this.id = uuidv4(); + this.name = name; + this.content = content; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..bcf579e --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "09-vanilla_api_persistence", + "version": "1.0.0", + "description": "![cf](https://i.imgur.com/7v5ASc8.png) Lab 09: Vanilla REST API w/ Persistence ======", + "main": "server.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "mocha", + "start": "node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/loganabsher/09-vanilla_api_persistence.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/loganabsher/09-vanilla_api_persistence/issues" + }, + "homepage": "https://github.com/loganabsher/09-vanilla_api_persistence#readme", + "devDependencies": { + "chai": "^4.1.0", + "mocha": "^3.4.2", + "superagent": "^3.5.2" + }, + "dependencies": { + "bluebird": "^3.5.0", + "uuid": "^3.1.0" + } +} diff --git a/route/note-route.js b/route/note-route.js new file mode 100644 index 0000000..be128e5 --- /dev/null +++ b/route/note-route.js @@ -0,0 +1,52 @@ +'use strict'; + +const storage = require('../lib/storage.js'); +const returnStatus = require('../lib/response.js'); +const Note = require('../model/note.js'); + +module.exports = (router) => { + router.get('/api/note', (req, res) => { + if(req.url.query.id){ + storage.fetchItem('note', req.url.query.id) + .then((note) => returnStatus.sendJson(res, 200, note)) + .catch((err) => { + console.error(err); + returnStatus.sendText(res, 404, 'note note found'); + }); + return; + } + returnStatus.sendText(res, 400, 'bad request'); + }); + + router.post('/api/note', (req, res) => { + try{ + let note = new Note(req.body.name, req.body.content); + storage.createItem('note', note); + returnStatus.sendJson(res, 200, note); + }catch(err){ + console.error(err); + returnStatus.sendText(res, 400, 'bad request'); + } + }); + + router.delete('/api/note', (req, res) => { + try{ + storage.removeItem('note', req.url.query.id); + returnStatus.sendJson(res, 202, ''); + }catch(err){ + console.error(err); + returnStatus.sendText(res, 400, 'bad request'); + } + }); + + router.put('/api/note', (req, res) => { + try{ + let note = new Note(req.body.name, req.body.content); + storage.updateItem('note', req.url.query.id, note); + returnStatus.sendJson(res, 200, JSON.stringify(note)); + }catch(err){ + console.error(err); + returnStatus.sendText(res, 400, 'bad request'); + } + }); +}; diff --git a/server.js b/server.js new file mode 100644 index 0000000..fd714cd --- /dev/null +++ b/server.js @@ -0,0 +1,15 @@ +'use strict'; + +const http = require('http'); +const Router = require('./lib/router.js'); + +const PORT = process.env.PORT || 3000; + +const router = new Router(); +require('./route/note-route.js')(router); + +const server = http.createServer(router.route()); + +server.listen(PORT, () => { + console.log('running on PORT', PORT); +}); diff --git a/test/note-route-test.js b/test/note-route-test.js new file mode 100644 index 0000000..18cde10 --- /dev/null +++ b/test/note-route-test.js @@ -0,0 +1,63 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; + +require('../server.js'); + +describe('Note Routes', () => { + var note = null; + + describe('POST: /api/note', () => { + it('Should return a note', (done) => { + request.post('localhost:8000/api/note') + .send({name: 'test name', content: 'test content'}) + .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.content).to.equal('test content'); + note = res.body; + done(); + }); + }); + }); + + describe('GET: /api/note', () => { + it('Should return a note', (done) => { + request.get(`localhost:8000/api/note?id=${note.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.content).to.equal('test content'); + done(); + }); + }); + }); + + describe('PUT: /api/note', () => { + it('Should replace a note\'s info', (done) => { + request.put(`localhost:8000/api/note?id=${note.id}`) + .send({name: 'new name', content: 'new content'}) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('new name'); + expect(res.body.content).to.equal('new content'); + done(); + }); + }); + }); + + describe('DELETE: /api/note', () => { + it('Should delete a note', (done) => { + request.delete(`localhost:8000/api/note?id=${note.id}`) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(202); + done(); + }); + }); + }); +});