diff --git a/README.md b/README.md index f50b9c82..961f37f8 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,63 @@ Your challenge is to develop an API, using Node.JS, for a product catalog manage - Code organization, module separation, readability and comments. - Commit history. - The use of MongoDB is a differentiator + +

Code Documentation

+Hello, dear. Below is the usage documentation. + +After downloading the project,it is necessary to install all project dependencies with npm + +```console +anotaai@pc:~$ npm install +``` +Now it is possible to run up the server with the command + +```console +anotaai@pc:~$ npm run dev +``` + +

About API

+Here is some technical information about modeling the problem + +

Schemas

+two schemes were models: + +- Products: mproducts has categories reference +```javascript + id: {type: String, required: false}, + category: {type: mongoose.Schema.Types.ObjectId, ref: 'category', required: true}, + title: {type: String, required: true}, + description: {type: String, required: true}, + price: {type: Number, required: true} +``` +- Categories: +```javascript +const categorySchema = new mongoose.Schema({ + id: {type: String, required: false}, + title: {type: String, require: true} +}) +``` + +

Routes

+ +Product Routes: + +Method | EndPoint | Body Params |Returns +:---------: | :------ | :-------: | :--------: +POST| /products | product | messag : Object +PUT | /products/:id | products | message : Object +GET | /products | - |products: Array +GET | /product/:id | - |product: Object +GET | /products/category/:id | - | products: Array +GET| /product/search?title=queryParam | - | message: Object +DELETE | /products/:id | - | message: Object + +Categorie Routes: + +Method | EndPoint | Body Params |Returns +:---------: | :------ | :-------: | :--------: +POST| /categories | category | message: Object +PUT | /categories/:id | category | message: Object +GET | /categories | - |categories: Array +GET | /categories/:id | - |category: Object +DELETE | /categories/:id | - | message: Object diff --git a/package.json b/package.json new file mode 100644 index 00000000..f8d00268 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "test-backend-nodejs", + "version": "1.0.0", + "description": "

Backend Analyst Candidate Testing

