diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..c043004 --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@babel/preset-env"], + "plugins": [["@babel/plugin-transform-runtime"]] +} 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..ad96442 100755 --- a/package.json +++ b/package.json @@ -5,16 +5,31 @@ "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", - "test": "echo \"Error: no test specified\" && exit 1" + "start": "NODE_ENV=development nodemon --exec babel-node src/server.js", + "build": "babel src --out-dir dist", + "clean": "rimraf dist", + "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", "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/plugin-transform-runtime": "^7.7.6", + "@babel/preset-env": "^7.7.7", + "@babel/register": "^7.7.7", + "mocha": "^6.2.2", + "mongodb-memory-server": "^6.2.0", + "nodemon": "^2.0.2", + "rimraf": "^3.0.0", + "should": "^13.2.3" }, "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/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/controllers/player_controller.js b/src/controllers/player_controller.js new file mode 100644 index 0000000..c9fe5fa --- /dev/null +++ b/src/controllers/player_controller.js @@ -0,0 +1,50 @@ +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) => { + try { + const { playerId } = req.params; + const player = await findPlayerById(playerId); + res.json(player); + } catch (e) { + next(e); + } + }, + create: async (req, res, next) => { + try { + const data = req.body; + const result = await createPlayer(data); + res.json(result); + } catch (e) { + next(e); + } + }, + update: async (req, res, next) => { + try { + const data = req.body; + const result = await updatePlayer(data); + res.json(result); + } catch (e) { + next(e); + } + }, + delete: async (req, res, next) => { + try { + const { playerId } = req.params; + const result = await deletePlayer(playerId); + res.json(result); + } catch (e) { + next(e); + } + } +}; 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?" +}; diff --git a/src/dao/database/mongodb.js b/src/dao/database/mongodb.js new file mode 100644 index 0000000..9215029 --- /dev/null +++ b/src/dao/database/mongodb.js @@ -0,0 +1,15 @@ +import mongoose from "mongoose"; +import { mongodbUri } from "./config"; + +export const connectDatabase = () => { + 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..268a2d3 --- /dev/null +++ b/src/dao/player.js @@ -0,0 +1,22 @@ +import mongoose from "./database/mongodb"; + +const schema = mongoose.Schema( + { + id: { + type: Number + }, + name: { + type: String, + required: [true, "name is required"] + }, + position: { + type: String, + enum: ["C", "PF", "SF", "PG", "SG"] + } + }, + { + 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..6e2769e --- /dev/null +++ b/src/routers/player_router.js @@ -0,0 +1,20 @@ +import express from "express"; +import player from "../controllers/player_controller"; +import { playerIdSchema, playerSchema } from "../utils/validation_schema"; +import { playerValidationErrors } from "../controllers/error_controller"; + +const router = express.Router(); + +router.get("/", player.getAll); + +router.get("/:playerId", playerIdSchema, player.getById); + +router.post("/", playerSchema, player.create); + +router.put("/", playerSchema, player.update); + +router.delete("/:playerId", playerIdSchema, player.delete); + +router.use(playerValidationErrors); + +export default router; diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..161ef7b --- /dev/null +++ b/src/server.js @@ -0,0 +1,24 @@ +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) => { + res.json({ + message: + "Building a RESTful CRUD API with Node.js, Express/Koa and MongoDB." + }); +}); + +app.listen(port, () => { + console.log(`Server is listening on port ${port}`); +}); + +connectDatabase(); 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; +}; 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" + } + } +}); 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); + }); +});