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..8e1d00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,123 @@ + +# 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 ### +*~ + +# 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* + + +# exclude everything +data/* + +# exception to the rule +!data/.gitkeep diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb58a25 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# Single Resource Express API + +This project creates a single resource REST API using [Express.js](http://expressjs.com/) and allows users send POST, GET and DELETE requests related +to dogs through the terminal. +When sending requests, use filepath `/api/dog` with a query and a response will return with appropriate message/content. +You will need to [HTTPie](https://httpie.org/) to send requets through the terminal. + +## How to run + +Install any Dependencies from the `package.json` file into the project root +directory. Using [Node.js](https://nodejs.org/), to create a `package.json` file, enter command `npm init` in the project root. +You can run the command `npm i` to install all depenenedcies. Make sure to have a `data` folder in the root directory. +This is where POST requests will write to. + +## Running server + +Run the `server.js` file using command `node server.js` or `npm start`. In terminal, you should see `Server up: 8000` or +port that is set in your environmental variable in terminal. + +## Sending POST GET DELETE Request + +>POST Request + +In an new terminal window, send a `POST` request by using the command +`http POST localhost:8000/api/dog name= breed= color=`. +Example: `http POST localhost:8000/api/dog name='Buddy' breed='Golden Retriever' color='brown` +The POST request must include `name` `breed` and `color` parameters. +The successful response should return a JSON object with values you entered along with a unique `id` and +a status code of `200`. This will also create a new `.json` file into the `data` folder with the `id` +as the file name. + +![POST request screenshot](/assets/post-response-screenshot.png) + +>GET Request + +In an new terminal window, send a `GET` request by using the command `http localhost:8000/api/dog?id=`. +Example: `http localhost:8000/api/dog?id=00000000-c303-11e6-a4a3-73422de980bc` +The successful reponse should return a JSON object with a status of `200`. + +![GET request screenshot](/assets/get-response-screenshot.png) + +>DELETE Request + +In an new terminal window, send a `DELETE` request by using the command +`http DELETE localhost:8000/api/dog?id=`. +Example: `http DELETE localhost:8000/api/dog?id=00000000-c303-11e6-a4a3-73422de980bc` +The a successful response should return a `204` status code with no content. + +![DELETE request screenshot](/assets/delete-response-screenshot.png) + +## Closing server + +In server terminal, enter ```control``` + ```c```. diff --git a/assets/delete-response-screenshot.png b/assets/delete-response-screenshot.png new file mode 100755 index 0000000..daa6a69 Binary files /dev/null and b/assets/delete-response-screenshot.png differ diff --git a/assets/get-response-screenshot.png b/assets/get-response-screenshot.png new file mode 100755 index 0000000..3dcd5af Binary files /dev/null and b/assets/get-response-screenshot.png differ diff --git a/assets/post-response-screenshot.png b/assets/post-response-screenshot.png new file mode 100755 index 0000000..a530181 Binary files /dev/null and b/assets/post-response-screenshot.png differ diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 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..6799abc --- /dev/null +++ b/lib/storage.js @@ -0,0 +1,49 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), {suffix: 'Prom'}); +const createError = require('http-errors'); +const debug = require('debug')('dogs:storage'); + +module.exports = exports = {}; + +exports.createItem = function(schemaName, item) { + debug('createItem'); + + if(!schemaName) return Promise.reject(createError(400, 'expected schema name')); + if(!item) return Promise.reject(createError(400, 'expected 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 schema name')); + if(!id) return Promise.reject(createError(400, 'expected 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 schema name')); + if(!id) return Promise.reject(createError(400, 'expected id')); + + return fs.unlinkProm(`${__dirname}/../data/${schemaName}/${id}.json`) + + .catch(err => Promise.reject(createError(404, err.message))); +}; diff --git a/model/dogs.js b/model/dogs.js new file mode 100644 index 0000000..c206a73 --- /dev/null +++ b/model/dogs.js @@ -0,0 +1,43 @@ +'use strict'; + +const uuid = require('node-uuid'); +const createError = require('http-errors'); +const debug = require('debug')('dogs:dogs'); +const storage = require('../lib/storage.js'); + +const Dog = module.exports = function(name, breed, color) { + debug('dog constructor'); + + if(!name) throw createError(400, 'expected name'); + if(!breed) throw createError(400, 'expected breed'); + if(!color) throw createError(400, 'expected color'); + + this.id = uuid.v1(); + this.name = name; + this.breed = breed; + this.color = color; + +}; + +Dog.createDog = function(_dog) { + debug('createDog'); + + try { + let dog = new Dog(_dog.name, _dog.breed, _dog.color); + return storage.createItem('dog', dog); + } catch (err) { + return Promise.reject(err); + } +}; + +Dog.fetchDog = function(id) { + debug('fetchDog'); + + return storage.fetchItem('dog', id); +}; + +Dog.deleteDog = function(id) { + debug('deleteDog'); + + return storage.deleteItem('dog', id); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb74b95 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "11-express_single_resource_api", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "DEBUG='dog*' node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/peterkim2/11-express_single_resource_api.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/peterkim2/11-express_single_resource_api/issues" + }, + "homepage": "https://github.com/peterkim2/11-express_single_resource_api#readme", + "devDependencies": { + "chai": "^3.5.0", + "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" + }, + "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", + "superagent": "^3.3.1" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..4dd5e44 --- /dev/null +++ b/server.js @@ -0,0 +1,69 @@ +'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')('note:server'); + +const app = express(); +const Dog = require('./model/dogs.js'); +const PORT = process.env.PORT || 8000; + +app.use(morgan('dev')); + +app.get('/test', function(req,res) { + debug('Debugging /test route'); + res.json( {'msg': 'test route worked'}); +}); + +app.post('/api/dog', jsonParser, function(req, res, next) { + debug('POST: /api/dog'); + + Dog.createDog(req.body) + .then( dog => res.json(dog)) + .catch( err => next(err)); +}); + +app.get('/api/dog', function(req, res, next) { + debug('GET: /api/dog'); + + Dog.fetchDog(req.query.id) + .then( dog => res.json(dog)) + .catch( err => next(err)); +}); + +app.delete('/api/dog', function(req, res, next) { + debug('DELETE: /api/dog'); + + Dog.deleteDog(req.query.id) + .then( () => { + res.status(204); + res.end(); + }) + .catch( err => next(err)); +}); + +// eslint-disable-next-line +app.put('/api/dog', jsonParser, function(req, res, next) { + debug('PUT: /api/dog'); + +}); + +// 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, () => { + console.log(`server up on PORT: ${PORT}`); +}); diff --git a/test/dogs-route-test.js b/test/dogs-route-test.js new file mode 100644 index 0000000..d17e1ad --- /dev/null +++ b/test/dogs-route-test.js @@ -0,0 +1,81 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; +const PORT = process.env.PORT || 8000; + +require('../server.js'); + +describe('Dog Routes', function() { + var dog = null; + + describe('POST: /api/dog', function() { + it('should return a 200 with valid body', function(done) { + request.post(`localhost:${PORT}/api/dog`) + .send({name: 'test name', breed: 'test breed', color: 'test color'}) + .end((err, res) => { + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('test name'); + expect(res.body.breed).to.equal('test breed'); + expect(res.body.color).to.equal('test color'); + dog = res.body; + done(); + }); + }); + it('should return 400', function(done) { + request.post(`localhost:${PORT}/api/dog`) + .send({eyes: 'test eyes', feet: 'test feet', ears: 'test ears'}) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('GET: /api/dog', function() { + it('should return a dog', function(done) { + request.get(`localhost:${PORT}/api/dog?id=${dog.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.breed).to.equal('test breed'); + expect(res.body.color).to.equal('test color'); + done(); + }); + }); + it('should return 404', function (done) { + request.get(`localhost:${PORT}/api/dog=?bad`) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + it('should return 200', function(done) { + request.get(`localhost:${PORT}/api/dog?id=${dog.id}`) + .end((err, res) => { + expect(res.status).to.equal(200); + done(); + }); + }); + it('should return 400 if no id provided', function(done) { + request.get(`localhost:${PORT}/api/dog?id=`) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('DELETE: /api/dog', function() { + it('should delete dog', function(done) { + request.delete(`localhost:${PORT}/api/dog?id=${dog.id}`) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + +});