diff --git a/README.md b/README.md deleted file mode 100644 index 0aaf9f2..0000000 --- a/README.md +++ /dev/null @@ -1,32 +0,0 @@ -![cf](https://i.imgur.com/7v5ASc8.png) Lab 09: Vanilla REST API w/ Persistence -====== - -## 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 learn how to save resource data to the file system for a layer of data persistence -* students will learn how to refactor commonly used coding constructs into custom helper modules - -## Requirements - -#### Configuration -* `package.json` -* `.eslintrc` -* `.gitignore` -* `README.md` - * your `README.md` should include detailed instructions on how to use your API - * this should include documentation on how to access your API endpoints - -#### Feature Tasks -* continue working on your vanilla REST API -* refactor your routes to be contained in a separate module (ex: `route/resource-route.js`) -* refactor your `res` messages & status codes to be contained in a separate module (ex: `response.js`) -* refactor the `storage.js` module to use file system persistence - * use the `fs` module to create and read the associated data files - * the name of the file should contain the related resource id diff --git a/lab-eddie/.gitignore b/lab-eddie/.gitignore new file mode 100644 index 0000000..e19c8ab --- /dev/null +++ b/lab-eddie/.gitignore @@ -0,0 +1,128 @@ + +# Created by https://www.gitignore.io/api/osx,vim,node,windows + +### 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 + +### 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,windows +data \ No newline at end of file diff --git a/lab-eddie/README.md b/lab-eddie/README.md new file mode 100644 index 0000000..9573976 --- /dev/null +++ b/lab-eddie/README.md @@ -0,0 +1,64 @@ +Vanilla REST API wth file system persistance :D + +This API is seperated into 6 major components: + +1.) The server + +2.) lib driectory + +3.) model directory + +4.) test directory + +5.) data directory + +6.) node_modules + +-----Component Overview----- + +1. The server. This is the meat and potatoes of it all. It uses the http module to host a server. It requires various modules for establishing routes, endponts and file scaffolding. + +2. lib directory houses all of our home made modules and other goodies. + +3. model directory houses all object constructors. + +4. test directory houses all of our tests >:( + +5. data driectory is used for file system persistent storage. It has a sub-directory for each model. + +6. node_modules houses all of our dependancies + +----Instructions---- + +Using httpie is probably the easiest way to go about this whole process. + +-Start off by running the server. Make sure you're in the lab directory and type the following : + +node server.js + +This should return a prompt telling you the sever is up. Also, if you don't have a data folder, it'll create one automatically for you and fill it with folders for each model. + +-Open another terminal tab. + +*To do a GET request type the following: +http GET :3000/api/(whatever model you pick) id==324235345234 + +this should return an object + +*To do a POST request, it's a bit harder. We have to know what our model looks like. We'll have to include more parameters in the request like this: + +http POST :3000/api/dog name==rover breed==pug age==7 + +This should return an object that looks like this: + +{ + name: 'rover', + breed: 'pug', + age: '7' +} + +*To do a DELETE request, it's identical to the GET request. Except it shouldn't return anything. It'll look like this: + +http DELETE :3000/api/car id==324235345234 + +All done! \ No newline at end of file diff --git a/lab-eddie/lib/auto-direct.js b/lab-eddie/lib/auto-direct.js new file mode 100644 index 0000000..058aa97 --- /dev/null +++ b/lab-eddie/lib/auto-direct.js @@ -0,0 +1,19 @@ +'use strict' + +const fs = require('fs') + +//Automatically creates a data directory and folders for each model +const autoDataDir = module.exports = function(models) { + + let labDir = fs.readdirSync('.'); + console.log(Object.keys(models).length) + if (!labDir.includes('data')) fs.mkdirSync('./data'); + autoModelDir(models); +}; + +const autoModelDir = function(models) { + let modelKeys = Object.keys(models); + let dataDir = fs.readdirSync('./data'); + modelKeys.forEach(key => { + if(!dataDir.includes(key)) fs.mkdirSync(`./data/${key}`)}); +} diff --git a/lab-eddie/lib/errorHandle.js b/lab-eddie/lib/errorHandle.js new file mode 100644 index 0000000..8f88ca1 --- /dev/null +++ b/lab-eddie/lib/errorHandle.js @@ -0,0 +1,12 @@ +'use strict' + +const errHandle = module.exports = {}; + +errHandle.normal = function(paramNames, params) { + let args = params.length; + if (paramNames.length !== args) throw new Error(`Expected ${paramNames[args]}`); +} + +errHandle.promiseErr = function(paramNames, params) { + +} \ No newline at end of file diff --git a/lab-eddie/lib/header.js b/lab-eddie/lib/header.js new file mode 100644 index 0000000..ab482d9 --- /dev/null +++ b/lab-eddie/lib/header.js @@ -0,0 +1,20 @@ +'use strict' + +const header = module.exports = {}; + +header.textHeader = function(res, code, msg) { + res.writeHeader(code, { + 'Content-Type': 'text/plain' + }); + res.write(msg); + res.end(); +} + +header.appHeader = function(res, code, json) { + res.writeHeader(code, { + 'Content-Type': 'application/json' + }); + res.write(JSON.stringify(json)); + res.end(); +} + diff --git a/lab-eddie/lib/model-paths.js b/lab-eddie/lib/model-paths.js new file mode 100644 index 0000000..1273c8d --- /dev/null +++ b/lab-eddie/lib/model-paths.js @@ -0,0 +1,100 @@ +'use strict'; + +const storage = require('./storage.js'); +const header = require('./header.js') +const Person = require('../model/person.js'); +const Car = require('../model/car.js'); +const Dog = require('../model/dog.js') +const Employee = require('../model/employee.js') +const Character = require('../model/character.js') + + +const modelRoutes = module.exports = {}; + +modelRoutes.models = { + person : Person, + car: Car, + dog: Dog, + employee: Employee, + character: Character +} + + +modelRoutes.allRoutes = function(model, router) { + modelRoutes.modelGet(model, router); + modelRoutes.modelDelete(model, router); + modelRoutes.modelPost(model, router); +} + + +modelRoutes.modelGet = function(model,router) { + router.get(`/api/${model}`, function(req, res) { + if (req.url.query.id) { + storage.fetchItem(`${model}`, req.url.query.id) + .then( item => { + header.appHeader(res, 200, item); + }) + .catch( err => { + console.error(err); + header.textHeader(res, 404, `${model} not found!`) + }); + + return; + } else if (req.url.query) { + storage.fetchItem(`${model}`) + .then( item => { + header.appHeader(res, 200, item); + }) + .catch( err => { + console.error(err); + header.textHeader(res, 400, 'Bad request!') + }); + + return; + }; + + header.textHeader(res, 400, 'Bad request!') + }); +}; + +modelRoutes.modelPost = function(model, router) { + router.post(`/api/${model}`, function(req, res) { + try { + let params = []; + for(let key in req.body) { + params.push(req.body[key]); + } + var newObj = new modelRoutes.models[model](...params); + + storage.createItem(`${model}`, newObj); + header.appHeader(res, 200, newObj); + + } catch (err) { + console.error(err); + header.textHeader(res, 400, 'Bad request!') + } + }); +} + +modelRoutes.modelDelete = function(model, router) { + + router.delete(`/api/${model}`, function(req, res) { + if (req.url.query.id) { + storage.deleteItem(`${model}`, req.url.query.id) + .then( item => { + header.appHeader(res, 202, item); + }) + .catch( err => { + console.error(err); + header.textHeader(res, 404, `${model} not found!`) + }); + + return; + }; + + header.textHeader(res, 400, 'Bad request!') + }); + +} + +require('./auto-direct.js')(modelRoutes.models) \ No newline at end of file diff --git a/lab-eddie/lib/parse-json.js b/lab-eddie/lib/parse-json.js new file mode 100644 index 0000000..6b9ad88 --- /dev/null +++ b/lab-eddie/lib/parse-json.js @@ -0,0 +1,34 @@ +'use strict'; + +module.exports = function(req) { + return new Promise((resolve, reject) => { + if (req.method === 'POST' || req.method === 'PUT') { + var 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/lab-eddie/lib/parse-url.js b/lab-eddie/lib/parse-url.js new file mode 100644 index 0000000..a718e7f --- /dev/null +++ b/lab-eddie/lib/parse-url.js @@ -0,0 +1,10 @@ +'use strict'; + +const parseUrl = require('url').parse; +const parseQuery = require('querystring').parse; + +module.exports = function(req) { + req.url = parseUrl(req.url); + req.url.query = parseQuery(req.url.query); + return Promise.resolve(req); +} diff --git a/lab-eddie/lib/router.js b/lab-eddie/lib/router.js new file mode 100644 index 0000000..068548a --- /dev/null +++ b/lab-eddie/lib/router.js @@ -0,0 +1,61 @@ +'use strict'; + +const parseUrl = require('./parse-url.js'); +const parseJSON = require('./parse-json.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.delete = function(endpoint, callback) { + this.routes.DELETE[endpoint] = callback; +}; + +Router.prototype.put = function(endpoint, callback) { + this.routes.PUT[endpoint] = callback; +}; + +Router.prototype.route = function() { + return (req, res) => { + Promise.all([ + parseUrl(req), + parseJSON(req) + ]) + .then(() => { + let reqMethod = this.routes[req.method]; + if(typeof reqMethod[req.url.pathname] === 'function') { + reqMethod[req.url.pathname](req, res); + return + } + console.error('route not found'); + res.writeHead(404, { + 'Content-Type' : 'text/plain' + }); + + res.write('route not found'); + res.end(); + }) + .catch(err => { + + res.writeHead(400, { + 'Content-Type' : 'text/plain' + }) + + res.write('bad request'); + res.end(); + }); + } +}; \ No newline at end of file diff --git a/lab-eddie/lib/storage.js b/lab-eddie/lib/storage.js new file mode 100644 index 0000000..1f723e3 --- /dev/null +++ b/lab-eddie/lib/storage.js @@ -0,0 +1,53 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), { suffix: 'Prom' }); + +module.exports = exports = {}; + +exports.createItem = function(category, item) { + if(!category) return Promise.reject(new Error(`Expecte category`)); + if(!item) return Promise.reject(new Error('Expected item')); + + let stringObj = JSON.stringify(item); + return fs.writeFileProm(`${__dirname}/../data/${category}/${item.id}.json`, stringObj) + .then(() => item) + .catch(err => Promise.reject(err)); +} + +exports.fetchItem = function(category, id) { + if(!category) return Promise.reject(new Error(`Expecte category`)); + if(!id) return exports.fetchCategory(category); + + + return fs.readFileProm(`${__dirname}/../data/${category}/${id}.json`) + .then(data => { + try { + let item = JSON.parse(data.toString()); + return item; + } catch (err) { + return Promise.reject(err); + } + }) +}; + +exports.deleteItem = function(category, id) { + if(!category) return Promise.reject(new Error(`Expecte category`)); + if(!id) return Promise.reject(new Error('Expected item')); + + return fs.unlinkProm(`${__dirname}/../data/${category}/${id}.json`) + .then(() => '') + .catch(err => Promise.reject(err)); + +}; + +exports.fetchCategory= function(category) { + if(!category) return Promise.reject(new Error(`Expecte category`)); + + return fs.readdirProm(`${__dirname}/../data/${category}`) + .then(data => { + data = data.map(id => id.split('.json')[0]) + return data; + }) + .catch(err => Promise.reject(err)) +} \ No newline at end of file diff --git a/lab-eddie/model/car.js b/lab-eddie/model/car.js new file mode 100644 index 0000000..fb64c89 --- /dev/null +++ b/lab-eddie/model/car.js @@ -0,0 +1,15 @@ +'use strict'; + +const uuidv4 = require('uuid/v4'); +const errHandle = require('../lib/errorHandle.js'); + +module.exports = function(make, model, year, color) { + errHandle.normal(['make', 'model', 'year', 'color'], arguments); + + this.id = uuidv4(); + this.make = make; + this.model = model; + this.year = year; + this.color = color; + +}; \ No newline at end of file diff --git a/lab-eddie/model/character.js b/lab-eddie/model/character.js new file mode 100644 index 0000000..8432229 --- /dev/null +++ b/lab-eddie/model/character.js @@ -0,0 +1,15 @@ +'use strict'; + +const uuidv4 = require('uuid/v4'); +const errHandle = require('../lib/errorHandle.js'); + +module.exports = function(name, healthPoints, experience, level) { + errHandle.normal(['name', 'healthPoints', 'experience', 'level'], arguments); + + this.id = uuidv4(); + this.name = name; + this.healthPoints = healthPoints; + this.experience = experience; + this.level = level + +}; \ No newline at end of file diff --git a/lab-eddie/model/dog.js b/lab-eddie/model/dog.js new file mode 100644 index 0000000..7ff8839 --- /dev/null +++ b/lab-eddie/model/dog.js @@ -0,0 +1,14 @@ +'use strict'; + +const uuidv4 = require('uuid/v4'); +const errHandle = require('../lib/errorHandle.js'); + +module.exports = function(name, breed, age) { + errHandle.normal(['name', 'breed', 'age'], arguments); + + this.id = uuidv4(); + this.name = name; + this.breed = breed; + this.age = age; + +}; \ No newline at end of file diff --git a/lab-eddie/model/employee.js b/lab-eddie/model/employee.js new file mode 100644 index 0000000..12f0593 --- /dev/null +++ b/lab-eddie/model/employee.js @@ -0,0 +1,15 @@ +'use strict'; + +const uuidv4 = require('uuid/v4'); +const errHandle = require('../lib/errorHandle.js'); + +module.exports = function(name, position, pay) { + errHandle.normal(['name', 'position', 'pay'], arguments); + + this.id = uuidv4(); + this.name = name; + this.position = position; + this.pay = pay; + this.fired = false + +}; \ No newline at end of file diff --git a/lab-eddie/model/person.js b/lab-eddie/model/person.js new file mode 100644 index 0000000..7a07b79 --- /dev/null +++ b/lab-eddie/model/person.js @@ -0,0 +1,14 @@ +'use strict'; + +const uuidv4 = require('uuid/v4'); +const errHandle = require('../lib/errorHandle.js'); + +module.exports = function(first, last, age, job) { + errHandle.normal(['first', 'last', 'age', 'job'], arguments); + + this.id = uuidv4(); + this.first = first; + this.last = last + this.age = age; + this.job = job; +}; \ No newline at end of file diff --git a/lab-eddie/package.json b/lab-eddie/package.json new file mode 100644 index 0000000..6fda210 --- /dev/null +++ b/lab-eddie/package.json @@ -0,0 +1,22 @@ +{ + "name": "lab-eddie", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "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/lab-eddie/server.js b/lab-eddie/server.js new file mode 100644 index 0000000..80770f0 --- /dev/null +++ b/lab-eddie/server.js @@ -0,0 +1,18 @@ +'use strict' + +const http = require('http'); +const Router = require('./lib/router.js'); +const PORT = process.env.PORT || 5000; +const modelPaths = require('./lib/model-paths.js') +const router = new Router(); + +const modelKeys = Object.keys(modelPaths.models); +modelKeys.forEach(key => { + modelPaths.allRoutes(key, router); +}) + +const server = http.createServer(router.route()); + +server.listen(PORT, () => { + console.log('server on at port:', PORT); +}); \ No newline at end of file diff --git a/lab-eddie/test/car-test.js b/lab-eddie/test/car-test.js new file mode 100644 index 0000000..1e58e47 --- /dev/null +++ b/lab-eddie/test/car-test.js @@ -0,0 +1,58 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; + +require('../server.js'); + +describe('car Routes', function() { + var car = null; + + describe('POST: /api/car', function() { + it('should return a car', function(done) { + request.post('localhost:3000/api/car') + .send({ make: 'Toyota', model: '4runner', year: 1987, color: 'black' }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.make).to.equal('Toyota'); + expect(res.body.model).to.equal('4runner'); + expect(res.body.color).to.equal('black'); + expect(res.body.year).to.equal(1987); + console.log('POST request car:', res.body); + car = res.body; + done(); + }); + }); + }); + describe('GET: /api/car', function() { + it('should return a car', function(done) { + request.get(`localhost:3000/api/car?id=${car.id}`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.make).to.equal('Toyota'); + expect(res.body.model).to.equal('4runner'); + expect(res.body.year).to.equal(1987); + expect(res.body.color).to.equal('black'); + console.log('GET request car:', res.body); + done(); + }); + }); + }); + describe('DELETE: /api/car', function() { + it('should return a an empty object', function(done) { + request.delete(`localhost:3000/api/car?id=${car.id}`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(202); + expect(res.body.make).to.equal(undefined); + expect(res.body.model).to.equal(undefined); + expect(res.body.year).to.equal(undefined); + expect(res.body.color).to.equal(undefined); + console.log('DELETE request car:', res.body); + done(); + }); + }); + }); +}); diff --git a/lab-eddie/test/person-test.js b/lab-eddie/test/person-test.js new file mode 100644 index 0000000..a835e02 --- /dev/null +++ b/lab-eddie/test/person-test.js @@ -0,0 +1,70 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; + +require('../server.js'); + +describe('person Routes', function() { + var person = null; + + describe('POST: /api/person', function() { + it('should return a person', function(done) { + request.post('localhost:3000/api/person') + .send({ first: 'eddie', last: 'del rio', age: 28, job: 'bum' }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.first).to.equal('eddie'); + expect(res.body.last).to.equal('del rio'); + expect(res.body.age).to.equal(28); + expect(res.body.job).to.equal('bum'); + console.log('POST request person:', res.body); + person = res.body; + done(); + }); + }); + }); + describe('GET: /api/person (No IDs)', function() { + it('should return an array of IDs', function(done) { + request.get('localhost:3000/api/person') + .end((err, res) => { + if(err) return done(err); + expect(Array.isArray(res.body)).to.equal(true); + expect(res.body.includes(person.id)).to.equal(true); + console.log('Array of IDs for person: ', res.body) + done() + }) + }) + }) + describe('GET: /api/person', function() { + it('should return a person', function(done) { + request.get(`localhost:3000/api/person?id=${person.id}`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.first).to.equal('eddie'); + expect(res.body.last).to.equal('del rio'); + expect(res.body.age).to.equal(28); + expect(res.body.job).to.equal('bum'); + console.log('GET request person:', res.body); + done(); + }); + }); + }); + describe('DELETE: /api/person', function() { + it('should return a an empty object', function(done) { + request.delete(`localhost:3000/api/person?id=${person.id}`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(202); + expect(res.body.first).to.equal(undefined); + expect(res.body.last).to.equal(undefined); + expect(res.body.age).to.equal(undefined); + expect(res.body.job).to.equal(undefined); + console.log('DELETE request person:', res.body); + done(); + }); + }); + }); +});