From b5f2e039ba1f6fd624a5fee9a3c733aa008bbd18 Mon Sep 17 00:00:00 2001 From: iantsai Date: Thu, 26 Dec 2019 18:32:04 +0800 Subject: [PATCH] node exam submit --- config/index.js | 4 + package.json | 14 +- server.js | 32 ++++- swagger/api/player.js | 22 ++++ swagger/controllers/Player.js | 105 +++++++++------ swagger/db/index.js | 23 ++++ swagger/db/models/index.js | 25 ++++ swagger/db/models/player.js | 19 +++ swagger/db/models/player_id.js | 14 ++ swagger/service/PlayerService.js | 48 ++++--- swagger/utils/validate.js | 11 ++ test/api/player.js | 213 +++++++++++++++++++++++++++++++ 12 files changed, 459 insertions(+), 71 deletions(-) create mode 100644 config/index.js create mode 100644 swagger/api/player.js create mode 100644 swagger/db/index.js create mode 100644 swagger/db/models/index.js create mode 100644 swagger/db/models/player.js create mode 100644 swagger/db/models/player_id.js create mode 100644 swagger/utils/validate.js create mode 100644 test/api/player.js diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..7e12c72 --- /dev/null +++ b/config/index.js @@ -0,0 +1,4 @@ +module.exports = { + mongoUri: 'mongodb://node-exam:nodeexam123@ds017165.mlab.com:17165/node-exam', + mongoTestUri: 'mongodb://node-exam:nodeexam123@ds115022.mlab.com:15022/node-exam-test' +} \ No newline at end of file diff --git a/package.json b/package.json index 7cd51e6..7c83cba 100755 --- a/package.json +++ b/package.json @@ -5,16 +5,22 @@ "description": "Building a RESTful CRUD API with Node.js, Express/Koa and MongoDB.", "main": "server.js", "scripts": { - "start": "NODE_ENV=development node server.js", + "start": "NODE_ENV=development nodemon server.js", "start:prod": "NODE_ENV=production node server.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "NODE_ENV=test mocha --recursive --exit" }, "dependencies": { + "body-parse": "^0.1.0", + "cors": "^2.8.5", "express": "^4.16.4", - "mongoose": "^5.4.8" + "mongoose": "^5.8.3" }, "devDependencies": { - "chai": "^4.2.0" + "chai": "^4.2.0", + "mocha": "^6.2.2", + "mockgoose": "^8.0.4", + "nodemon": "^2.0.2", + "supertest": "^4.0.2" }, "engines": { "node": ">=10.15.0" diff --git a/server.js b/server.js index 72e5b39..df4bb2b 100755 --- a/server.js +++ b/server.js @@ -1,11 +1,37 @@ const express = require('express'); +const bodyParser = require('body-parser'); +const cors = require('cors'); +const db = require('./swagger/db'); +const PlayerRoutes = require('./swagger/api/player'); +const Model = require('./swagger/db/models'); const app = express(); +// use body parser middleware +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +// cors +app.use(cors()); + +// Router app.get('/', (req, res) => { res.json({"message": "Building a RESTful CRUD API with Node.js, Express/Koa and MongoDB."}); }); +app.use('/player', PlayerRoutes) + +// error handler + +// db models +global.db = new Model().getDb(); + +// connect db and start the serve +const port = process.env.PORT || 3000 +db.connect() + .then(() => { + app.listen(port, () => { + console.log(`Server is listening on port ${port}!!`); + }); + }) -app.listen(3000, () => { - console.log("Server is listening on port 3000"); -}); \ No newline at end of file +module.exports = app \ No newline at end of file diff --git a/swagger/api/player.js b/swagger/api/player.js new file mode 100644 index 0000000..66b7d5d --- /dev/null +++ b/swagger/api/player.js @@ -0,0 +1,22 @@ +const express = require('express'); +const router = express.Router(); +const PlayController = require('../controllers/Player'); + +// create player +router.post('/', (res, req, next) => { + PlayController.addPlayer(res, req, next) +}) +// delete player +router.delete('/:playerId', (res, req, next) => { + PlayController.deletePlayer(res, req, next) +}) +// get player +router.get('/:playerId', (res, req, next) => { + PlayController.getPlayerById(res, req, next) +}) +// update player +router.put('/', (res, req, next) => { + PlayController.updatePlayer(res, req, next) +}) + +module.exports = router \ No newline at end of file diff --git a/swagger/controllers/Player.js b/swagger/controllers/Player.js index 95cda8c..0f929c8 100755 --- a/swagger/controllers/Player.js +++ b/swagger/controllers/Player.js @@ -1,48 +1,77 @@ 'use strict'; -var utils = require('../utils/writer.js'); -var Player = require('../service/PlayerService'); +// var utils = require('../utils/writer.js'); +const Player = require('../service/PlayerService'); +const validNameAndPosition = require('../utils/validate'); -module.exports.addPlayer = function addPlayer (req, res, next) { - var body = req.swagger.params['body'].value; - Player.addPlayer(body) - .then(function (response) { - utils.writeJson(res, response); - }) - .catch(function (response) { - utils.writeJson(res, response); - }); +module.exports.addPlayer = async function addPlayer (req, res, next) { + const body = req.body; + // validate + if(validNameAndPosition(req.body)) { + res.status(405).json({ message: 'Invalid Input' }) + } else { + // add id automatically + let dbPlayerId = await global.db.PlayerId.findOne({active: true}) + if(!dbPlayerId) { + dbPlayerId = await new global.db.PlayerId({ + number: 0, + active: true + }).save() + } + dbPlayerId.number += 1 + await dbPlayerId.save() + await Player.addPlayer({ ...body, id: dbPlayerId.number }); + res.status(200).json({ playerId: dbPlayerId.number }); + } }; -module.exports.deletePlayer = function deletePlayer (req, res, next) { - var playerId = req.swagger.params['playerId'].value; - Player.deletePlayer(playerId) - .then(function (response) { - utils.writeJson(res, response); - }) - .catch(function (response) { - utils.writeJson(res, response); - }); +module.exports.deletePlayer = async function deletePlayer (req, res, next) { + const playerId = req.params.playerId + // validate: playerId must be number + if(!isNaN(parseInt(playerId))) { + const status = await Player.deletePlayer(playerId) + if(status === 404) { + res.status(404).json({ message: 'Player not found' }) + } else { + res.status(200).end(); + } + } else { + res.status(400).json({ message: 'Invalid ID supplied' }) + } }; -module.exports.getPlayerById = function getPlayerById (req, res, next) { - var playerId = req.swagger.params['playerId'].value; - Player.getPlayerById(playerId) - .then(function (response) { - utils.writeJson(res, response); - }) - .catch(function (response) { - utils.writeJson(res, response); - }); +module.exports.getPlayerById = async function getPlayerById (req, res, next) { + const playerId = req.params.playerId + // validate: playerId must be number + if(!isNaN(parseInt(playerId))) { + const foundPlayer = await Player.getPlayerById(playerId) + if(foundPlayer === 404) { + res.status(404).json({ message: 'Player not found' }) + } else { + res.status(200).json({ + id: foundPlayer.id, + name: foundPlayer.name, + position: foundPlayer.position + }); + } + } else { + res.status(400).json({ message: 'Invalid ID supplied' }) + } }; -module.exports.updatePlayer = function updatePlayer (req, res, next) { - var body = req.swagger.params['body'].value; - Player.updatePlayer(body) - .then(function (response) { - utils.writeJson(res, response); - }) - .catch(function (response) { - utils.writeJson(res, response); - }); +module.exports.updatePlayer = async function updatePlayer (req, res, next) { + const body = req.body + // validate: body.id must be number + if(!isNaN(parseInt(body.id))) { + // validate name and position + if(validNameAndPosition(req.body)) { + res.status(405).json({ message: 'Validation exception' }) + } else { + const status = await Player.updatePlayer(body) + if(status === 404) res.status(404).json({ message: 'Player not found' }) + else res.status(200).end(); + } + } else { + res.status(400).json({ message: 'Invalid ID supplied' }) + } }; diff --git a/swagger/db/index.js b/swagger/db/index.js new file mode 100644 index 0000000..18dbc82 --- /dev/null +++ b/swagger/db/index.js @@ -0,0 +1,23 @@ +const mongoose = require('mongoose') +const mongoUri = require('../../config').mongoUri +const mongoTestUri = require('../../config').mongoTestUri + +async function connect() { + return new Promise((resolve, reject) => { + const URI = process.env.NODE_ENV === 'test'? mongoTestUri: mongoUri + mongoose.connect(URI, { useNewUrlParser: true, useCreateIndex: true }) + .then((res, err) => { + if(err) return reject(err) + resolve() + }) + }) +} + +function close() { + return mongoose.disconnect() +} + +module.exports = { + connect, + close +} \ No newline at end of file diff --git a/swagger/db/models/index.js b/swagger/db/models/index.js new file mode 100644 index 0000000..888b743 --- /dev/null +++ b/swagger/db/models/index.js @@ -0,0 +1,25 @@ +const fs = require('fs') +const path = require('path') + +module.exports = class Models { + constructor() { + let db = {} + + fs + .readdirSync(__dirname) + .filter(file => { + return (file.indexOf('.') !== 0) && (file !== 'index.js'); + }) + .forEach(file => { + const model = require(path.join(__dirname, file)) + // modelName + db[model.modelName] = model + }) + + this.db = db + } + + getDb() { + return this.db + } +} \ No newline at end of file diff --git a/swagger/db/models/player.js b/swagger/db/models/player.js new file mode 100644 index 0000000..d1bc909 --- /dev/null +++ b/swagger/db/models/player.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); + +const playerShcema = new mongoose.Schema({ + id: { + type: Number, + unique: true, + require: true + }, + name: { + type: String, + require: true + }, + position: { + type: String, + enum: ['C', 'PF', 'SF', 'PG', 'SG'] + } +}) + +module.exports = mongoose.model('Player', playerShcema) \ No newline at end of file diff --git a/swagger/db/models/player_id.js b/swagger/db/models/player_id.js new file mode 100644 index 0000000..4a49815 --- /dev/null +++ b/swagger/db/models/player_id.js @@ -0,0 +1,14 @@ +const mongoose = require('mongoose'); + +const playerIdShcema = new mongoose.Schema({ + number: { + type: Number, + require: true + }, + active: { + type: Boolean, + default: true + } +}) + +module.exports = mongoose.model('PlayerId', playerIdShcema) \ No newline at end of file diff --git a/swagger/service/PlayerService.js b/swagger/service/PlayerService.js index 319fb15..a6af23f 100755 --- a/swagger/service/PlayerService.js +++ b/swagger/service/PlayerService.js @@ -8,10 +8,13 @@ * body Player Player object * no response value expected for this operation **/ -exports.addPlayer = function(body) { - return new Promise(function(resolve, reject) { - resolve(); - }); +exports.addPlayer = async function(body) { + const newPlayer = new global.db.Player({ + id: body.id, + name: body.name, + position: body.position + }) + return newPlayer.save() } @@ -22,10 +25,10 @@ exports.addPlayer = function(body) { * playerId Long Player id to delete * no response value expected for this operation **/ -exports.deletePlayer = function(playerId) { - return new Promise(function(resolve, reject) { - resolve(); - }); +exports.deletePlayer = async function(playerId) { + const result = await global.db.Player.deleteOne({id: playerId}) + if(result.deletedCount === 0) return 404 + return 200 } @@ -36,20 +39,10 @@ exports.deletePlayer = function(playerId) { * playerId Long ID of player to return * returns Player **/ -exports.getPlayerById = function(playerId) { - return new Promise(function(resolve, reject) { - var examples = {}; - examples['application/json'] = { - "name" : "LeBron", - "id" : 0, - "position" : "C" -}; - if (Object.keys(examples).length > 0) { - resolve(examples[Object.keys(examples)[0]]); - } else { - resolve(); - } - }); +exports.getPlayerById = async function(playerId) { + const dbPlayer = await global.db.Player.findOne({id: playerId}) + if(!dbPlayer) return 404 + return dbPlayer } @@ -60,9 +53,12 @@ exports.getPlayerById = function(playerId) { * body Player Player object that needs to be added to the team * no response value expected for this operation **/ -exports.updatePlayer = function(body) { - return new Promise(function(resolve, reject) { - resolve(); - }); +exports.updatePlayer = async function(body) { + const dbPlayer = await global.db.Player.findOne({id: body.id}) + if(!dbPlayer) return 404 + dbPlayer.name = body.name || dbPlayer.name + dbPlayer.position = body.position || dbPlayer.position + await dbPlayer.save() + return 200 } diff --git a/swagger/utils/validate.js b/swagger/utils/validate.js new file mode 100644 index 0000000..e6fc6c2 --- /dev/null +++ b/swagger/utils/validate.js @@ -0,0 +1,11 @@ +module.exports = validNameAndPosition = (body) => { + // !req.body.name || typeof req.body.name !== 'string' ||foundIndex === -1 + const noName = !body.name + const notStringName = body.name && typeof body.name !== 'string' + const notValidPosiiton = body.position && ( + ['C', 'PF', 'SF', 'PG', 'SG'].findIndex(position => ( + position === body.position + ))) === -1 + + return noName || notStringName || notValidPosiiton +} \ No newline at end of file diff --git a/test/api/player.js b/test/api/player.js new file mode 100644 index 0000000..c0d3571 --- /dev/null +++ b/test/api/player.js @@ -0,0 +1,213 @@ +const expect = require('chai').expect; +const request = require('supertest'); +const app = require('../../server'); +const conn = require('../../swagger/db'); + +describe('player api test', function() { + this.timeout(10000) + // before(async () => { + // try { + // await conn.connect(); + // await Promise.all(Object.keys(global.db).map(async (modelName) => { + // global.db[modelName].deleteMany({}, (err) => { + // if(err) throw new Error(err) + // }) + // })); + // } catch(err) { + // throw new Error(err) + // } + // }) + before((done) => { + conn.connect() + .then(() => { + Promise.all(Object.keys(global.db).map(async (modelName) => { + global.db[modelName].deleteMany({}, (err) => { + if(err) throw new Error(err) + }) + })).then(() => { + done() + }).catch((err) => { + done(err) + }) + }) + }) + + after((done) => { + Promise.all(Object.keys(global.db).map(async (modelName) => { + global.db[modelName].deleteMany({}, (err) => { + if(err) throw new Error(err) + }) + })).then(() => { + conn.close() + .then(() => done()) + .catch(err => done(err)) + }) + }) + + // get + it('should get empty Player arr', (done) => { + global.db.Player.find({}, (err, arr) => { + expect(arr.length).to.equal(0) + done() + }).catch(err => { + done(err) + }) + }) + it('should get Wade', (done) => { + request(app).post('/player').send({ + name: 'Wade', + position: 'SG' + }).then(res => { + request(app).get(`/player/${res.body.playerId}`) + .then(res => { + expect(res.status).to.equal(200) + expect(res.body).to.have.property('id') + expect(res.body).to.have.property('name') + expect(res.body).to.have.property('position') + expect(res.body.name).to.equal('Wade') + done() + }).catch(err => { + done(err) + }) + }) + }) + it('should return Invalid ID supplied and 400', (done) => { + request(app).get(`/player/wrongId`) + .then(res => { + expect(res.status).to.equal(400) + expect(res.body.message).to.equal('Invalid ID supplied') + done() + }) + }) + it('should return Player not found and 404', (done) => { + request(app).get(`/player/-1`) + .then(res => { + expect(res.status).to.equal(404) + expect(res.body.message).to.equal('Player not found') + done() + }).catch(err => { + done(err) + }) + }) + + + // post + it('should add PlayerId automatically and create a new Player', (done) => { + request(app).post('/player').send({ + name: 'Lebron', + position: 'SF' + }).then(res => { + expect(res.status).to.equal(200) + done() + }).catch(err => { + done(err) + }) + }) + it('position invalid, should get Invalid Input message', (done) => { + request(app).post('/player').send({ + name: 'Lebron', + position: 'DD' + }).then(res => { + expect(res.status).to.equal(405) + expect(res.body.message).to.equal('Invalid Input') + done() + }).catch(err => { + done(err) + }) + }) + it('name invalid, should get Invalid Input message', (done) => { + request(app).post('/player').send({ + name: '', + position: 'DD' + }).then(res => { + expect(res.status).to.equal(405) + expect(res.body.message).to.equal('Invalid Input') + done() + }).catch(err => { + done(err) + }) + }) + + // update + it('should return Invalid ID supplied and 400', (done) => { + request(app).put('/player').send({ + id: 'wrongId', + name: 'Kobe' + }).then(res => { + expect(res.status).to.equal(400) + expect(res.body.message).to.equal('Invalid ID supplied') + done() + }).catch(err => { + done(err) + }) + }) + it('should return Player not found and 404', (done) => { + request(app).put('/player').send({ + id: -1, + name: 'Kobe' + }).then(res => { + expect(res.status).to.equal(404) + expect(res.body.message).to.equal('Player not found') + done() + }).catch(err => { + done(err) + }) + }) + it('invalid name: should return 405', (done) => { + global.db.Player.findOne({name: 'Wade'}, (err, player) => { + request(app).put('/player').send({ + id: player.id, + name: '' + }).then(res => { + expect(res.status).to.equal(405) + expect(res.body.message).to.equal('Validation exception') + done() + }) + }).catch(err => { + done(err) + }) + }) + it('success, should return 200 status code', (done) => { + global.db.Player.findOne({name: 'Wade'}, (err, player) => { + request(app).put('/player').send({ + id: player.id, + name: 'Kobe' + }).then(res => { + expect(res.status).to.equal(200) + done() + }) + }).catch(err => { + done(err) + }) + }) + + // delete + it('should return Invalid ID supplied and 400', (done) => { + request(app).delete('/player/wrongId') + .then(res => { + expect(res.status).to.equal(400) + expect(res.body.message).to.equal('Invalid ID supplied') + done() + }).catch(err => { + done(err) + }) + }) + it('should return Player not found and 404', (done) => { + request(app).delete('/player/-1') + .then(res => { + expect(res.status).to.equal(404) + done() + }).catch(err => { + done(err) + }) + }) + it('success, should return 200', (done) => { + request(app).delete('/player/1') + .then(res => { + expect(res.status).to.equal(200) + done() + }).catch(err => { + done(err) + }) + }) +}) \ No newline at end of file