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..aa783c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,120 @@ + +# Created by https://www.gitignore.io/api/node,macos,windows,linux + +### Node ### +# Logs +logs +*.log +npm-debug.log* +node_modules +coverage + +# 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 + + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +### Linux ### +*~ + + +### Extra ignore ### +.console.js + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* diff --git a/README.md b/README.md new file mode 100644 index 0000000..691786d --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Vanilla Ski Report REST api + +## About +The vanilla Ski Report REST api is built to allow consumers to get valuable ski report data about any area they want to ski in. The api will respond to any of the following commands `GET`, `POST`, `DELETE`. The routes on the api are currently set to be reachable at `localhost:/api/ski-data`. In order for this API to work you will need to `npm i` to install `node-uuid` and `bluebird`, which is required to run this API. You will also need HTTPie installed globally on your machine in order for the API responses to reach your terminal correctly. + +## Current Version (0.1.0) +* The current version of this application will persist user data locally on the users file system. Users can `POST`, `GET`, and `DELETE` files. + +## Setting up Vanilla Ski Report REST api on your local machine +* Fork this repo +* `git clone` the forked copy to your local machine +* Node is required to run the server. Confirm you have `node` installed on your local machine by typing `npm -v` into your terminal. If you don't have node installed please follow the instructions [here](https://nodejs.org/en/). +* Install the dependencies of `node-uuid` and `bluebird` by running `npm i`. +* In order to turn on the server you will need to run either `nodemon server.js` or `node server.js` if you do not have nodemon installed globally. + * When you start the server via `node server.js` the port number should be printed to the terminal console. You will need to provide this to any users wanting to connect. + +## API Commands +* `POST`: `http POST localhost:/api/ski-data location='' rating=` + * This will return a header with status code and a JSON representation of the data you just added. + * The `POST` will write a file to the `../data/location/` file saved with `uuid` as the file name. +* `GET`: `http localhost:/api/ski-data?id=` + * This will return a header with status code and a JSON representation of the data you just requested. + * If you request a file via and `id` that does not exist then the api will return 404. + * If you run a bad `GET` method and do not pass an `id` the api will return 400. +* `DELETE`: `http DELETE localhost:/api/ski-data?id=` + * This will return a header with a status code of 204. + * The object deleted will be removed for the database. +* `Mocha`: this will run tests set up to validate that the code is working as expected. + * at the time of publication of this README.md all tests are currently passing. diff --git a/data/location/82007ecc-4bf0-4207-8b59-f6b1c09b06a0.json b/data/location/82007ecc-4bf0-4207-8b59-f6b1c09b06a0.json new file mode 100644 index 0000000..7ac39f9 --- /dev/null +++ b/data/location/82007ecc-4bf0-4207-8b59-f6b1c09b06a0.json @@ -0,0 +1 @@ +{"id":"82007ecc-4bf0-4207-8b59-f6b1c09b06a0","location":"Mt baker","rating":10} \ No newline at end of file diff --git a/data/location/8b6a4e5c-0bc0-4d57-978d-9fa88fb67e4c.json b/data/location/8b6a4e5c-0bc0-4d57-978d-9fa88fb67e4c.json new file mode 100644 index 0000000..a091d46 --- /dev/null +++ b/data/location/8b6a4e5c-0bc0-4d57-978d-9fa88fb67e4c.json @@ -0,0 +1 @@ +{"id":"8b6a4e5c-0bc0-4d57-978d-9fa88fb67e4c","location":"mt baker","rating":"5"} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..e1b95d1 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,23 @@ +'use strict'; + +const gulp = require('gulp'); +const eslint = require('gulp-eslint'); +const mocha = require('mocha'); + +gulp.task('lint', function() { + gulp.src(['**/*.js', '!node_modules']) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +gulp.task('test', function() { + gulp.src(['./test/*test.js','!node_modules'], {read:false}) + .pipe(mocha({report: 'spec'})); +}); + +gulp.task('dev', function() { + gulp.watch(['**/*.js', '!node_modules'], ['lint', 'test']); +}); + +gulp.task('default', ['dev']); diff --git a/lab-08_in_memory_resource_api.md b/lab-08_in_memory_resource_api.md index c488aaa..d64a202 100644 --- a/lab-08_in_memory_resource_api.md +++ b/lab-08_in_memory_resource_api.md @@ -18,11 +18,11 @@ * `README.md` ## Description -* Create the following directories to organize your code: - * `lib` - * `model` - * `test` -* Create an HTTP server using the native NodeJS `http` module +* [x] Create the following directories to organize your code: + * [x] `lib` + * [x] `model` + * [x] `test` +* [x] Create an HTTP server using the native NodeJS `http` module * Create an object constructor that creates a _simple resource_ with at least 3 properties * include an `id` property that is set to a unique id (**hint:** you'll need to use `node-uuid`) * include two additional properties of your choice (ex: name, content, etc.) diff --git a/lib/data.js b/lib/data.js new file mode 100644 index 0000000..d1d27e1 --- /dev/null +++ b/lib/data.js @@ -0,0 +1,36 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), {suffix: 'Prom'}); + +module.exports = exports = {}; + +exports.setData = (schemaName, item) => { + if(!schemaName) return Promise.reject(new Error('Expected schema name but was not provided one')); + if(!item) return Promise.reject(new Error('Expected item but was not provided one')); + + let json = JSON.stringify(item); + return fs.writeFileProm(`${__dirname}/../data/${schemaName}/${item.id}.json`, json) + .then( () => item) + .catch( err => Promise.reject(err)); +}; + +exports.getData = (schemaName, id) => { + if(!schemaName) return Promise.reject(new Error('Expected schema name but was not provided one')); + if(!id) return Promise.reject(new Error('Expected id but was not provided one')); + + return fs.readFileProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .then( data => { + let item = JSON.parse(data.toString()); + return item; + }) + .catch( err => Promise.reject(err)); +}; + +exports.removeData = (schemaName, id) => { + if(!schemaName) return Promise.reject(new Error('Expected schema name but was not provided one')); + if(!id) return Promise.reject(new Error('Expected id but was not provided one')); + + return fs.unlinkProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .catch( err => Promise.reject(err)); +}; diff --git a/lib/parse-json.js b/lib/parse-json.js new file mode 100644 index 0000000..e6874e8 --- /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(); + }); +}; diff --git a/lib/parse-url.js b/lib/parse-url.js new file mode 100644 index 0000000..3c88889 --- /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); +}; diff --git a/lib/response.js b/lib/response.js new file mode 100644 index 0000000..3c126ae --- /dev/null +++ b/lib/response.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = exports = {}; + +exports.JSON = function(res, status, data) { + res.writeHead(status, {'Content-Type': 'application/json'}); + res.write(JSON.stringify(data)); + res.end(); +}; + +exports.text = function(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..b514f4a --- /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 response = require('../lib/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'); + response.text(res, 404, 'Not found'); + }) + .catch( err => { + console.error(err); + response.text(res, 400, 'Bad request'); + }); + }; +}; diff --git a/model/ski-data.js b/model/ski-data.js new file mode 100644 index 0000000..8d7e072 --- /dev/null +++ b/model/ski-data.js @@ -0,0 +1,12 @@ +'use strict'; + +const uuid = require('node-uuid'); + +module.exports = function(location, rating) { + if(!location) throw new Error('expected location'); + if(!rating) throw new Error('expected rating'); + + this.id = uuid.v4(); + this.location = location; + this.rating = rating; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..698676e --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "08-vanilla_rest_api", + "version": "1.0.0", + "description": "", + "main": "gulpfile.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/jonathanheemstra/08-vanilla_rest_api.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/jonathanheemstra/08-vanilla_rest_api/issues" + }, + "homepage": "https://github.com/jonathanheemstra/08-vanilla_rest_api#readme", + "devDependencies": { + "chai": "^3.5.0", + "eslint": "^3.12.2", + "gulp": "^3.9.1", + "gulp-eslint": "^3.0.1", + "mocha": "^3.2.0", + "superagent": "^3.3.0" + }, + "dependencies": { + "bluebird": "^3.4.6", + "node-uuid": "^1.4.7" + } +} diff --git a/routes/routes.js b/routes/routes.js new file mode 100644 index 0000000..a81e96d --- /dev/null +++ b/routes/routes.js @@ -0,0 +1,49 @@ +'use strict'; + +const data = require('../lib/data.js'); +const SkiData = require('../model/ski-data.js'); +const response = require('../lib/response.js'); + +module.exports = function(routes){ + routes.get('/api/ski-data', (req, res) => { + if(req.url.query.id) { + data.getData('location', req.url.query.id) + .then( skiData => { + response.JSON(res, 200, skiData); + }) + .catch( () => { + response.text(res, 404, 'Not found'); + }); + return; + } + if(!req.url.query.id) { + response.text(res, 400, 'Bad request'); + } + }); + + routes.post('/api/ski-data', (req, res) => { + try { + var skiData = new SkiData(req.body.location, req.body.rating); + data.setData('location', skiData); + response.JSON(res, 200, skiData); + } catch(err) { + response.text(res, 400, 'Bad request'); + } + }); + + routes.delete('/api/ski-data', (req, res) => { + if(req.url.query.id) { + data.removeData('location', req.url.query.id) + .then( () => { + response.JSON(res, 204, 'No content'); + }) + .catch( () => { + response.text(res, 404, 'Not found'); + }); + return; + } + if(!req.url.query.id) { + response.text(res, 400, 'Bad request'); + } + }); +}; diff --git a/server.js b/server.js new file mode 100644 index 0000000..76c4bf1 --- /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 routes = new Router(); + +require('./routes/routes.js')(routes); + +const server = http.createServer(routes.route()); + +server.listen(PORT, () => { + console.log(`Server is up on ${PORT}`); +}); diff --git a/test/tests.js b/test/tests.js new file mode 100644 index 0000000..1bbe748 --- /dev/null +++ b/test/tests.js @@ -0,0 +1,69 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; +const PORT = process.env.PORT || 3000; +// require('../server.js'); + +describe('Data routes', function() { + var data = null; + + describe('POST: /api/ski-data', function() { + it('should return a data object', function(done) { + request.post(`localhost:${PORT}/api/ski-data`) + .send({location: 'Mt baker', rating: 10}) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.location).to.equal('Mt baker'); + expect(res.body.rating).to.equal(10); + data = res.body; + done(); + }); + }); + it('should return a data object', function(done) { + request.post(`localhost:${PORT}/api/ski-data`) + .send({location: 'mt baker'}) + .end((res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + describe('GET: /api/ski-data', function() { + it('should return a data object', function(done) { + request.get(`localhost:${PORT}/api/ski-data?id=${data.id}`) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.location).to.equal('Mt baker'); + expect(res.body.rating).to.equal(10); + done(); + }); + }); + it('should return a 404 error', function(done) { + request.get(`localhost:${PORT}/api/ski-data?id=123456789`) + .end((res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + it('should return a 400 error', function(done) { + request.get(`localhost:${PORT}/api/ski-data`) + .end((res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + describe('DELETE: /api/ski-data', function() { + it('should delete a data object', function(done) { + request.delete(`localhost:${PORT}/api/ski-data?id=${data.id}`) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); +});