diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..393ef53 --- /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 \ No newline at end of file diff --git a/data/pokemon/27f12373-fb8a-47ec-946b-dcb4d54cea8e.json b/data/pokemon/27f12373-fb8a-47ec-946b-dcb4d54cea8e.json new file mode 100644 index 0000000..bbb6692 --- /dev/null +++ b/data/pokemon/27f12373-fb8a-47ec-946b-dcb4d54cea8e.json @@ -0,0 +1 @@ +{"id":"27f12373-fb8a-47ec-946b-dcb4d54cea8e","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/283d1fc5-501a-4a46-8c78-85c46723884c.json b/data/pokemon/283d1fc5-501a-4a46-8c78-85c46723884c.json new file mode 100644 index 0000000..372ac32 --- /dev/null +++ b/data/pokemon/283d1fc5-501a-4a46-8c78-85c46723884c.json @@ -0,0 +1 @@ +{"id":"283d1fc5-501a-4a46-8c78-85c46723884c","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/32136a12-7ed5-4d20-bd4d-847c32a94fb9.json b/data/pokemon/32136a12-7ed5-4d20-bd4d-847c32a94fb9.json new file mode 100644 index 0000000..32f6556 --- /dev/null +++ b/data/pokemon/32136a12-7ed5-4d20-bd4d-847c32a94fb9.json @@ -0,0 +1 @@ +{"id":"32136a12-7ed5-4d20-bd4d-847c32a94fb9","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/4827c263-cafe-4501-ab0a-7910d552d5be.json b/data/pokemon/4827c263-cafe-4501-ab0a-7910d552d5be.json new file mode 100644 index 0000000..909b251 --- /dev/null +++ b/data/pokemon/4827c263-cafe-4501-ab0a-7910d552d5be.json @@ -0,0 +1 @@ +{"id":"4827c263-cafe-4501-ab0a-7910d552d5be","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/517c4f33-7c28-46ae-bc7c-911747cd360e.json b/data/pokemon/517c4f33-7c28-46ae-bc7c-911747cd360e.json new file mode 100644 index 0000000..a5c34ba --- /dev/null +++ b/data/pokemon/517c4f33-7c28-46ae-bc7c-911747cd360e.json @@ -0,0 +1 @@ +{"id":"517c4f33-7c28-46ae-bc7c-911747cd360e","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/53e99c40-5d82-43f6-8fb8-09d93f385d7d.json b/data/pokemon/53e99c40-5d82-43f6-8fb8-09d93f385d7d.json new file mode 100644 index 0000000..efd5e21 --- /dev/null +++ b/data/pokemon/53e99c40-5d82-43f6-8fb8-09d93f385d7d.json @@ -0,0 +1 @@ +{"id":"53e99c40-5d82-43f6-8fb8-09d93f385d7d","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/59f45ea3-d3d0-4f53-a183-b358c523c061.json b/data/pokemon/59f45ea3-d3d0-4f53-a183-b358c523c061.json new file mode 100644 index 0000000..15f59f7 --- /dev/null +++ b/data/pokemon/59f45ea3-d3d0-4f53-a183-b358c523c061.json @@ -0,0 +1 @@ +{"id":"59f45ea3-d3d0-4f53-a183-b358c523c061","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/5f67961e-09d4-4959-9841-b490de0ded3a.json b/data/pokemon/5f67961e-09d4-4959-9841-b490de0ded3a.json new file mode 100644 index 0000000..76988a0 --- /dev/null +++ b/data/pokemon/5f67961e-09d4-4959-9841-b490de0ded3a.json @@ -0,0 +1 @@ +{"id":"5f67961e-09d4-4959-9841-b490de0ded3a","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/75de85b5-da56-4f29-baf7-7c5728e8e4cd.json b/data/pokemon/75de85b5-da56-4f29-baf7-7c5728e8e4cd.json new file mode 100644 index 0000000..5833e65 --- /dev/null +++ b/data/pokemon/75de85b5-da56-4f29-baf7-7c5728e8e4cd.json @@ -0,0 +1 @@ +{"id":"75de85b5-da56-4f29-baf7-7c5728e8e4cd","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/7ac8b986-dccd-4090-aba8-a12eee4eec5f.json b/data/pokemon/7ac8b986-dccd-4090-aba8-a12eee4eec5f.json new file mode 100644 index 0000000..707662e --- /dev/null +++ b/data/pokemon/7ac8b986-dccd-4090-aba8-a12eee4eec5f.json @@ -0,0 +1 @@ +{"id":"7ac8b986-dccd-4090-aba8-a12eee4eec5f","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/838924e1-00dd-4f27-93a6-5f3dff4903f3.json b/data/pokemon/838924e1-00dd-4f27-93a6-5f3dff4903f3.json new file mode 100644 index 0000000..919d02e --- /dev/null +++ b/data/pokemon/838924e1-00dd-4f27-93a6-5f3dff4903f3.json @@ -0,0 +1 @@ +{"id":"838924e1-00dd-4f27-93a6-5f3dff4903f3","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/865f2841-a5e3-43bd-a806-ee153f4d0868.json b/data/pokemon/865f2841-a5e3-43bd-a806-ee153f4d0868.json new file mode 100644 index 0000000..547b050 --- /dev/null +++ b/data/pokemon/865f2841-a5e3-43bd-a806-ee153f4d0868.json @@ -0,0 +1 @@ +{"id":"865f2841-a5e3-43bd-a806-ee153f4d0868","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/8a909a8d-ce13-4fc4-88c7-96f47f4f756d.json b/data/pokemon/8a909a8d-ce13-4fc4-88c7-96f47f4f756d.json new file mode 100644 index 0000000..a0f63f5 --- /dev/null +++ b/data/pokemon/8a909a8d-ce13-4fc4-88c7-96f47f4f756d.json @@ -0,0 +1 @@ +{"id":"8a909a8d-ce13-4fc4-88c7-96f47f4f756d","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/b0a22f67-1fca-4fb6-8ec9-dca9c7f80063.json b/data/pokemon/b0a22f67-1fca-4fb6-8ec9-dca9c7f80063.json new file mode 100644 index 0000000..eab2c22 --- /dev/null +++ b/data/pokemon/b0a22f67-1fca-4fb6-8ec9-dca9c7f80063.json @@ -0,0 +1 @@ +{"id":"b0a22f67-1fca-4fb6-8ec9-dca9c7f80063","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/b27f533e-4d3a-4787-b410-793ec5c5974f.json b/data/pokemon/b27f533e-4d3a-4787-b410-793ec5c5974f.json new file mode 100644 index 0000000..33591fc --- /dev/null +++ b/data/pokemon/b27f533e-4d3a-4787-b410-793ec5c5974f.json @@ -0,0 +1 @@ +{"id":"b27f533e-4d3a-4787-b410-793ec5c5974f","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/b441890a-b8c4-4ab5-acab-a87b808049f2.json b/data/pokemon/b441890a-b8c4-4ab5-acab-a87b808049f2.json new file mode 100644 index 0000000..7fb0132 --- /dev/null +++ b/data/pokemon/b441890a-b8c4-4ab5-acab-a87b808049f2.json @@ -0,0 +1 @@ +{"id":"b441890a-b8c4-4ab5-acab-a87b808049f2","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/b6d8c895-501e-4f8b-87de-930c1b1ec07d.json b/data/pokemon/b6d8c895-501e-4f8b-87de-930c1b1ec07d.json new file mode 100644 index 0000000..7c857c4 --- /dev/null +++ b/data/pokemon/b6d8c895-501e-4f8b-87de-930c1b1ec07d.json @@ -0,0 +1 @@ +{"id":"b6d8c895-501e-4f8b-87de-930c1b1ec07d","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/dfe51568-81de-4841-a4a4-2b4ae8e75c77.json b/data/pokemon/dfe51568-81de-4841-a4a4-2b4ae8e75c77.json new file mode 100644 index 0000000..69023d9 --- /dev/null +++ b/data/pokemon/dfe51568-81de-4841-a4a4-2b4ae8e75c77.json @@ -0,0 +1 @@ +{"id":"dfe51568-81de-4841-a4a4-2b4ae8e75c77","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/data/pokemon/f80e2854-a3a3-4134-8c53-f4d43c24b7f7.json b/data/pokemon/f80e2854-a3a3-4134-8c53-f4d43c24b7f7.json new file mode 100644 index 0000000..7517968 --- /dev/null +++ b/data/pokemon/f80e2854-a3a3-4134-8c53-f4d43c24b7f7.json @@ -0,0 +1 @@ +{"id":"f80e2854-a3a3-4134-8c53-f4d43c24b7f7","name":"test name","type":"test type","gen":"test gen"} \ No newline at end of file diff --git a/lib/parse-json.js b/lib/parse-json.js new file mode 100644 index 0000000..2502ad7 --- /dev/null +++ b/lib/parse-json.js @@ -0,0 +1,32 @@ +'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(); + }); +}; \ No newline at end of file diff --git a/lib/parse-url.js b/lib/parse-url.js new file mode 100644 index 0000000..563a7d2 --- /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 = function(req) { + req.url = parseUrl(req.url); + req.url.query = parseQuery(req.url.query); + return Promise.resolve(req); +}; \ No newline at end of file diff --git a/lib/response.js b/lib/response.js new file mode 100644 index 0000000..3814584 --- /dev/null +++ b/lib/response.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = exports = {}; + +exports.sendJSON = function(res, status, data) { + res.writeHead(status, { + 'Content-Type': 'application/json' + }); + + res.write(JSON.stringify(data)); + res.end(); +}; + +exports.sendText = function(res, status, msg) { + res.writeHead(status, { + 'Content-Type': 'text/plain' + }); + + res.write(msg); + res.end(); +}; \ No newline at end of file diff --git a/lib/router.js b/lib/router.js new file mode 100644 index 0000000..a03e664 --- /dev/null +++ b/lib/router.js @@ -0,0 +1,63 @@ +'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.put = function(endpoint, callback) { + this.routes.PUT[endpoint] = callback; +}; + +Router.prototype.delete = function(endpoint, callback) { + this.routes.DELETE[endpoint] = callback; +}; + +Router.prototype.post = function(endpoint, callback) { + this.routes.POST[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'); + + res.writeHead(404, { + 'Content-Type': 'text/plain' + }); + + res.write('route not found'); + res.end(); + }) + .catch(err => { + console.error(err); + + res.writeHead(400, { + 'Content-Type': 'text/plain' + }); + + res.write('bad request'); + res.end(); + }); + }; +}; \ No newline at end of file diff --git a/lib/storage.js b/lib/storage.js new file mode 100644 index 0000000..4ab7bcf --- /dev/null +++ b/lib/storage.js @@ -0,0 +1,33 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), {suffix: 'Prom'}); + +module.exports = exports = {}; + +exports.createItem = function(schemaName, item) { + if(!schemaName) return Promise.reject(new Error('not a schema name')); + if(!item) return Promise.reject(new Error('not an 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) { + if(!schemaName) return Promise.reject(new Error('not a schema name')); + if(!id) return Promise.reject(new Error('not 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(err); + } + }) + .catch(err => Promise.reject(err)); +}; + diff --git a/model/pokemon.js b/model/pokemon.js new file mode 100644 index 0000000..d4806cd --- /dev/null +++ b/model/pokemon.js @@ -0,0 +1,14 @@ +'use strict'; + +const uuidv4 = require('uuid/v4'); + +module.exports = function(name, type, gen) { + if(!name)throw new Error('expect Pokemon name'); + if(!type)throw new Error('expected Pokemon type'); + if(!gen)throw new Error('expected a generation'); + + this.id = uuidv4(); + this.name = name; + this.type = type; + this.gen = gen; +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b27dc3d --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "08-vanilla_rest_api", + "version": "1.0.0", + "description": "![cf](https://i.imgur.com/7v5ASc8.png) Lab 08: Vanilla REST API ======", + "main": "server.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "mocha", + "start": "node server.js", + "lint": "eslintrc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Loaye/08-vanilla_rest_api.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/Loaye/08-vanilla_rest_api/issues" + }, + "homepage": "https://github.com/Loaye/08-vanilla_rest_api#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/pokemon-route.js b/route/pokemon-route.js new file mode 100644 index 0000000..19af0ae --- /dev/null +++ b/route/pokemon-route.js @@ -0,0 +1,34 @@ +'use strict'; + +const storage = require('../lib/storage.js'); +const response = require('../lib/response.js'); +const Pokemon = require('../model/pokemon.js'); + +module.exports = function(router) { + router.get('/api/pokemon', function(req, res) { + if(req.url.query.id) { + storage.fetchItem('pokemon', req.url.query.id) + .then((pokemon) => { + response.sendJSON(res, 200, pokemon); + }) + .catch((err) => { + response.sendText(res, 404, 'not found'); + }); + + return; + } + + response.sendText(res, 400, 'bad request'); + }); + + router.post('/api/pokemon', function(req, res) { + try { + var pokemon = new Pokemon(req.body.name, req.body.type, req.body.gen); + storage.createItem('pokemon', pokemon); + response.sendJSON(res, 200, pokemon); + } catch(err) { + console.error(err); + response.sendText(res, 400, 'bad request'); + } + }); +}; \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..9a6a8ef --- /dev/null +++ b/server.js @@ -0,0 +1,14 @@ +'use strict'; + +const http = require('http'); +const Router = require('./lib/router.js'); +const PORT = process.env.PORT || 3000; +const router = new Router(); + +require('./route/pokemon-route.js')(router); + +const server = http.createServer(router.route()); + +server.listen(PORT, () => { + console.log('server up:', PORT); +}); \ No newline at end of file diff --git a/test/pokemon-route-test.js b/test/pokemon-route-test.js new file mode 100644 index 0000000..7955eff --- /dev/null +++ b/test/pokemon-route-test.js @@ -0,0 +1,71 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; + +require('../server.js'); + +describe('Pokemon Routes', function() { + var pokemon = null; + + describe('POST: /api/pokemon', function() { + it('should return a pokemon', function(done) { + request.post('localhost:8000/api/pokemon') + .send({name: 'test name', type: 'test type', gen: 'test gen'}) + .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.type).to.equal('test type'); + expect(res.body.gen).to.equal('test gen'); + pokemon = res.body; + done(); + }); + }); + }); + + describe('POST: /api', function() { + it('should return 400', function(done) { + request.post('localhost:8000/api/pokemon') + .send({}) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('GET: /api/pokemon', function() { + it('should return a pokemon', function(done){ + request.get(`localhost:8000/api/pokemon?id=${pokemon.id}`) + .end(function(err, res) { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('test name'); + expect(res.body.type).to.equal('test type'); + expect(res.body.gen).to.equal('test gen'); + console.log('GET request pokemon:', res.body); + done(); + }); + }); + }); + + describe('GET: /api/pokemon', function() { + it('should return 400 bad request', function(done) { + request.get('localhost:8000/api/pokemon?id=') + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + + it('should return 404', function(done) { + request.get('localhost:8000/api/pokemon?id=489') + .end((err, res) => { + expect(res.status).to.equal(404); + console.log('get 404'); + done(); + }); + }); + }); +}); \ No newline at end of file