diff --git a/examples/clean-architecture/controllers/note.controller.js b/examples/clean-architecture/controllers/note.controller.js new file mode 100644 index 00000000000..52540a2e87b --- /dev/null +++ b/examples/clean-architecture/controllers/note.controller.js @@ -0,0 +1,40 @@ +var Note = require("../entities/note.entity"); + +/** + * Please, note that here I'm not using ES6 classes just for compatibility purposes + * In a real-world application, you should use ES6 classes + */ +function NoteController(noteUseCase) { + this.noteUseCase = noteUseCase; +} + +NoteController.prototype.getAll = function (req, res) { + var notes = this.noteUseCase.getAll(); + res.json(notes); +}; + +NoteController.prototype.getById = function (req, res) { + var noteId = parseInt(req.params.id); + var note = this.noteUseCase.getById(noteId); + res.json(note); +}; + +NoteController.prototype.create = function (req, res) { + var note = new Note(req.body.title, req.body.content); + var createdNote = this.noteUseCase.create(note); + res.status(201).json(createdNote); +}; + +NoteController.prototype.update = function (req, res) { + var noteId = parseInt(req.params.id); + this.noteUseCase.updateById(noteId, req.body.title, req.body.content); + res.json("Note updated"); +}; + +NoteController.prototype.delete = function (req, res) { + var noteId = parseInt(req.params.id); + this.noteUseCase.delete(noteId); + res.json("Note deleted"); +}; + +module.exports = NoteController; diff --git a/examples/clean-architecture/entities/note.entity.js b/examples/clean-architecture/entities/note.entity.js new file mode 100644 index 00000000000..5190c3e4622 --- /dev/null +++ b/examples/clean-architecture/entities/note.entity.js @@ -0,0 +1,43 @@ +var isEmptyString = function (str) { + return str === undefined || str === null || str.trim() === ""; +}; +var throwIfEmpty = function (str, field) { + if (isEmptyString(str)){ + var error = new Error("field '" + field + "' should not be empty"); + error.status = 400; + throw error + } +}; + +function Note(title, content, createdAt, updatedAt, id) { + throwIfEmpty(title, "title"); + + this.id = id; + this.title = title; + this.content = content; + this.createdAt = createdAt; + this.updatedAt = updatedAt; +} + +Note.prototype.setId = function (id) { + this.id = id; +}; + +Note.prototype.setTitle = function (title) { + throwIfEmpty(title, "title"); + this.title = title; +}; + +Note.prototype.setContent = function (content) { + this.content = content; +}; + +Note.prototype.setCreatedAt = function (createdAt) { + this.createdAt = createdAt; +}; + +Note.prototype.setUpdatedAt = function (updatedAt) { + this.updatedAt = updatedAt; +}; + +module.exports = Note; diff --git a/examples/clean-architecture/index.js b/examples/clean-architecture/index.js new file mode 100644 index 00000000000..a6d1db87560 --- /dev/null +++ b/examples/clean-architecture/index.js @@ -0,0 +1,35 @@ +'use strict' + +/** + * Module dependencies. + */ + +/** + * This example is a Clean Architecture implementation of a CRUD application. + * You have only the Note entity and 4 rest api endpoints: + * - GET /note + * - GET /note/:id + * - POST /note + * - PATCH /note/:id + * - DELETE /note/:id + */ + +var express = require('../..'); +var logger = require('morgan'); +var loadRoutes = require('./routes'); + +var app = module.exports = express(); + +// log +if (!module.parent) app.use(logger('dev')); + +// parse request bodies (req.body) +app.use(express.json()); + +loadRoutes(app) + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/examples/clean-architecture/repositories/note.repository.js b/examples/clean-architecture/repositories/note.repository.js new file mode 100644 index 00000000000..cb8ba92c5b6 --- /dev/null +++ b/examples/clean-architecture/repositories/note.repository.js @@ -0,0 +1,60 @@ +function NotesRepository() { + //Really simple in-memory database + this.notes = []; + this.autoIncrementID = 0; +} + +NotesRepository.prototype.getAll = function() { + return this.notes; +}; + +NotesRepository.prototype.getById = function(id) { + var result = this.notes.find(function(note) { + return note.id === id; + }); + if (typeof result === "undefined") { + var err = new Error("Note not found"); + err.status = 404; + throw err + } + return result; +}; + +NotesRepository.prototype.create = function(note) { + note.setId(this.autoIncrementID); + note.setCreatedAt(new Date()); + note.setUpdatedAt(new Date()); + this.autoIncrementID++; + + this.notes.push(note); + + return note; +}; + +NotesRepository.prototype.deleteById = function(id) { + var index = this.notes.findIndex(function(note) { + return note.id === id; + }); + if (index === -1) { + var err = new Error("Note not found"); + err.status = 404; + throw err + } + this.notes.splice(index, 1); +}; + +NotesRepository.prototype.updateById = function(id, title, content) { + var index = this.notes.findIndex(function(note) { + return note.id === id; + }); + if (index === -1) { + var err = new Error("Note not found"); + err.status = 404; + throw err + } + this.notes[index].setTitle(title); + this.notes[index].setContent(content); + this.notes[index].setUpdatedAt(new Date()); +}; + +module.exports = NotesRepository; diff --git a/examples/clean-architecture/routes/index.js b/examples/clean-architecture/routes/index.js new file mode 100644 index 00000000000..6cc044c285b --- /dev/null +++ b/examples/clean-architecture/routes/index.js @@ -0,0 +1,4 @@ +module.exports = function loadRoutes(app) { + //Add here your routes + app.use("/notes", require("./notes.route")); +}; diff --git a/examples/clean-architecture/routes/notes.route.js b/examples/clean-architecture/routes/notes.route.js new file mode 100644 index 00000000000..9582bf37271 --- /dev/null +++ b/examples/clean-architecture/routes/notes.route.js @@ -0,0 +1,24 @@ +var router = require('../../..').Router(); + +//Database connection (you can change it to use a different database) +var NotesRepository = require('../repositories/note.repository'); + +//Controller class to handle express requests +var NotesController = require('../controllers/note.controller'); + +//Business logic class +var NotesService = require('../use-cases/notes'); + +var db = new NotesRepository() +var businessLogic = new NotesService(db) +var controller = new NotesController(businessLogic) + + +router.get('/', controller.getAll.bind(controller)); +router.get('/:id', controller.getById.bind(controller)); +router.post('/', controller.create.bind(controller)); +router.put('/:id', controller.update.bind(controller)); +router.delete('/:id', controller.delete.bind(controller)); + + +module.exports = router; diff --git a/examples/clean-architecture/use-cases/notes/index.js b/examples/clean-architecture/use-cases/notes/index.js new file mode 100644 index 00000000000..ad7b6df9755 --- /dev/null +++ b/examples/clean-architecture/use-cases/notes/index.js @@ -0,0 +1,31 @@ +/** + * Since this is just a simple CRUD, I'm only calling the underlying repository methods. + * In a real world application, in the same use-case method, you will insert the entire business logic. + */ + +function NotesService(notesRepository) { + this.notesRepository = notesRepository; +} + +NotesService.prototype.getAll = function() { + return this.notesRepository.getAll(); +}; + +NotesService.prototype.getById = function(id) { + return this.notesRepository.getById(id); +}; + +NotesService.prototype.create = function(note) { + //Add here other business logic... + return this.notesRepository.create(note); +}; + +NotesService.prototype.updateById = function(id, title, content) { + return this.notesRepository.updateById(id, title, content); +}; + +NotesService.prototype.delete = function(id) { + return this.notesRepository.deleteById(id); +}; + +module.exports = NotesService; diff --git a/test/acceptance/clean-architecture.js b/test/acceptance/clean-architecture.js new file mode 100644 index 00000000000..c742aae7ecb --- /dev/null +++ b/test/acceptance/clean-architecture.js @@ -0,0 +1,136 @@ +var app = require("../../examples/clean-architecture"); +var request = require("supertest"); + +describe("clean-architecture-crud", function () { + describe("GET /", function () { + it("should return empty array", function (done) { + request(app).get("/notes").expect(200, [], done); + }); + + it("list after creation", function (done) { + request(app) + .post("/notes") + .set("Content-Type", "application/json") + .send('{"title": "Text"}') + .expect(201) + .then(function (res) { + const obj = { + title: "Text", + id: 0, + }; + request(app) + .get("/notes") + .expect(function (res) { + obj.createdAt = res.body[0].createdAt; + obj.updatedAt = res.body[0].updatedAt; + }) + .expect(200, [obj], done); + }); + }); + }); + + describe("GET /:id", function () { + it("get after creation", function (done) { + request(app) + .post("/notes") + .set("Content-Type", "application/json") + .send('{"title": "Text"}') + .expect(201, function (err, res) { + if (err) return done(err); + request(app) + .get("/notes/" + res.body.id) + .expect(200, function (err, res) { + if (err) return done(err); + if (res.body.title === "Text") { + done(); + } + }); + }); + }); + + it("return error if cannot find by id", function (done) { + request(app).get("/notes/100").expect(404, done); + }); + }); + + describe("POST /", function () { + it("validation error on title", function (done) { + request(app) + .post("/notes") + .set("Content-Type", "application/json") + .send('{"title": ""}') + .expect(400, done); + }); + + it("creation ok", function (done) { + request(app) + .post("/notes") + .set("Content-Type", "application/json") + .send('{"title": "Text"}') + .expect(201, done); + }); + }); + + describe("POST /", function () { + it("validation error on title", function (done) { + request(app) + .post("/notes") + .set("Content-Type", "application/json") + .send('{"title": ""}') + .expect(400, done); + }); + }); + + describe("DELETE /", function () { + it("delete after creation", function (done) { + request(app) + .post("/notes") + .set("Content-Type", "application/json") + .send('{"title": "Text"}') + .expect(201, function (err, res) { + if (err) return done(err); + request(app) + .delete("/notes/" + res.body.id) + .expect(200, done); + }); + }); + + it("delete unexisting node", function (done) { + request(app).delete("/notes/100").expect(404, done); + }); + }); + + describe("put /", function () { + it("update after creation", function (done) { + request(app) + .post("/notes") + .set("Content-Type", "application/json") + .send('{"title": "Text"}') + .expect(201, function (err, res) { + if (err) return done(err); + request(app) + .put("/notes/" + res.body.id) + .send({ title: "Test2" }) + .expect(200, '"Note updated"', done); + }); + }); + + it("error in update after creation", function (done) { + request(app) + .post("/notes") + .set("Content-Type", "application/json") + .send('{"title": "Text"}') + .expect(201, function (err, res) { + if (err) return done(err); + request(app) + .put("/notes/" + res.body.id) + .send({ title: "" }) + .expect(400, done); + }); + }); + + it("try to update post not found", function (done) { + request(app).put("/notes/100").send({ title: "Test1" }).expect(404, done); + }); + }); +});