", + "main": "index.js", + "type": "module", + "scripts": { + "test": "mocha scripts/test/product-service.spec.js", + "dev": "nodemon server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Lebackrobot/test-backend-nodejs.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/Lebackrobot/test-backend-nodejs/issues" + }, + "homepage": "https://github.com/Lebackrobot/test-backend-nodejs#readme", + "dependencies": { + "chai": "^4.3.7", + "chai-http": "^4.3.0", + "chai-json-schema": "^1.5.1", + "express": "^4.18.2", + "mocha": "^10.2.0", + "mongoose": "^6.8.3", + "swagger-ui-express": "^4.6.0" + }, + "devDependencies": { + "mocha": "^10.2.0", + "@types/swagger-ui-express": "^4.1.3", + "nodemon": "^2.0.20" + } +} diff --git a/scripts/app.js b/scripts/app.js new file mode 100644 index 00000000..cbee26ba --- /dev/null +++ b/scripts/app.js @@ -0,0 +1,14 @@ +import express from 'express' +import db from './config/db-connect.js' + +import { routes } from './routes/index.js' + +db.on('error', err => {console.log(`Connection error ${err}`)}) +db.once('open', () => console.log('Success to connection on db')) + +const app = express() +app.use(express.json()) + +routes(app) + +export default app \ No newline at end of file diff --git a/scripts/config/db-connect.js b/scripts/config/db-connect.js new file mode 100644 index 00000000..46b07113 --- /dev/null +++ b/scripts/config/db-connect.js @@ -0,0 +1,5 @@ +import mongoose from 'mongoose' + +mongoose.connect('mongodb+srv://admin:admin123@cluster.ovjsowh.mongodb.net/test-backend-nodejs') + +export default mongoose.connection \ No newline at end of file diff --git a/scripts/controllers/category-controller.js b/scripts/controllers/category-controller.js new file mode 100644 index 00000000..088da85e --- /dev/null +++ b/scripts/controllers/category-controller.js @@ -0,0 +1,66 @@ +import CategoryService from './../services/category-services.js' + +class CategoryController { + + static createCategory = (require, response) => { + + CategoryService.createCategory(require) + .then(success => { + response.status(201).send({message:'Successfully create category'}) + }) + + .catch(err => { + response.status(500).send({ message: err.message}) + }) + } + + static getCategories = (require, response) => { + + CategoryService.getCategories() + .then(categories => { + response.status(200).send(categories) + }) + + .catch(err => { + response.status(400).send({ message: err.message }) + }) + } + + static getCategory = (require, response) => { + + CategoryService.getCategory(require) + .then(category => { + response.status(200).send(category) + }) + + .catch(err => { + response.status(400).send({ message: err.message }) + }) + + } + + static updateCategory = (require, response) => { + + CategoryService.updateCategory(require) + .then(success => { + response.status(200).send({ message: "Successfully update category" }) + }) + + .catch(err => { + response.status(500).send({message: err.message}) + }) + } + + static deleteCategory = (require, response) => { + CategoryService.deleteCategory(require) + .then(success => { + response.status(200).send({message: "Successfully delete category"}) + }) + + .catch(err => { + response.status(500).send({message: err.message}) + }) + } +} + +export default CategoryController \ No newline at end of file diff --git a/scripts/controllers/product-controller.js b/scripts/controllers/product-controller.js new file mode 100644 index 00000000..7e9ff3af --- /dev/null +++ b/scripts/controllers/product-controller.js @@ -0,0 +1,85 @@ +import ProductService from './../services/product-service.js' + +class ProductController { + + static createProduct = (require, response) => { + ProductService.createProduct(require) + .then(() => { + response.status(201).send({message: 'Successfully create product'}) + }) + + .catch(err => { + response.status(500).send({message: err.message}) + }) + } + + static getProducts = (require, response) => { + ProductService.getProducts(require) + .then(products => { + response.status(200).send(products) + }) + + .catch(err => { + response.status(400).send({message: err.message}) + }) + } + + static searchProductsByCategory = (require, response) => { + ProductService.searchProductsByCategory(require) + .then(products => { + response.status(200).send(products) + }) + + .catch(err => { + response.status(400).send({message: err.message}) + }) + } + + static searchProductByTitle = (require, response) => { + ProductService.searchProductByTitle(require) + .then(product => { + response.status(200).send(product) + }) + + .catch(err => { + response.status(400).send({message: err.message}) + }) + } + + static getProduct = (require, response) => { + + ProductService.getProduct(require) + .then(product => { + response.status(200).send(product) + }) + + .catch(err => { + response.status(400).send({message: err.message}) + }) + } + + static updateProduct = (require, response) => { + + ProductService.updateProduct(require) + .then( success => { + response.status(200).send({message: 'Successfully update product'}) + }) + + .catch(err => { + response.status(500).send({message: err.message}) + }) + } + + static deleteProduct = (require, response) => { + ProductService.deleteProduct(require) + .then(() => { + response.status(200).send({message: 'Successfully delete product'}) + }) + + .catch(err => { + response.status(500).send({message: err.message}) + }) + } +} + +export default ProductController \ No newline at end of file diff --git a/scripts/models/category-model.js b/scripts/models/category-model.js new file mode 100644 index 00000000..5288e816 --- /dev/null +++ b/scripts/models/category-model.js @@ -0,0 +1,8 @@ +import mongoose from 'mongoose' + +const categorySchema = new mongoose.Schema({ + id: {type: String, required: false}, + title: {type: String, required: true} +}) + +export default mongoose.model('category', categorySchema) \ No newline at end of file diff --git a/scripts/models/product-model.js b/scripts/models/product-model.js new file mode 100644 index 00000000..3e6107ff --- /dev/null +++ b/scripts/models/product-model.js @@ -0,0 +1,11 @@ +import mongoose from 'mongoose' + +const productSchema = new mongoose.Schema({ + id: {type: String, required: false}, + category: {type: mongoose.Schema.Types.ObjectId, ref: 'category', required: true}, + title: {type: String, required: true}, + description: {type: String, required: true}, + price: {type: Number, required: true} +}) + +export default mongoose.model('products', productSchema) \ No newline at end of file diff --git a/scripts/routes/category-router.js b/scripts/routes/category-router.js new file mode 100644 index 00000000..3db1bfaf --- /dev/null +++ b/scripts/routes/category-router.js @@ -0,0 +1,13 @@ +import express from 'express' +import CategoryController from '../controllers/category-controller.js' + +const CategoryRouter = express.Router() + +CategoryRouter + .post('/categories', CategoryController.createCategory) + .get('/categories', CategoryController.getCategories) + .get('/categories/:id', CategoryController.getCategory) + .put('/categories/:id', CategoryController.updateCategory) + .delete('/categories/:id', CategoryController.deleteCategory) + +export default CategoryRouter \ No newline at end of file diff --git a/scripts/routes/index.js b/scripts/routes/index.js new file mode 100644 index 00000000..adeac773 --- /dev/null +++ b/scripts/routes/index.js @@ -0,0 +1,19 @@ +import express from 'express' +import productRouter from './product-router.js' +import categoryRouter from './category-router.js' + +import swaggerUI from 'swagger-ui-express' +import swaggerJson from './../swagger.json' assert { type: "json" } + +const routes = app => { + app.route('/').get((req, res) => { + res.status(200).send({message: 'Lets bora!'}) + }) + + app + .use(express.json()) + .use('/docs', swaggerUI.serve, swaggerUI.setup(swaggerJson)) + .use(productRouter, categoryRouter) +} + +export { routes } \ No newline at end of file diff --git a/scripts/routes/product-router.js b/scripts/routes/product-router.js new file mode 100644 index 00000000..f9f09b16 --- /dev/null +++ b/scripts/routes/product-router.js @@ -0,0 +1,16 @@ +import express from 'express' +import ProductController from '../controllers/product-controller.js' + +const productRouter = express.Router() + +productRouter + .get('/products/search', ProductController.searchProductByTitle) + .get('/products/category/:id', ProductController.searchProductsByCategory) + .post('/products', ProductController.createProduct) + .get('/products', ProductController.getProducts) + .get('/products/:id', ProductController.getProduct) + .put('/products/:id', ProductController.updateProduct) + .delete('/products/:id', ProductController.deleteProduct) + + +export default productRouter \ No newline at end of file diff --git a/scripts/services/category-services.js b/scripts/services/category-services.js new file mode 100644 index 00000000..d00451b7 --- /dev/null +++ b/scripts/services/category-services.js @@ -0,0 +1,25 @@ +import Category from './../models/category-model.js' + +class CategoryService { + static createCategory = require => { + return new Category(require.body).save() + } + + static getCategories = () => { + return Category.find().lean() + } + + static getCategory = require => { + return Category.findById(require.params.id).lean() + } + + static updateCategory = require => { + return Category.findByIdAndUpdate(require.params.id, {$set: require.body}) + } + + static deleteCategory = require => { + return Category.findByIdAndDelete(require.params.id) + } +} + +export default CategoryService \ No newline at end of file diff --git a/scripts/services/product-service.js b/scripts/services/product-service.js new file mode 100644 index 00000000..55d53dc0 --- /dev/null +++ b/scripts/services/product-service.js @@ -0,0 +1,34 @@ +import Product from './../models/product-model.js' + +class ProductService { + static createProduct = require => { + return new Product(require.body).save() + } + + static getProducts = require => { + return Product.find().populate('category') + } + + static getProduct = require => { + return Product.findById(require.params.id).lean() + } + + static searchProductsByCategory = require => { + return Product.find({category: require.params.id}) + } + + static searchProductByTitle = require => { + return Product.find({title: require.query.title}).lean() + } + + + static updateProduct = require => { + return Product.findByIdAndUpdate(require.params.id, {$set: require.body}) + } + + static deleteProduct = require => { + return Product.findByIdAndDelete(require.params.id) + } +} + +export default ProductService \ No newline at end of file diff --git a/scripts/swagger.json b/scripts/swagger.json new file mode 100644 index 00000000..998aa7c0 --- /dev/null +++ b/scripts/swagger.json @@ -0,0 +1,322 @@ +{ + "openapi": "3.0.0", + "paths": { + "/products": { + "post": { + "tags": ["products"], + "summary": "Create Product", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "description": {"type": "string"}, + "price": {"type": "number"}, + "category": {"type": "_id"} + }, + "example": { + "title": "Doritos", + "description": "Salgadinho caro", + "price": 15.99, + "category": 567 + + } + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "types": "string" + } + }, + "example": { + "message": "Successfully create product" + } + } + } + } + } + } + }, + "get": { + "tags": ["products"], + "summary": "List all products", + "responses": { + "200": { + "description": "List all products", + "content": { + "application/json": { + "schema": { + "type": "Array", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "price": { + "type": "number" + }, + "category": { + "type": "_id" + } + } + }, + "example": { + "categories": [ + {"_id": "123", "title": "Doritos", "description": "Salgadinho caro", "price": 15.99, "category": "567" }, + {"_id": "123", "title": "Coxinha", "description": "frango e queijo", "price": 5.00, "category": "789" } + ] + } + + } + } + } + } + }, + "put": { + "tags": [ + "products" + ], + "summary": "Update Product", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "example": { + "title": "Doces" + } + } + } + } + }, + "responses": { + "200": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "types": "string" + } + }, + "example": { + "message": "Successfully update product" + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "products" + ], + "summary": "Update Product", + "responses": { + "200": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "types": "string" + } + }, + "example": { + "message": "Successfully delete product" + } + } + } + } + } + } + } + }, + + "/categories": { + "post": { + "tags": [ + "categories" + ], + "summary": "Create Category", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "example": { + "title": "Doces" + } + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "types": "string" + } + }, + "example": { + "message": "Successfully create product" + } + } + } + } + } + } + }, + "get": { + "tags": [ + "categories" + ], + "summary": "List all categories", + "responses": { + "200": { + "description": "List all categories", + "content": { + "application/json": { + "schema": { + "type": "Array", + "properties": { + "title": { + "type": "string" + }, + "_id": { + "type": "_id" + } + } + }, + "example": { + "categories": [ + { + "_id": 123, + "title": "Doces" + }, + { + "_id": 456, + "title": "Salgados" + }, + { + "_id": 567, + "title": "Sashimi" + } + ] + } + } + } + } + } + }, + "put": { + "tags": [ + "categories" + ], + "summary": "Update Category", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "example": { + "title": "Doces" + } + } + } + } + }, + "responses": { + "200": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "types": "string" + } + }, + "example": { + "message": "Successfully update product" + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "categories" + ], + "summary": "Update Category", + "responses": { + "200": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "types": "string" + } + }, + "example": { + "message": "Successfully delete product" + } + } + } + } + } + } + } + } + } +} + + diff --git a/scripts/test/category-service.spec.js b/scripts/test/category-service.spec.js new file mode 100644 index 00000000..0f3e2983 --- /dev/null +++ b/scripts/test/category-service.spec.js @@ -0,0 +1,66 @@ +import chai from 'chai' +import chaiHttp from 'chai-http' +import chaiJsonSchema from 'chai-json-schema' +import Category from '../models/category-model.js' + +chai.use(chaiHttp) +chai.use(chaiJsonSchema) + +const urlBase = 'http://localhost:3000' + +describe('Category Router', () => { + it('POST /categories', done => { + const category = {title: 'Doces'} + + chai.request(urlBase).post('/categories').send(category).end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(201) + chai.expect(response.body).to.include({ message: 'Successfully create category'}) + }) + + done() + }) + + it('GET /categories', done => { + chai.request(urlBase).get('/categories').end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(200) + chai.expect(response.body).to.be.jsonSchema([Category]) + }) + + done() + }) + + it('GET /categories/:id', done => { + const category = '63c179fef0992a4b3378dd8c' + + chai.request(urlBase).get(`/categories/${category}`).end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(200) + chai.expect(response.body).to.be.jsonSchema(Category) + }) + done() + }) + + it('UPDATE /categories/:id', done => { + const category = '63c179fef0992a4b3378dd8c' + + chai.request(urlBase).put(`/categories/${category}`).send({title: "Salgadinhos"}).end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(200) + chai.expect(response.body).to.include({message: 'Successfully update category'}) + }) + done() + }) + + it('REMOVE /categories:id', done => { + const category = '63caac82a5ea9dfa1387524a' + + chai.request(urlBase).delete(`/categories/${category}`).end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(200) + chai.expect(response.body).to.include({message: 'Successfully delete category'}) + }) + done() + }) +}) diff --git a/scripts/test/product-service.spec.js b/scripts/test/product-service.spec.js new file mode 100644 index 00000000..2cf622cc --- /dev/null +++ b/scripts/test/product-service.spec.js @@ -0,0 +1,97 @@ +import chai from 'chai' +import chaiHttp from 'chai-http' +import chaiJsonSchema from 'chai-json-schema' +import Product from '../models/product-model.js' + +chai.use(chaiHttp) +chai.use(chaiJsonSchema) + +const urlBase = 'http://localhost:3000' + +describe('Product Router', () => { + it('POST /products', done => { + const product = { category: '63c179fef0992a4b3378dd8c', title: 'Fofura', description: 'Sabor pimenta', price: 4.00} + + chai.request(urlBase).post('/products').send(product).end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(201) + chai.expect(response.body).to.include({ message: 'Successfully create product'}) + }) + + done() + }) + + it('GET /products', done => { + chai.request(urlBase).get('/products').end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(200) + chai.expect(response.body).to.be.jsonSchema([Product]) + }) + + done() + }) + + it('GET /products/:id', done => { + const id = '63cfddd5c9ebca69f3fe1242' + + chai.request(urlBase).get(`/products/${id}`).end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(200) + chai.expect(response).to.be.jsonSchema(Product) + }) + + done() + }) + + it('GET /products/search', done => { + chai.request(urlBase).get('/products/search').query({title: 'Coxinha'}).end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(200) + chai.expect(response).to.be.jsonSchema(Product) + }) + + done() + }) + + if('GET /product/category/:id', done => { + const id = '63c179fef0992a4b3378dd8c' + + chai.request(urlBase).get(`products/category/${id}`).end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(200) + chai.expect(response).to.be.jsonSchema(Product) + }) + + done() + }) + + + it('UPDATE /products/:id', done => { + + const id = '63cfddd5c9ebca69f3fe1242' + const product = { category: '63c179fef0992a4b3378dd8c', title: 'Doritos', description: 'sabor primenta', price: 4.00} + + chai.request(urlBase).put(`/products/${id}`).send(product).end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(200) + chai.expect(response).to.include({ message: 'Successfully update product' }) + + }) + + done() + }) + + it('DELETE /products/:id', done => { + const id = '63cfd8955ca541cc45e67a64' + + chai.request(urlBase).delete(`/products/${id}`).end((err, response) => { + chai.expect(err).to.be.null + chai.expect(response).to.have.status(200) + chai.expect(response).to.include({ message: 'Successfully delete product' }) + }) + + done() + }) + + +}) diff --git a/server.js b/server.js new file mode 100644 index 00000000..143f8d3c --- /dev/null +++ b/server.js @@ -0,0 +1,10 @@ +import app from './scripts/app.js' + +import express from 'express' + +const port = process.env.PORT || 3000 + + +app.listen(port, () => { + console.log(`Server listen on port ${port}`) +}) \ No newline at end of file