diff --git a/server/controllers/orderController.js b/server/controllers/orderController.js index 9487050..48010bb 100644 --- a/server/controllers/orderController.js +++ b/server/controllers/orderController.js @@ -4,7 +4,7 @@ import orderFormatter from '../middleware/formatter'; class OrderController { static async getAllOrders(req, res) { try { - const dbQuery = 'SELECT orders.id, menu.food_name, users.name, orders.date, orders.status FROM orders JOIN menu ON orders.item = menu.id JOIN users ON orders.author = users.id'; + const dbQuery = 'SELECT orders.id, orders.items, users.name, orders.date, orders.status FROM orders JOIN users ON orders.author = users.id'; const allOrders = (await pool.query(dbQuery)).rows; const formattedOrders = orderFormatter(allOrders); @@ -22,24 +22,16 @@ class OrderController { const { id } = req.params; if (Number.isNaN(Number(id))) { - return res.status(400).json({ - status: 'error', - message: 'invalid order id format', - }); + return res.status(400).json({ status: 'error', message: 'invalid order id format' }); } try { - const allOrders = (await pool.query('SELECT orders.id, menu.food_name, users.name, orders.date, orders.status FROM orders JOIN menu ON orders.item = menu.id JOIN users ON orders.author = users.id')).rows; + const allOrders = (await pool.query('SELECT orders.id, orders.items, users.name, orders.date, orders.status FROM orders JOIN users ON orders.author = users.id')).rows; const formattedOrders = orderFormatter(allOrders); const targetOrder = formattedOrders.find(order => order.id === Number(id)); - if (!targetOrder) { - return res.status(404).json({ - status: 'error', - message: 'no such order exists', - }); - } + if (!targetOrder) return res.status(404).json({ status: 'error', message: 'no such order exists' }); return res.status(200).json({ status: 'success', @@ -52,38 +44,13 @@ class OrderController { } static async newOrder(req, res) { - const { foodId } = req.body; - if (!foodId) { - return res.status(400).json({ - status: 'error', - message: 'incomplete data', - }); - } - - if (Number.isNaN(Number(foodId))) { - return res.status(400).json({ - status: 'error', - message: 'invalid data provided', - }); - } - try { - const foodExists = (await pool.query('SELECT * FROM menu WHERE id=$1', [foodId])).rowCount; - - if (!foodExists) { - return res.status(400).json({ - status: 'error', - message: 'no such food exists', - }); - } - } catch (error) { - res.status(500).json(error); - } + const { foodItems } = req; - const dbInsertQuery = 'INSERT INTO orders(item, author) VALUES($1, $2)'; - const dbSelectQuery = 'SELECT orders.id, menu.food_name, users.name, orders.date, orders.status FROM orders JOIN menu ON orders.item = menu.id JOIN users ON orders.author = users.id'; + const dbInsertQuery = 'INSERT INTO orders(items, author) VALUES($1, $2)'; + const dbSelectQuery = 'SELECT orders.id, orders.items, users.name, orders.date, orders.status FROM orders JOIN users ON orders.author = users.id'; try { - await pool.query(dbInsertQuery, [foodId, req.userId]); + await pool.query(dbInsertQuery, [JSON.stringify(foodItems), req.userId]); const allOrders = (await pool.query(dbSelectQuery)); const newOrder = allOrders.rows[allOrders.rowCount - 1]; @@ -93,7 +60,7 @@ class OrderController { order: { id: newOrder.id, author: newOrder.name, - title: newOrder.food_name, + items: JSON.parse(newOrder.items), date: newOrder.date, status: newOrder.status, }, @@ -105,28 +72,14 @@ class OrderController { static async updateOrder(req, res) { const { id } = req.params; - - if (!Number(id)) { - return res.status(400).json({ - status: 'error', - message: 'invalid order id provided', - }); - } + if (!Number(id)) return res.status(400).json({ status: 'error', message: 'invalid order id provided' }); try { - const dbQuery = 'UPDATE orders SET status=$1 WHERE id=$2'; - await pool.query(dbQuery, [req.status, Number(id)]); - const orderExists = (await pool.query('SELECT * FROM orders WHERE id=$1', [Number(id)])).rowCount; + if (!orderExists) return res.status(404).json({ status: 'error', message: 'no order with that id exists' }); - if (!orderExists) { - return res.status(404).json({ - status: 'error', - message: 'no order with that id exists', - }); - } - - const updatedOrders = (await pool.query('SELECT orders.id, menu.food_name, users.name, orders.date, orders.status FROM orders JOIN menu ON orders.item = menu.id JOIN users ON orders.author = users.id')).rows; + await pool.query('UPDATE orders SET status=$1 WHERE id=$2', [req.status, Number(id)]); + const updatedOrders = (await pool.query('SELECT orders.id, orders.items, users.name, orders.date, orders.status FROM orders JOIN users ON orders.author = users.id')).rows; const formattedOrders = orderFormatter(updatedOrders); const targetOrder = formattedOrders.find(order => order.id === Number(id)); @@ -142,13 +95,7 @@ class OrderController { static async getAllUserOrders(req, res) { const { id } = req.params; - - if (Number.isNaN(Number(id))) { - return res.status(400).json({ - status: 'error', - message: 'invalid user id', - }); - } + if (Number.isNaN(Number(id))) return res.status(400).json({ status: 'error', message: 'invalid user id' }); if (Number(id) !== req.userId) { return res.status(403).json({ @@ -159,10 +106,12 @@ class OrderController { try { const userOrders = (await pool.query('SELECT * FROM orders WHERE author=$1', [id])).rows; + const formattedUserOrders = userOrders + .map(order => ({ items: JSON.parse(order.items), date: order.date, status: order.status })); return res.status(200).json({ status: 'success', message: 'orders fetched successfully', - orders: userOrders, + orders: formattedUserOrders, }); } catch (error) { return res.status(500).json({ error }); diff --git a/server/db/orders.js b/server/db/orders.js deleted file mode 100644 index 1c46d8d..0000000 --- a/server/db/orders.js +++ /dev/null @@ -1,32 +0,0 @@ -const orders = [ - { - id: 1, - author: 'Michelle Graham', - title: 'Shrimps', - date: '2018-05-20', - status: 'pending', - }, - { - id: 2, - author: 'Chrisette Clark', - title: 'Chicken & Chips', - date: '2018-05-21', - status: 'pending', - }, - { - id: 3, - author: 'Jamiu Rafa', - title: 'Shrimps', - date: '2018-06-01', - status: 'pending', - }, - { - id: 4, - author: 'Tomal Gardner', - title: 'Spiced Turkey', - date: '2018-06-04', - status: 'pending', - }, -]; - -export default orders; diff --git a/server/db/schema.sql b/server/db/schema.sql index 5799f70..fee926d 100644 --- a/server/db/schema.sql +++ b/server/db/schema.sql @@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS menu ( CREATE TABLE IF NOT EXISTS orders ( id serial PRIMARY KEY, - item INTEGER REFERENCES menu(id), + items TEXT NOT NULL, author INTEGER REFERENCES users(id), date DATE NOT NULL DEFAULT CURRENT_DATE, status VARCHAR(50) NOT NULL DEFAULT 'new' diff --git a/server/middleware/formatter.js b/server/middleware/formatter.js index 6707a55..a098179 100644 --- a/server/middleware/formatter.js +++ b/server/middleware/formatter.js @@ -3,7 +3,7 @@ const formatOrders = (orders) => { const formatted = { id: foundOrder.id, author: foundOrder.name, - title: foundOrder.food_name, + items: JSON.parse(foundOrder.items), date: foundOrder.date, status: foundOrder.status, }; diff --git a/server/middleware/sanitizer.js b/server/middleware/sanitizer.js index df57096..0156c5e 100644 --- a/server/middleware/sanitizer.js +++ b/server/middleware/sanitizer.js @@ -104,6 +104,34 @@ class Sanitize { req.price = price; return next(); } + + static async newOrder(req, res, next) { + const { foodIds } = req.body; + + if (!foodIds + || !Validator.isArray(foodIds) + || !foodIds.length + || !Validator.isArrayOfNumbers(foodIds) + ) { + return res.status(400).json({ + status: 'error', + message: 'foodIds should be an array of numbers', + }); + } + const { allFoodExists, allFoodItems } = await Validator.isArrayOfValidFoodIds(foodIds); + if (!allFoodExists) { + return res.status(404).json({ + status: 'error', + message: 'one of the requested food items does not exist', + }); + } + // Create Array of requested food names. + const foodItems = foodIds + .map(foodId => allFoodItems.find(foodItem => foodItem.id === Number(foodId)).food_name); + + req.foodItems = foodItems; + return next(); + } } export default Sanitize; diff --git a/server/routes/ordersRouter.js b/server/routes/ordersRouter.js index 5f6b612..afe2bb5 100644 --- a/server/routes/ordersRouter.js +++ b/server/routes/ordersRouter.js @@ -6,7 +6,7 @@ import Sanitize from '../middleware/sanitizer'; const router = new Router(); router.get('/users/:id/orders', AuthHandler.authorize, OrderController.getAllUserOrders); -router.post('/orders', AuthHandler.authorize, OrderController.newOrder); +router.post('/orders', AuthHandler.authorize, Sanitize.newOrder, OrderController.newOrder); router.get('/orders', AuthHandler.authorize, AuthHandler.authorizeAdmin, OrderController.getAllOrders); router.get( diff --git a/server/validators/validator.js b/server/validators/validator.js index 1b4442f..d02d479 100644 --- a/server/validators/validator.js +++ b/server/validators/validator.js @@ -1,3 +1,5 @@ +import pool from '../db/config'; + class Validator { static isEmail(email) { const re = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/ig; @@ -15,6 +17,27 @@ class Validator { static isValidName(name) { return name.trim().length >= 2; } + + static isArray(value) { + return Array.isArray(value); + } + + static isArrayOfNumbers(array) { + return array.every(element => !Number.isNaN(Number(element))); + } + + static async isArrayOfValidFoodIds(foodIds) { + try { + const allFoodItems = (await pool.query('SELECT * FROM menu')).rows; + const validFoodIds = allFoodItems.map(food => food.id); + return { + allFoodExists: foodIds.every(foodId => validFoodIds.includes(Number(foodId))), + allFoodItems, + }; + } catch (error) { + throw new Error(error); + } + } } /* Refs. diff --git a/tests/routes/menu.spec.js b/tests/routes/menu.spec.js index e4953b6..e57c8f7 100644 --- a/tests/routes/menu.spec.js +++ b/tests/routes/menu.spec.js @@ -73,6 +73,27 @@ describe('POST /menu', () => { done(); }); }); + + it('should add another new food item to the menu successfully', (done) => { + chai.request(app) + .post('/api/v1/menu') + .set('x-auth', generateValidToken(users.admin)) + .send({ + foodName: 'Turkey Wings', + foodImage: 'https://i.imgur.com/Bfn1CxC.jpg', + price: '1200', + }) + .end((err, res) => { + if (err) done(err); + + res.status.should.eql(201); + res.body.should.be.an('object').which.has.all.keys(['status', 'message', 'food']); + res.body.food.should.have.all.keys(['id', 'food_name', 'food_image', 'price']); + res.body.food.food_name.should.eql('Turkey Wings'); + res.body.food.price.should.eql(1200); + done(); + }); + }); }); describe('GET /menu', () => { @@ -88,6 +109,7 @@ describe('GET /menu', () => { res.status.should.eql(200); res.body.message.should.eql('menu fetched successfully'); res.body.menu.should.be.an('array'); + res.body.menu.length.should.eql(2); done(); }); }); @@ -102,6 +124,7 @@ describe('GET /menu', () => { res.status.should.eql(200); res.body.message.should.eql('menu fetched successfully'); res.body.menu.should.be.an('array'); + res.body.menu.length.should.eql(2); done(); }); }); diff --git a/tests/routes/orders.spec.js b/tests/routes/orders.spec.js index 12a6842..e79010a 100644 --- a/tests/routes/orders.spec.js +++ b/tests/routes/orders.spec.js @@ -15,45 +15,49 @@ describe('POST /orders', () => { chai.request(app) .post('/api/v1/orders') .set('x-auth', generateValidToken(validUser)) - .send({ foodId: 1 }) + .send({ foodIds: [1, 2] }) .end((err, res) => { if (err) done(err); res.status.should.eql(201); res.body.status.should.eql('success'); res.body.order.should.be.an('object'); - res.body.order.should.have.keys(['id', 'author', 'title', 'date', 'status']); + res.body.order.should.have.keys(['id', 'author', 'items', 'date', 'status']); + res.body.order.items.should.be.an('array'); + res.body.order.items.length.should.eql(2); res.body.order.status.should.eql('new'); done(); }); }); - it('should successfuly place order for food if food id is string', (done) => { + it('should successfuly place order for food if any food id is string', (done) => { chai.request(app) .post('/api/v1/orders') .set('x-auth', generateValidToken(validUser)) - .send({ foodId: '1' }) + .send({ foodIds: ['1', 2] }) .end((err, res) => { if (err) done(err); res.status.should.eql(201); res.body.status.should.eql('success'); res.body.order.should.be.an('object'); - res.body.order.should.have.keys(['id', 'author', 'title', 'date', 'status']); + res.body.order.should.have.keys(['id', 'author', 'items', 'date', 'status']); + res.body.order.items.should.be.an('array'); + res.body.order.items.length.should.eql(2); res.body.order.status.should.eql('new'); done(); }); }); - it('should not place order if provided food id doesn\'t exist', (done) => { + it('should not place order if any provided food id doesn\'t exist', (done) => { chai.request(app) .post('/api/v1/orders') .set('x-auth', generateValidToken(validUser)) - .send({ foodId: 2 }) + .send({ foodIds: [1, 2, 6] }) .end(async (err, res) => { if (err) done(err); - res.status.should.eql(400); + res.status.should.eql(404); res.body.should.not.have.keys(['order']); res.body.status.should.eql('error'); done(); @@ -64,7 +68,7 @@ describe('POST /orders', () => { chai.request(app) .post('/api/v1/orders') .set('x-auth', generateValidToken(validUser)) - .send({ foodId: 'something weird' }) + .send({ foodIds: [1, 2, 'lol'] }) .end((err, res) => { if (err) done(err); @@ -163,6 +167,8 @@ describe('GET /orders', () => { res.body.should.have.keys(['status', 'message', 'orders']); res.body.orders.should.be.an('array'); res.body.orders[0].should.be.an('object'); + res.body.orders[0].should.be.have.keys(['id', 'author', 'items', 'date', 'status']); + res.body.orders[0].items.should.be.an('array'); done(); }); }); @@ -228,7 +234,8 @@ describe('GET /orders/:id', () => { res.status.should.eql(200); res.body.should.have.keys(['status', 'message', 'order']); res.body.order.should.be.an('object'); - res.body.order.should.have.all.keys(['id', 'author', 'date', 'status', 'title']); + res.body.order.should.have.all.keys(['id', 'items', 'author', 'date', 'status']); + res.body.order.items.should.be.an('array'); done(); }); }); diff --git a/tests/seed/seed.js b/tests/seed/seed.js index b26d7f0..10487bc 100644 --- a/tests/seed/seed.js +++ b/tests/seed/seed.js @@ -89,7 +89,7 @@ const emptyTables = async () => { const createOrdersTableQuery = `CREATE TABLE orders ( id serial PRIMARY KEY, - item INTEGER REFERENCES menu(id), + items TEXT NOT NULL, author INTEGER REFERENCES users(id), date DATE NOT NULL DEFAULT CURRENT_DATE, status VARCHAR(50) NOT NULL DEFAULT 'new'