From e10af0199fda54ba58156076370663b20682d553 Mon Sep 17 00:00:00 2001 From: Wade Date: Fri, 27 Dec 2019 00:08:27 +0800 Subject: [PATCH 1/5] feat: restful api, services, and db config for player --- .babelrc | 3 +++ .gitignore | 4 +++- package.json | 15 +++++++++--- server.js | 11 --------- src/controllers/player_controller.js | 34 ++++++++++++++++++++++++++++ src/dao/database/mongodb.js | 14 ++++++++++++ src/dao/player.js | 23 +++++++++++++++++++ src/routers/player_router.js | 16 +++++++++++++ src/server.js | 21 +++++++++++++++++ src/services/player_service.js | 26 +++++++++++++++++++++ 10 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 .babelrc delete mode 100755 server.js create mode 100644 src/controllers/player_controller.js create mode 100644 src/dao/database/mongodb.js create mode 100644 src/dao/player.js create mode 100644 src/routers/player_router.js create mode 100644 src/server.js create mode 100644 src/services/player_service.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..1320b9a --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env"] +} diff --git a/.gitignore b/.gitignore index 3b9771e..0b1c9d4 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,6 @@ bin/Release .idea # Mac files -.DS_Store \ No newline at end of file +.DS_Store +dist +config.js diff --git a/package.json b/package.json index 7cd51e6..195fa01 100755 --- a/package.json +++ b/package.json @@ -5,16 +5,25 @@ "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:prod": "NODE_ENV=production node server.js", + "start": "NODE_ENV=development nodemon --exec babel-node src/server.js", + "build": "babel src --out-dir dist", + "clean": "rimraf dist", + "serve": "node dist/server.js", "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { + "body-parser": "^1.19.0", "express": "^4.16.4", "mongoose": "^5.4.8" }, "devDependencies": { - "chai": "^4.2.0" + "@babel/cli": "^7.7.7", + "@babel/core": "^7.7.7", + "@babel/node": "^7.7.7", + "@babel/preset-env": "^7.7.7", + "chai": "^4.2.0", + "nodemon": "^2.0.2", + "rimraf": "^3.0.0" }, "engines": { "node": ">=10.15.0" diff --git a/server.js b/server.js deleted file mode 100755 index 72e5b39..0000000 --- a/server.js +++ /dev/null @@ -1,11 +0,0 @@ -const express = require('express'); - -const app = express(); - -app.get('/', (req, res) => { - res.json({"message": "Building a RESTful CRUD API with Node.js, Express/Koa and MongoDB."}); -}); - -app.listen(3000, () => { - console.log("Server is listening on port 3000"); -}); \ No newline at end of file diff --git a/src/controllers/player_controller.js b/src/controllers/player_controller.js new file mode 100644 index 0000000..93c4d97 --- /dev/null +++ b/src/controllers/player_controller.js @@ -0,0 +1,34 @@ +import { + findAllPlayers, + findPlayerById, + createPlayer, + updatePlayer, + deletePlayer +} from "../services/player_service"; + +export default { + getAll: async (req, res, next) => { + const players = await findAllPlayers(); + res.json(players); + }, + getById: async (req, res, next) => { + const { playerId } = req.params; + const player = await findPlayerById(playerId); + res.json(player); + }, + create: async (req, res, next) => { + const data = req.body; + const result = await createPlayer(data); + res.json(result); + }, + update: async (req, res, next) => { + const data = req.body; + const result = await updatePlayer(data); + res.json(result); + }, + delete: async (req, res, next) => { + const { playerId } = req.params; + const result = await deletePlayer(playerId); + res.json(result); + } +}; diff --git a/src/dao/database/mongodb.js b/src/dao/database/mongodb.js new file mode 100644 index 0000000..c013a7b --- /dev/null +++ b/src/dao/database/mongodb.js @@ -0,0 +1,14 @@ +import mongoose from "mongoose"; +import { mongodbUri } from "./config"; + +console.log(process.env.NODE_ENV) +const uri = mongodbUri[process.env.NODE_ENV]; + +if (!uri || uri.length === 0) throw new Error("Undefined node environment"); + +mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true }); +mongoose.connection.on("error", e => + console.error("MongoDB connection error.", e.toString()) +); + +export default mongoose; diff --git a/src/dao/player.js b/src/dao/player.js new file mode 100644 index 0000000..2cb165b --- /dev/null +++ b/src/dao/player.js @@ -0,0 +1,23 @@ +import mongoose from './database/mongodb'; + +const schema = mongoose.Schema( + { + id: { + type: Number, + required: true + }, + name: { + type: String, + required: true + }, + position: { + type: String, + required: true + } + }, + { + timestamps: true + } +); + +export default mongoose.model("Player", schema); diff --git a/src/routers/player_router.js b/src/routers/player_router.js new file mode 100644 index 0000000..cd5b30c --- /dev/null +++ b/src/routers/player_router.js @@ -0,0 +1,16 @@ +import express from "express"; +import player from "../controllers/player_controller"; + +const router = express.Router(); + +router.get("/", player.getAll); + +router.get("/:playerId", player.getById); + +router.post("/", player.create); + +router.put("/", player.update); + +router.delete("/:playerId", player.delete); + +export default router; \ No newline at end of file diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..a668410 --- /dev/null +++ b/src/server.js @@ -0,0 +1,21 @@ +import express from "express"; +import bodyParser from "body-parser"; +import playerRouter from "./routers/player_router"; + +const app = express(); + +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); + +app.use("/player", playerRouter); + +app.get("/", (req, res) => { + res.json({ + message: + "Building a RESTful CRUD API with Node.js, Express/Koa and MongoDB." + }); +}); + +app.listen(3000, () => { + console.log("Server is listening on port 3000"); +}); diff --git a/src/services/player_service.js b/src/services/player_service.js new file mode 100644 index 0000000..fd34b16 --- /dev/null +++ b/src/services/player_service.js @@ -0,0 +1,26 @@ +import Player from "../dao/player"; + +export const findAllPlayers = async () => { + const players = await Player.find(); + return players; +}; + +export const findPlayerById = async id => { + const player = await Player.findOne({ id }); + return player; +}; + +export const createPlayer = async doc => { + const result = await Player.create(doc); + return result; +}; + +export const updatePlayer = async doc => { + const result = await Player.updateOne({ id: doc.id }, doc); + return result; +}; + +export const deletePlayer = async id => { + const result = await Player.deleteOne({ id }); + return result; +}; From 78c1b60e8afbeac0fe0dff673d8d9aa081a55ff4 Mon Sep 17 00:00:00 2001 From: Wade Date: Fri, 27 Dec 2019 18:24:36 +0800 Subject: [PATCH 2/5] feat: implement validation functions --- package.json | 1 + src/controllers/player_controller.js | 40 +++++++++++++++++++--------- src/dao/player.js | 9 +++---- src/routers/player_router.js | 21 +++++++++++---- src/utils/validation_schema.js | 34 +++++++++++++++++++++++ 5 files changed, 83 insertions(+), 22 deletions(-) create mode 100644 src/utils/validation_schema.js diff --git a/package.json b/package.json index 195fa01..768230f 100755 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "body-parser": "^1.19.0", "express": "^4.16.4", + "express-validator": "^6.3.0", "mongoose": "^5.4.8" }, "devDependencies": { diff --git a/src/controllers/player_controller.js b/src/controllers/player_controller.js index 93c4d97..c9fe5fa 100644 --- a/src/controllers/player_controller.js +++ b/src/controllers/player_controller.js @@ -12,23 +12,39 @@ export default { res.json(players); }, getById: async (req, res, next) => { - const { playerId } = req.params; - const player = await findPlayerById(playerId); - res.json(player); + try { + const { playerId } = req.params; + const player = await findPlayerById(playerId); + res.json(player); + } catch (e) { + next(e); + } }, create: async (req, res, next) => { - const data = req.body; - const result = await createPlayer(data); - res.json(result); + try { + const data = req.body; + const result = await createPlayer(data); + res.json(result); + } catch (e) { + next(e); + } }, update: async (req, res, next) => { - const data = req.body; - const result = await updatePlayer(data); - res.json(result); + try { + const data = req.body; + const result = await updatePlayer(data); + res.json(result); + } catch (e) { + next(e); + } }, delete: async (req, res, next) => { - const { playerId } = req.params; - const result = await deletePlayer(playerId); - res.json(result); + try { + const { playerId } = req.params; + const result = await deletePlayer(playerId); + res.json(result); + } catch (e) { + next(e); + } } }; diff --git a/src/dao/player.js b/src/dao/player.js index 2cb165b..268a2d3 100644 --- a/src/dao/player.js +++ b/src/dao/player.js @@ -1,18 +1,17 @@ -import mongoose from './database/mongodb'; +import mongoose from "./database/mongodb"; const schema = mongoose.Schema( { id: { - type: Number, - required: true + type: Number }, name: { type: String, - required: true + required: [true, "name is required"] }, position: { type: String, - required: true + enum: ["C", "PF", "SF", "PG", "SG"] } }, { diff --git a/src/routers/player_router.js b/src/routers/player_router.js index cd5b30c..9bd95ea 100644 --- a/src/routers/player_router.js +++ b/src/routers/player_router.js @@ -1,16 +1,27 @@ import express from "express"; +import { validationResult } from "express-validator"; +import { playerIdSchema, playerSchema } from "../utils/validation_schema"; import player from "../controllers/player_controller"; const router = express.Router(); router.get("/", player.getAll); -router.get("/:playerId", player.getById); +router.get("/:playerId", playerIdSchema, player.getById); -router.post("/", player.create); +router.post("/", playerSchema, player.create); -router.put("/", player.update); +router.put("/", playerSchema, player.update); -router.delete("/:playerId", player.delete); +router.delete("/:playerId", playerIdSchema, player.delete); -export default router; \ No newline at end of file +router.use((err, req, res, next) => { + const errors = validationResult(req).formatWith(({ param, msg }) => ({ + [param]: msg + })); + if (!errors.isEmpty()) + res.status(405).json(errors.array({ onlyFirstError: true })); + else res.status(400).json({ errors: "something went wrong" }); +}); + +export default router; diff --git a/src/utils/validation_schema.js b/src/utils/validation_schema.js new file mode 100644 index 0000000..a2ed8b0 --- /dev/null +++ b/src/utils/validation_schema.js @@ -0,0 +1,34 @@ +import { checkSchema } from "express-validator"; + +export const playerIdSchema = checkSchema({ + playerId: { + in: "params", + exists: true, + errorMessage: "must provide a player id" + } +}); + +export const playerSchema = checkSchema({ + id: { + in: "body", + optional: true, + isInt: { + errorMessage: "id must be an integer" + } + }, + name: { + in: "body", + exists: true, + isString: true, + notEmpty: true, + errorMessage: "name is required" + }, + position: { + in: "body", + optional: true, + matches: { + options: [/\b(?:C|PF|SF|PG|SG)\b/], + errorMessage: "position must be one of C, PF, SF, PG, SG" + } + } +}); From abffff7c1a6bcde4423fc189de9ddc0098def69b Mon Sep 17 00:00:00 2001 From: jweiwu Date: Sat, 28 Dec 2019 19:03:22 +0800 Subject: [PATCH 3/5] fix: create config.js to avoid crashing server --- src/dao/database/config.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/dao/database/config.js diff --git a/src/dao/database/config.js b/src/dao/database/config.js new file mode 100644 index 0000000..577a920 --- /dev/null +++ b/src/dao/database/config.js @@ -0,0 +1,5 @@ +export const mongodbUri = { + test: "", + development: "mongodb://127.0.0.1:27017/054cf1d4-0eac-407d-ab22-d33197b1d306?", + production: "mongodb://127.0.0.1:27017/9f0710f2-be19-4045-954b-a2e3fcdcc639?" +}; From 1f687359cc2da978db6a90fdb74f8514116e01ca Mon Sep 17 00:00:00 2001 From: Wade Date: Sun, 29 Dec 2019 18:38:41 +0800 Subject: [PATCH 4/5] feat: unit test of player via mocha --- .babelrc | 3 +- package.json | 13 ++++++--- src/dao/database/mongodb.js | 15 +++++----- src/server.js | 9 ++++-- test/player_test.js | 55 +++++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 test/player_test.js diff --git a/.babelrc b/.babelrc index 1320b9a..c043004 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,4 @@ { - "presets": ["@babel/preset-env"] + "presets": ["@babel/preset-env"], + "plugins": [["@babel/plugin-transform-runtime"]] } diff --git a/package.json b/package.json index 768230f..ad96442 100755 --- a/package.json +++ b/package.json @@ -8,10 +8,11 @@ "start": "NODE_ENV=development nodemon --exec babel-node src/server.js", "build": "babel src --out-dir dist", "clean": "rimraf dist", - "serve": "node dist/server.js", - "test": "echo \"Error: no test specified\" && exit 1" + "serve": "NODE_ENV=production node dist/server.js", + "test": "NODE_ENV=test mocha --require @babel/register --exit" }, "dependencies": { + "@babel/runtime": "^7.7.7", "body-parser": "^1.19.0", "express": "^4.16.4", "express-validator": "^6.3.0", @@ -21,10 +22,14 @@ "@babel/cli": "^7.7.7", "@babel/core": "^7.7.7", "@babel/node": "^7.7.7", + "@babel/plugin-transform-runtime": "^7.7.6", "@babel/preset-env": "^7.7.7", - "chai": "^4.2.0", + "@babel/register": "^7.7.7", + "mocha": "^6.2.2", + "mongodb-memory-server": "^6.2.0", "nodemon": "^2.0.2", - "rimraf": "^3.0.0" + "rimraf": "^3.0.0", + "should": "^13.2.3" }, "engines": { "node": ">=10.15.0" diff --git a/src/dao/database/mongodb.js b/src/dao/database/mongodb.js index c013a7b..9215029 100644 --- a/src/dao/database/mongodb.js +++ b/src/dao/database/mongodb.js @@ -1,14 +1,15 @@ import mongoose from "mongoose"; import { mongodbUri } from "./config"; -console.log(process.env.NODE_ENV) -const uri = mongodbUri[process.env.NODE_ENV]; +export const connectDatabase = () => { + const uri = mongodbUri[process.env.NODE_ENV]; -if (!uri || uri.length === 0) throw new Error("Undefined node environment"); + if (!uri || uri.length === 0) throw new Error("Undefined node environment"); -mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true }); -mongoose.connection.on("error", e => - console.error("MongoDB connection error.", e.toString()) -); + mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true }); + mongoose.connection.on("error", e => + console.error("MongoDB connection error.", e.toString()) + ); +}; export default mongoose; diff --git a/src/server.js b/src/server.js index a668410..161ef7b 100644 --- a/src/server.js +++ b/src/server.js @@ -1,12 +1,13 @@ import express from "express"; import bodyParser from "body-parser"; import playerRouter from "./routers/player_router"; +import { connectDatabase } from "./dao/database/mongodb"; const app = express(); +const port = process.env.NODE_ENV === "production" ? 80 : 3000; app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); - app.use("/player", playerRouter); app.get("/", (req, res) => { @@ -16,6 +17,8 @@ app.get("/", (req, res) => { }); }); -app.listen(3000, () => { - console.log("Server is listening on port 3000"); +app.listen(port, () => { + console.log(`Server is listening on port ${port}`); }); + +connectDatabase(); diff --git a/test/player_test.js b/test/player_test.js new file mode 100644 index 0000000..065e4c8 --- /dev/null +++ b/test/player_test.js @@ -0,0 +1,55 @@ +import should from "should"; +import { MongoMemoryServer } from "mongodb-memory-server"; +import { mongodbUri } from "../src/dao/database/config"; +import { connectDatabase } from "../src/dao/database/mongodb"; +import { + createPlayer, + findPlayerById, + updatePlayer, + deletePlayer +} from "../src/services/player_service"; + +describe("#NBA player RESTful API", async () => { + before(async () => { + const mongod = new MongoMemoryServer(); + const uri = await mongod.getConnectionString(); + mongodbUri.test = uri; + connectDatabase(); + }); + + it("should return a object when the player has been created", async () => { + const doc = { + id: 123, + name: "Wade", + position: "PG" + }; + const player = await createPlayer(doc); + should(player).have.property("id", 123); + should(player).have.property("name", "Wade"); + should(player).have.property("position", "PG"); + }); + + it("should return a object of the player 123", async () => { + const player = await findPlayerById(123); + should(player).have.property("id", 123); + should(player).have.property("name", "Wade"); + should(player).have.property("position", "PG"); + }); + + it("should return a object when the player has been updated", async () => { + const doc = { + id: 123, + name: "LeBron", + position: "PF" + }; + const result = await updatePlayer(doc); + should(result).have.property("n", 1); + should(result).have.property("nModified", 1); + }); + + it("should return a object when the player has been deleted", async () => { + const result = await deletePlayer(123); + should(result).have.property("n", 1); + should(result).have.property("deletedCount", 1); + }); +}); From 39861fb081336a17c0fd54b26fd7e14ae4139316 Mon Sep 17 00:00:00 2001 From: Wade Date: Sun, 29 Dec 2019 18:52:00 +0800 Subject: [PATCH 5/5] refactor: move the validation errors controller out of the player router --- src/controllers/error_controller.js | 10 ++++++++++ src/routers/player_router.js | 13 +++---------- 2 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 src/controllers/error_controller.js diff --git a/src/controllers/error_controller.js b/src/controllers/error_controller.js new file mode 100644 index 0000000..14ca852 --- /dev/null +++ b/src/controllers/error_controller.js @@ -0,0 +1,10 @@ +import { validationResult } from "express-validator"; + +export const playerValidationErrors = (err, req, res, next) => { + const errors = validationResult(req).formatWith(({ param, msg }) => ({ + [param]: msg + })); + if (!errors.isEmpty()) + res.status(405).json(errors.array({ onlyFirstError: true })); + else res.status(400).json({ errors: "something went wrong" }); +}; diff --git a/src/routers/player_router.js b/src/routers/player_router.js index 9bd95ea..6e2769e 100644 --- a/src/routers/player_router.js +++ b/src/routers/player_router.js @@ -1,7 +1,7 @@ import express from "express"; -import { validationResult } from "express-validator"; -import { playerIdSchema, playerSchema } from "../utils/validation_schema"; import player from "../controllers/player_controller"; +import { playerIdSchema, playerSchema } from "../utils/validation_schema"; +import { playerValidationErrors } from "../controllers/error_controller"; const router = express.Router(); @@ -15,13 +15,6 @@ router.put("/", playerSchema, player.update); router.delete("/:playerId", playerIdSchema, player.delete); -router.use((err, req, res, next) => { - const errors = validationResult(req).formatWith(({ param, msg }) => ({ - [param]: msg - })); - if (!errors.isEmpty()) - res.status(405).json(errors.array({ onlyFirstError: true })); - else res.status(400).json({ errors: "something went wrong" }); -}); +router.use(playerValidationErrors); export default router;