diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..b663d77 --- /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" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/README.md b/README.md index 37f3822..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,60 +0,0 @@ -![CF](https://camo.githubusercontent.com/70edab54bba80edb7493cad3135e9606781cbb6b/687474703a2f2f692e696d6775722e636f6d2f377635415363382e706e67) 13: Single Resource Mongo and Express API -=== - -## 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 be able to work with the MongoDB database management system -* students will understand the primary concepts of working with a NoSQL database management system -* students will be able to create custom data models *(schemas)* through the use of mongoose.js -* students will be able to use mongoose.js helper methods for interacting with their database persistence layer - -## Requirements -#### Configuration -* `package.json` -* `.eslintrc` -* `.gitignore` -* `README.md` - * your `README.md` should include detailed instructions on how to use your API - -#### Feature Tasks -* create an HTTP Server using `express` -* create a resource **model** of your choice that uses `mongoose.Schema` and `mongoose.model` -* use the `body-parser` express middleware to parse the `req` body on `POST` and `PUT` requests -* use the npm `debug` module to log the functions and methods that are being used in your application -* use the express `Router` to create a route for doing **RESTFUL CRUD** operations against your _model_ - -## Server Endpoints -### `/api/resource-name` -* `POST` request - * should pass data as stringifed JSON in the body of a post request to create a new resource - -### `/api/resource-name/:id` -* `GET` request - * should pass the id of a resource through the url endpoint to get a resource - * **this should use `req.params`, not querystring parameters** -* `PUT` request - * should pass data as stringifed JSON in the body of a put request to update a pre-existing resource -* `DELETE` request - * should pass the id of a resource though the url endpoint to delete a resource - * **this should use `req.params`** - -### Tests -* create a test that will ensure that your API returns a status code of 404 for routes that have not been registered -* create a series of tests to ensure that your `/api/resource-name` endpoint responds as described for each condition below: - * `GET` - test 200, returns a resource with a valid body - * `GET` - test 404, respond with 'not found' for valid requests made with an id that was not found - * `PUT` - test 200, returns a resource with an updated body - * `PUT` - test 400, responds with 'bad request' if no request body was provided - * `PUT` - test 404, responds with 'not found' for valid requests made with an id that was not found - * `POST` - test 400, responds with 'bad request' if no request body was provided - * `POST` - test 200, returns a resource for requests made with a valid body - -### Bonus -* **2pts:** a `GET` request to `/api/resource-name` should return an array of stored resources diff --git a/lib/error-middleware.js b/lib/error-middleware.js new file mode 100644 index 0000000..21f7880 --- /dev/null +++ b/lib/error-middleware.js @@ -0,0 +1,37 @@ +'use strict'; + +const createError = require('http-errors'); +const debug = require('debug')('pokemon:error-middleware'); + +module.exports = function(err, req, res, next) { + console.error(err.message); + console.error(err.name); + + if(err.name === 'CastError'){ + err = createError(404, err.message); + res.status(err.status).send(err.message); + next(); + return; + } + + if(err.name === 'ValidationError'){ + err = createError(400, err.message); + res.status(err.status).send(err.message); + next(); + return; + } + + if(err.status) { + debug('user error'); + + res.status(err.status).send(err.name); + next(); + + return; + } + + debug('server error'); + err = createError(500, err.message); + res.status(err.status).send(err.name); + next(); +}; \ No newline at end of file diff --git a/model/pokemon.js b/model/pokemon.js new file mode 100644 index 0000000..4701fb7 --- /dev/null +++ b/model/pokemon.js @@ -0,0 +1,13 @@ +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const pokemonSchema = Schema({ + name: {type: String, required: true}, + type: {type: String, required: true}, + gen: {type: String, require: true}, + timestamp: {type: Date, required: true} +}); + +module.exports = mongoose.model('pokemon', pokemonSchema); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6a47e90 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "13-mongodb", + "version": "1.0.0", + "description": "![CF](https://camo.githubusercontent.com/70edab54bba80edb7493cad3135e9606781cbb6b/687474703a2f2f692e696d6775722e636f6d2f377635415363382e706e67) 13: Single Resource Mongo and Express API ===", + "main": "server.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "DEBUG='pokemon*' mocha", + "start": "DEBUG='pokemon*' node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Loaye/13-mongodb.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/Loaye/13-mongodb/issues" + }, + "homepage": "https://github.com/Loaye/13-mongodb#readme", + "dependencies": { + "bluebird": "^3.5.0", + "body-parser": "^1.17.2", + "cors": "^2.8.4", + "debug": "^2.6.8", + "express": "^4.15.3", + "mongoose": "^4.11.5", + "morgan": "^1.8.2" + }, + "devDependencies": { + "chai": "^4.1.0", + "mocha": "^3.5.0", + "superagent": "^3.5.2" + } +} diff --git a/route/pokemon-route.js b/route/pokemon-route.js new file mode 100644 index 0000000..0e229fa --- /dev/null +++ b/route/pokemon-route.js @@ -0,0 +1,53 @@ +'use strict'; + +const Router = require('express').Router; +const jsonParser = require('body-parser').json(); +const debug = require('debug')('pokemon:pokemon-router'); +const Pokemon = require('../model/pokemon.js'); +const pokemonRouter = module.exports = new Router(); + +pokemonRouter.post('/api/pokemon', jsonParser, function (req, res, next) { + debug('POST: /api/pokemon'); + + req.body.timestamp = new Date(); + new Pokemon(req.body).save() + .then(pokemon => res.json(pokemon)) + .catch(next); +}); + +pokemonRouter.get('/api/pokemon/:id', function (req, res, next) { + debug('GET: /api/pokemon/:id'); + Pokemon.findById(req.params.id) + .then(pokemon => res.json(pokemon)) + .catch(next); +}); + +pokemonRouter.put('/api/pokemon/:id', jsonParser, (req, res, next) => { + debug('PUT /api/pokemons/:id'); + + if (Object.keys(req.body).length === 0) { + Pokemon.findById(req.params.id) + .then(pokemon => { + res.status(400); + res.json(pokemon); + }) + .catch(next); + return; + } + + let options = { + runValidator: true, + new: true, + }; + + Pokemon.findByIdAndUpdate(req.params.id, req.body, options) + .then(pokemon => res.json(pokemon)) + .catch(next); +}); + +pokemonRouter.delete('/api/pokemon/:id', function (req, res, next) { + debug('GET: /api/pokemon/:id'); + + Pokemon.findByIdAndRemove(req.params.id) + .catch(next); +}); \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..44577d5 --- /dev/null +++ b/server.js @@ -0,0 +1,26 @@ +'use strict'; + +const express = require('express'); +const morgan = require('morgan'); +const cors = require('cors'); +const Promise = require('bluebird'); +const mongoose = require('mongoose'); +const debug = require('debug')('pokemon:server'); +const pokemonRouter = require('./route/pokemon-route.js'); +const errors = require('./lib/error-middleware.js'); + +const app = express(); +const PORT = process.env.PORT || 3000; +const MONGODB_URI = 'mongodb://localhost/pokemonlist'; + +mongoose.Promise = Promise; +mongoose.connect(MONGODB_URI); + +app.use(cors()); +app.use(morgan('dev')); +app.use(pokemonRouter); +app.use(errors); + +app.listen(PORT, () => { + debug(`listening on ${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..66ad749 --- /dev/null +++ b/test/pokemon-route-test.js @@ -0,0 +1,169 @@ +'use strict'; + +const expect = require('chai').expect; +const request = require('superagent'); +const Pokemon = require('../model/pokemon.js'); +const PORT = process.env.PORT || 3000; +const mongoose = require('mongoose'); + +mongoose.Promise = Promise; +require('../server.js'); + +const url = `http://localhost:${PORT}`; + +let tempPokemon; + +const examplePokemon = { + name: 'the pokemon name', + type: 'the type', + gen: 'the gen' +}; + +const newPokemon = { + name: 'the pokemon name', + type: 'the type', + gen: 'the gen' +}; + +describe('Pokemon Routes', function() { + describe('POST: /api/pokemon', function() { + describe('with a valid req body', function() { + after( done => { + if(this.tempPokemon) { + Pokemon.remove({}) + .then(() => done()) + .catch(done); + return; + } + done(); + }); + + it('should return a pokemon', done => { + request.post(`${url}/api/pokemon`) + .send(examplePokemon) + .end((err,res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('the pokemon name'); + expect(res.body.type).to.equal('the type'); + expect(res.body.gen).to.equal('the gen'); + this.tempPokemon = res.body; + done(); + }); + }); + }); + + describe('with an invalid request', function() { + it('should return 400', done => { + request.post(`${url}/api/pokemon`) + .send() + .end((err,res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + }); + + describe('GET: /api/pokemon/:id', function() { + describe('with a valid body', function() { + before(done => { + examplePokemon.timestamp = new Date(); + new Pokemon(examplePokemon).save() + .then(pokemon => { + this.tempPokemon = pokemon; + done(); + }) + .catch(done); + }); + + after(done => { + delete examplePokemon.timestamp; + if(this.tempPokemon) { + Pokemon.remove({}) + .then(() => done()) + .catch(done); + return; + } + done(); + }); + + it('should return a pokemon', done => { + request.get(`${url}/api/pokemon/${this.tempPokemon._id}`) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('the pokemon name'); + expect(res.body.type).to.equal('the type'); + expect(res.body.gen).to.equal('the gen'); + done(); + }); + }); + }); + + describe('with an invalid request', function(){ + it('should return 404', done => { + request.get(`${url}/api/pokemon/1236795`) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + }); + + describe('testing PUT /api/pokemon', () => { + before(done => { + examplePokemon.timestamp = new Date(); + new Pokemon(examplePokemon).save() + .then(pokemon => { + this.tempPokemon = pokemon; + done(); + }) + .catch(done); + }); + + after(done => { + delete examplePokemon.timestamp; + if (this.tempPokemon) { + Pokemon.remove({}) + .then(() => done()) + .catch(done); + return; + } + done(); + }); + it('should respond with a 200 status code and an updated pokemon object.', () => { + console.log(this.tempPokemon._id); + return request.put(`${url}/api/pokemon/${this.tempPokemon._id}`) + .send(newPokemon) + .then(res => { + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('the pokemon name'); + expect(res.body.type).to.equal('the type'); + expect(res.body.gen).to.equal('the gen'); + tempPokemon = res.body; + }); + }); + }); + + it('should respond with a 400 error code.', () => { + return request.post(`${url}/api/pokemon`) + .send(tempPokemon) + .then((res) => { + tempPokemon = res.body; + return request.put(`${url}/api/pokemon/${this.tempPokemon._id}`) + .send(null); + }) + .catch(err => { + expect(err.status).to.equal(400); + }); + }); + + it('should respond with a 404 error code if an ID is not found.', () => { + return request.get(`${url}/api/pokemon/12345`) + .catch(err => { + expect(err.status).to.equal(404); + }); + }); +}); \ No newline at end of file