diff --git a/2-api/cyf-ecommerce-api/.env.sample b/2-api/cyf-ecommerce-api/.env.sample new file mode 100644 index 00000000..6879867d --- /dev/null +++ b/2-api/cyf-ecommerce-api/.env.sample @@ -0,0 +1,5 @@ +DB_HOST= +DB_PORT= +DB_NAME= +DB_USER= +DB_PASS= \ No newline at end of file diff --git a/2-api/cyf-ecommerce-api/.gitignore b/2-api/cyf-ecommerce-api/.gitignore new file mode 100644 index 00000000..a57bde28 --- /dev/null +++ b/2-api/cyf-ecommerce-api/.gitignore @@ -0,0 +1,3 @@ +*.env +node_modules +package-lock.json \ No newline at end of file diff --git a/2-api/cyf-ecommerce-api/cyf_ecommerce.sql b/2-api/cyf-ecommerce-api/cyf_ecommerce.sql new file mode 100644 index 00000000..7b335282 --- /dev/null +++ b/2-api/cyf-ecommerce-api/cyf_ecommerce.sql @@ -0,0 +1,124 @@ +drop table if exists order_items; +drop table if exists orders cascade; +DROP TABLE IF EXISTS product_availability cascade; +drop table if exists customers cascade; +drop table if exists products cascade; +drop table if exists suppliers cascade; + +CREATE TABLE customers ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL, + address VARCHAR(120), + city VARCHAR(30), + country VARCHAR(20) +); + +CREATE TABLE suppliers ( + id SERIAL PRIMARY KEY, + supplier_name VARCHAR(100) NOT NULL, + country VARCHAR(20) NOT NULL +); + +CREATE TABLE products ( + id SERIAL PRIMARY KEY, + product_name VARCHAR(100) NOT NULL +); + +create table product_availability ( + prod_id integer references products(id), + supp_id integer references suppliers(id), + unit_price integer not null, + primary key (prod_id, supp_id) +); + +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + order_date DATE NOT NULL, + order_reference VARCHAR(10) NOT NULL, + customer_id INT REFERENCES customers(id) +); + +CREATE TABLE order_items ( + id SERIAL PRIMARY KEY, + order_id INT NOT NULL REFERENCES orders(id), + product_id INT NOT NULL, + supplier_id INT NOT NULL, + quantity INT NOT NULL, + FOREIGN KEY (product_id, supplier_id) + REFERENCES product_availability (prod_id, supp_id) +); + +INSERT INTO customers (name, address, city, country) VALUES ('Guy Crawford','770-2839 Ligula Road','Paris','France'); +INSERT INTO customers (name, address, city, country) VALUES ('Hope Crosby','P.O. Box 276, 4976 Sit Rd.','Steyr','United Kingdom'); +INSERT INTO customers (name, address, city, country) VALUES ('Britanney Kirkland','P.O. Box 577, 5601 Sem, St.','Little Rock','United Kingdom'); +INSERT INTO customers (name, address, city, country) VALUES ('Amber Tran','6967 Ac Road','Villafranca Asti','United States'); +INSERT INTO customers (name, address, city, country) VALUES ('Edan Higgins','Ap #840-3255 Tincidunt St.','Arles','United States'); +INSERT INTO customers (name, address, city, country) VALUES ('Quintessa Austin','597-2737 Nunc Rd.','Saint-Marc','United Kingdom'); + +INSERT INTO suppliers (supplier_name, country) VALUES ('Amazon', 'United States'); +INSERT INTO suppliers (supplier_name, country) VALUES ('Taobao', 'China'); +INSERT INTO suppliers (supplier_name, country) VALUES ('Argos', 'United Kingdom'); +INSERT INTO suppliers (supplier_name, country) VALUES ('Sainsburys', 'United Kingdom'); + + +INSERT INTO products (product_name) VALUES ('Mobile Phone X'); +INSERT INTO products (product_name) VALUES ('Javascript Book'); +INSERT INTO products (product_name) VALUES ('Le Petit Prince'); +INSERT INTO products (product_name) VALUES ('Super warm socks'); +INSERT INTO products (product_name) VALUES ('Coffee Cup'); +INSERT INTO products (product_name) VALUES ('Ball'); +INSERT INTO products (product_name) VALUES ('Tee Shirt Olympic Games'); + +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (1, 4, 249); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (1, 1, 299); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (2, 2, 41); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (2, 3, 39); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (2, 1, 40); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (3, 4, 10); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (3, 1, 10); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (4, 4, 10); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (4, 3, 8); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (4, 2, 5); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (4, 1, 10); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (5, 4, 5); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (5, 3, 4); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (5, 2, 4); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (5, 1, 3); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (6, 2, 20); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (6, 4, 15); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (6, 1, 14); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (7, 3, 21); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (7, 2, 18); +INSERT INTO product_availability (prod_id, supp_id, unit_price) VALUES (7, 1, 20); + +INSERT INTO orders (order_date, order_reference, customer_id) VALUES ('2019-06-01', 'ORD001', 1); +INSERT INTO orders (order_date, order_reference, customer_id) VALUES ('2019-07-15', 'ORD002', 1); +INSERT INTO orders (order_date, order_reference, customer_id) VALUES ('2019-07-11', 'ORD003', 1); +INSERT INTO orders (order_date, order_reference, customer_id) VALUES ('2019-05-24', 'ORD004', 2); +INSERT INTO orders (order_date, order_reference, customer_id) VALUES ('2019-05-30', 'ORD005', 3); +INSERT INTO orders (order_date, order_reference, customer_id) VALUES ('2019-07-05', 'ORD006', 4); +INSERT INTO orders (order_date, order_reference, customer_id) VALUES ('2019-04-05', 'ORD007', 4); +INSERT INTO orders (order_date, order_reference, customer_id) VALUES ('2019-07-23', 'ORD008', 5); +INSERT INTO orders (order_date, order_reference, customer_id) VALUES ('2019-07-24', 'ORD009', 5); +INSERT INTO orders (order_date, order_reference, customer_id) VALUES ('2019-05-10', 'ORD010', 5); + +INSERT INTO order_items VALUES (1, 1, 7, 2, 1); +INSERT INTO order_items VALUES (2, 1, 4, 2, 5); +INSERT INTO order_items VALUES (3, 2, 4, 3, 4); +INSERT INTO order_items VALUES (4, 2, 3, 4, 1); +INSERT INTO order_items VALUES (5, 3, 5, 3, 10); +INSERT INTO order_items VALUES (6, 3, 6, 2, 2); +INSERT INTO order_items VALUES (7, 4, 1, 1, 1); +INSERT INTO order_items VALUES (8, 5, 2, 3, 2); +INSERT INTO order_items VALUES (9, 5, 3, 1, 1); +INSERT INTO order_items VALUES (10, 6, 5, 2, 3); +INSERT INTO order_items VALUES (11, 6, 2, 2, 1); +INSERT INTO order_items VALUES (12, 6, 3, 4, 1); +INSERT INTO order_items VALUES (13, 6, 4, 4, 3); +INSERT INTO order_items VALUES (14, 7, 4, 3, 15); +INSERT INTO order_items VALUES (15, 8, 7, 1, 1); +INSERT INTO order_items VALUES (16, 8, 1, 4, 1); +INSERT INTO order_items VALUES (17, 9, 6, 4, 2); +INSERT INTO order_items VALUES (18, 10, 6, 2, 1); +INSERT INTO order_items VALUES (19, 10, 4, 1, 5); + diff --git a/2-api/cyf-ecommerce-api/package.json b/2-api/cyf-ecommerce-api/package.json new file mode 100644 index 00000000..1940013a --- /dev/null +++ b/2-api/cyf-ecommerce-api/package.json @@ -0,0 +1,21 @@ +{ + "name": "cyf-ecommerce-api", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "dotenv": "^16.0.3", + "express": "^4.18.2", + "pg": "^8.10.0" + }, + "devDependencies": { + "nodemon": "^2.0.22" + } +} diff --git a/2-api/cyf-ecommerce-api/project-screenshot.png b/2-api/cyf-ecommerce-api/project-screenshot.png new file mode 100644 index 00000000..bb0afd19 Binary files /dev/null and b/2-api/cyf-ecommerce-api/project-screenshot.png differ diff --git a/2-api/cyf-ecommerce-api/public/bazmurphy.jpeg b/2-api/cyf-ecommerce-api/public/bazmurphy.jpeg new file mode 100644 index 00000000..65c53c46 Binary files /dev/null and b/2-api/cyf-ecommerce-api/public/bazmurphy.jpeg differ diff --git a/2-api/cyf-ecommerce-api/public/elephant.png b/2-api/cyf-ecommerce-api/public/elephant.png new file mode 100644 index 00000000..94612cfe Binary files /dev/null and b/2-api/cyf-ecommerce-api/public/elephant.png differ diff --git a/2-api/cyf-ecommerce-api/public/favicon.ico b/2-api/cyf-ecommerce-api/public/favicon.ico new file mode 100644 index 00000000..cc037662 Binary files /dev/null and b/2-api/cyf-ecommerce-api/public/favicon.ico differ diff --git a/2-api/cyf-ecommerce-api/public/index.html b/2-api/cyf-ecommerce-api/public/index.html new file mode 100644 index 00000000..7a2ae619 --- /dev/null +++ b/2-api/cyf-ecommerce-api/public/index.html @@ -0,0 +1,289 @@ + + + + + + + + PostgreSQL API - Ecommerce v2.0 + + +
+
+ +

PostgreSQL API - Ecommerce v2.0

+
+ +
+
+
+ +
+ GET Examples +
+ +
+
+ GET /customers/:customerId + + + +
+
+ +
+
+ GET /products?name= + + + +
+
+ +
+
+ POST /customers + + + + + + + + + +
+
+ +
+
+ POST /products + + + +
+
+ +
+
+ POST /availability + + + + + + + +
+
+ +
+
+ POST /customer/:customerId/orders + + + + + + + +
+
+ +
+
+ PUT /customers/:customerId + + + + + + + + + + + +
+
+ +
+
+ DELETE /orders/:orderId + + + +
+
+ +
+
+ DELETE /customers/:customerId + + + +
+
+
+
+

API Response (JSON) :

+
+
+
+ + + diff --git a/2-api/cyf-ecommerce-api/public/script.js b/2-api/cyf-ecommerce-api/public/script.js new file mode 100644 index 00000000..1177d1b2 --- /dev/null +++ b/2-api/cyf-ecommerce-api/public/script.js @@ -0,0 +1,289 @@ +async function fetchFormatAndOutput(path, options = {}) { + try { + const response = await fetch(path, options); + const data = await response.json(); + console.log("fetch data:", data); + const responseContainer = document.getElementById("response-container"); + responseContainer.innerText = ""; + const pre = document.createElement("pre"); + const code = document.createElement("code"); + code.id = "output-json"; + code.innerText = JSON.stringify(data, null, 2); + pre.appendChild(code); + responseContainer.appendChild(pre); + } catch (error) { + console.log(error); + } +} + +const paths = ["/customers", "/suppliers", "/products"]; + +paths.forEach((path) => { + const navButtons = document.getElementById("get-buttons"); + const button = document.createElement("button"); + button.id = path; + button.innerText = path; + button.addEventListener("click", (event) => + fetchFormatAndOutput(event.target.id) + ); + navButtons.appendChild(button); +}); + +const getCustomerByIdForm = document.getElementById("form-get-customer-byid"); +getCustomerByIdForm.addEventListener("submit", async (event) => { + event.preventDefault(); + try { + const getCustomerByIdSubmit = document.getElementById( + "form-get-customer-byid-submit" + ); + getCustomerByIdSubmit.disabled = true; + + const customerId = document.getElementById( + "form-get-customer-byid-id" + ).value; + + await fetchFormatAndOutput(`/customers/${customerId}`); + + getCustomerByIdSubmit.disabled = false; + } catch (error) { + console.log(error); + } +}); + +const getProductByNameForm = document.getElementById("form-get-product-byname"); +getProductByNameForm.addEventListener("submit", async (event) => { + event.preventDefault(); + try { + const getProductByNameSubmit = document.getElementById( + "form-get-product-byname-submit" + ); + getProductByNameSubmit.disabled = true; + + const productName = document.getElementById( + "form-get-product-byname-productname" + ).value; + + await fetchFormatAndOutput(`/products?name=${productName}`); + + getProductByNameSubmit.disabled = false; + } catch (error) { + console.log(error); + } +}); + +const postCustomerForm = document.getElementById("form-post-customer"); +postCustomerForm.addEventListener("submit", async (event) => { + event.preventDefault(); + try { + const postCustomerSubmit = document.getElementById( + "form-post-customer-submit" + ); + postCustomerSubmit.disabled = true; + + const name = document.getElementById("form-post-customer-name").value; + const address = document.getElementById("form-post-customer-address").value; + const city = document.getElementById("form-post-customer-city").value; + const country = document.getElementById("form-post-customer-country").value; + + await fetchFormatAndOutput(`/customers`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: name, + address: address, + city: city, + country: country, + }), + }); + + postCustomerSubmit.disabled = false; + } catch (error) { + console.log(error); + } +}); + +const postProductForm = document.getElementById("form-post-product"); +postProductForm.addEventListener("submit", async (event) => { + event.preventDefault(); + try { + const postProductSubmit = document.getElementById( + "form-post-product-submit" + ); + postProductSubmit.disabled = true; + + const productName = document.getElementById( + "form-post-product-productname" + ).value; + + await fetchFormatAndOutput(`products`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + productname: productName, + }), + }); + + postProductSubmit.disabled = false; + } catch (error) { + console.log(error); + } +}); + +const postAvailabilityForm = document.getElementById("form-post-availability"); +postAvailabilityForm.addEventListener("submit", async (event) => { + event.preventDefault(); + try { + const postAvailabilitySubmit = document.getElementById( + "form-post-availability-submit" + ); + postAvailabilitySubmit.disabled = true; + + const productId = document.getElementById( + "form-post-availability-productid" + ).value; + const supplierId = document.getElementById( + "form-post-availability-supplierid" + ).value; + const unitPrice = document.getElementById( + "form-post-availability-unitprice" + ).value; + + await fetchFormatAndOutput(`/availability`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + productid: productId, + supplierid: supplierId, + unitprice: unitPrice, + }), + }); + + postAvailabilitySubmit.disabled = false; + } catch (error) { + console.log(error); + } +}); + +const postCustomerOrderForm = document.getElementById( + "form-post-customer-order" +); +postCustomerOrderForm.addEventListener("submit", async (event) => { + event.preventDefault(); + try { + const postCustomerOrderSubmit = document.getElementById( + "form-post-customer-order-submit" + ); + postCustomerOrderSubmit.disabled = true; + + const customerId = document.getElementById( + "form-post-customer-order-customerid" + ).value; + const orderDate = document.getElementById( + "form-post-customer-order-orderdate" + ).value; + const orderReference = document.getElementById( + "form-post-customer-order-orderreference" + ).value; + + await fetchFormatAndOutput(`/customers/${customerId}/orders`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + orderdate: orderDate, + orderreference: orderReference, + }), + }); + + postCustomerOrderSubmit.disabled = false; + } catch (error) { + console.log(error); + } +}); + +const putCustomerByIdForm = document.getElementById("form-put-customer"); +putCustomerByIdForm.addEventListener("submit", async (event) => { + event.preventDefault(); + try { + const putCustomerByIdSubmit = document.getElementById( + "form-put-customer-submit" + ); + putCustomerByIdSubmit.disabled = true; + + const customerId = document.getElementById( + "form-put-customer-customerid" + ).value; + const name = document.getElementById("form-put-customer-name").value; + const address = document.getElementById("form-put-customer-address").value; + const city = document.getElementById("form-put-customer-city").value; + const country = document.getElementById("form-put-customer-country").value; + + await fetchFormatAndOutput(`/customers/${customerId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: name, + address: address, + city: city, + country: country, + }), + }); + + putCustomerByIdSubmit.disabled = false; + } catch (error) { + console.log(error); + } +}); + +const deleteOrderByIdForm = document.getElementById("form-delete-order"); +deleteOrderByIdForm.addEventListener("submit", async (event) => { + event.preventDefault(); + try { + const deleteOrderByIdSubmit = document.getElementById( + "form-delete-order-submit" + ); + deleteOrderByIdSubmit.disabled = true; + + const orderId = document.getElementById("form-delete-order-orderid").value; + + await fetchFormatAndOutput(`/orders/${orderId}`, { + method: "DELETE", + }); + + deleteOrderByIdSubmit.disabled = false; + } catch (error) { + console.log(error); + } +}); + +const deleteCustomerByIdForm = document.getElementById("form-delete-customer"); +deleteCustomerByIdForm.addEventListener("submit", async (event) => { + event.preventDefault(); + try { + const deleteCustomerByIdSubmit = document.getElementById( + "form-delete-customer-submit" + ); + deleteCustomerByIdSubmit.disabled = true; + + const customerId = document.getElementById( + "form-delete-customer-customerid" + ).value; + + await fetchFormatAndOutput(`/customers/${customerId}`, { + method: "DELETE", + }); + + deleteCustomerByIdSubmit.disabled = false; + } catch (error) { + console.log(error); + } +}); diff --git a/2-api/cyf-ecommerce-api/public/style.css b/2-api/cyf-ecommerce-api/public/style.css new file mode 100644 index 00000000..aa3876fb --- /dev/null +++ b/2-api/cyf-ecommerce-api/public/style.css @@ -0,0 +1,124 @@ +@import url("https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;600&display=swap"); + +* { + box-sizing: border-box; + margin: 0; + font-family: "Instrument Sans", system-ui, sans-serif; + color: black; + font-size: 14px; + font-weight: 400; +} + +body { + margin: 0 auto; + max-width: 800px; + background-color: hsl(0, 0%, 92%); +} + +header { + display: flex; + justify-content: space-between; + border-bottom: 1px solid hsl(207, 47%, 39%); + margin-bottom: 10px; +} + +#title { + display: flex; + align-items: center; + gap: 10px; +} + +#title img { + max-height: 40px; +} + +#title h1 { + margin: 0; + font-size: 24px; + font-weight: 600; +} + +#bazmurphy { + max-height: 40px; + border-radius: 100%; +} + +main { + display: grid; + grid-template-columns: 0.4fr 0.6fr; + align-items: start; + column-gap: 10px; +} + +#input-container { + display: grid; + align-items: start; + row-gap: 5px; +} + +#output-container { + /* margin-top: 9px; */ +} + +#output-container > h2 { + font-weight: 600; +} + +#response-container { + padding: 5px; + overflow-x: scroll; + overflow-y: scroll; + background-color: hsl(0, 0%, 95%); + border: 1px solid darkgray; + min-height: 100px; + max-height: 800px; +} + +pre { + /* to wrap the json text over to the next line */ + white-space: pre-wrap; +} + +#output-json { + font-family: monospace; +} + +#get-buttons { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 10px; + padding: 5px; +} + +fieldset { + display: grid; + grid-template-columns: 40% 60%; + gap: 5px; + border: 1px solid hsl(207, 47%, 69%); +} + +legend { + font-weight: 600; +} + +button, +input[type="submit"] { + background-color: hsl(207, 47%, 39%); + color: hsl(0, 0%, 90%); + border: none; + border-radius: 2px; + padding: 5px 8px; + cursor: pointer; +} + +input[type="submit"] { + grid-column: 1 / 3; +} + +button:hover, +input[type="submit"]:hover { + background-color: hsl(207, 47%, 49%); + color: hsl(0, 0%, 90%); +} diff --git a/2-api/cyf-ecommerce-api/readme.md b/2-api/cyf-ecommerce-api/readme.md new file mode 100644 index 00000000..20022d7b --- /dev/null +++ b/2-api/cyf-ecommerce-api/readme.md @@ -0,0 +1,38 @@ +# PostgreSQL Ecommerce API v2.0 + +## Description + +Written from scratch Frontend with HTML, CSS, Vanilla JS +Showcasing the Backend Node Express API and its functionality +That is interacting with a PostgreSQL Database + +## Screenshot: + +![](project-screenshot.png) + +## Instructions: + +1. clone this repo +2. install [postgresql](https://www.postgresql.org/) +3. inside the root of the repository, open a terminal: +4. move into the `2-api/cyf-ecommerce-api/` folder: + `cd 2-api/cyf-ecommerce-api/` +5. create the database + `createdb cyf_ecommerce` +6. populate the database + `createdb -d cyf_ecommerce -f cyf_ecommerce.sql` +7. install the project dependencies: + `npm install` +8. create your own `.env` file in the `/cyf-hotels-api/` directory and add your PostgreSQL details (refer to the `.env.example` file) : + +``` +DB_HOST= +DB_PORT= +DB_NAME= +DB_USER= +DB_PASS= +``` + +9. start the server in develoment mode: + `npm run dev` +10. visit http://localhost:3000 diff --git a/2-api/cyf-ecommerce-api/server.js b/2-api/cyf-ecommerce-api/server.js new file mode 100644 index 00000000..65748043 --- /dev/null +++ b/2-api/cyf-ecommerce-api/server.js @@ -0,0 +1,624 @@ +require("dotenv").config(); +const express = require("express"); +const app = express(); + +app.use(express.static("public")); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +const { + isValidId, + isInjectionFree, + isValidDate, + isValidOrderReference, +} = require("./utils"); + +const { Pool } = require("pg"); + +const db = new Pool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT, + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASS, +}); + +// [2] Add a new GET endpoint `/customers` to return all the customers from the database +app.get("/customers", async (req, res) => { + try { + const queryGetCustomers = await db.query( + `SELECT * + FROM customers;` + ); + res.status(200).json({ success: true, data: queryGetCustomers.rows }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// [3] Add a new GET endpoint /customers/:customerId to load a single customer by ID. +app.get("/customers/:customerId", async (req, res) => { + try { + const customerId = req.params.customerId; + + if (!isValidId(customerId)) { + return res.status(400).json({ + success: false, + error: `Customer id: ${customerId} is not a valid integer`, + }); + } + + const queryGetCustomerById = await db.query( + `SELECT * + FROM customers + WHERE id = $1;`, + [customerId] + ); + + if (queryGetCustomerById.rowCount === 0) { + return res.status(400).json({ + success: false, + error: `No customer with the id: ${customerId} was found`, + }); + } + res.status(200).json({ success: true, data: queryGetCustomerById.rows }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// [2] Add a new GET endpoint `/suppliers` to return all the suppliers from the database +app.get("/suppliers", async (req, res) => { + try { + const queryGetSuppliers = await db.query( + `SELECT * + FROM suppliers;` + ); + res.status(200).json({ success: true, data: queryGetSuppliers.rows }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// [2] Add a new GET endpoint `/products` to return all the product names along with their prices and supplier names. +// [3] Update the previous GET endpoint /products to filter the list of products by name using a query parameter, for example /products?name=Cup. This endpoint should still work even if you don't use the name query parameter! +app.get("/products", async (req, res) => { + try { + const productName = req.query.name; + + if (!productName) { + const queryGetProducts = await db.query( + `SELECT + p.product_name, + pa.unit_price, + s.supplier_name + FROM product_availability pa + INNER JOIN products p + ON (pa.prod_id = p.id) + INNER JOIN suppliers s + ON (pa.supp_id = s.id) + ORDER BY p.product_name, pa.unit_price;` + ); + + return res + .status(200) + .json({ success: true, data: queryGetProducts.rows }); + } + + if (productName && !isInjectionFree(productName)) { + return res + .status(400) + .json({ success: false, error: "You are attempting SQL Injection" }); + } + + const queryGetProductByName = await db.query( + `SELECT + p.product_name, + pa.unit_price, + s.supplier_name + FROM product_availability pa + INNER JOIN products p + ON (pa.prod_id = p.id) + INNER JOIN suppliers s + ON (pa.supp_id = s.id) + WHERE LOWER(p.product_name) LIKE '%' || LOWER($1) || '%' + ORDER BY p.product_name, pa.unit_price;`, + [productName] + ); + + if (queryGetProductByName.rowCount === 0) { + return res.status(400).json({ + success: false, + error: `No products with the name: ${productName} were found`, + }); + } + + return res + .status(200) + .json({ success: true, data: queryGetProductByName.rows }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// [3] Add a new POST endpoint /customers to create a new customer with name, address, city and country. +app.post("/customers", async (req, res) => { + try { + const { + name: newName, + address: newAddress, + city: newCity, + country: newCountry, + } = req.body; + + // this has issues because of blank spaces in user input... + // if ( + // !isInjectionFree(newName) || + // !isInjectionFree(newAddress) || + // !isInjectionFree(newCity) || + // !isInjectionFree(newCountry) + // ) { + // return res + // .status(400) + // .json({ success: false, error: "You may be attempting SQL Injection" }); + // } + + const queryGetCustomerByName = await db.query( + `SELECT 1 + FROM customers + WHERE LOWER(name) = $1;`, + [newName.toLowerCase()] + ); + + if (queryGetCustomerByName.rowCount > 0) { + return res.status(400).json({ + success: false, + error: `Customer with name: ${newName} already exists`, + }); + } + + const queryAddCustomer = await db.query( + `INSERT INTO customers (name, address, city, country) + VALUES ($1, $2, $3, $4) + RETURNING id;`, + [newName, newAddress, newCity, newCountry] + ); + + const newId = queryAddCustomer.rows[0].id; + + res.status(200).json({ + success: true, + message: `Customer CREATED, with id: ${newId}, name: ${newName}, address: ${newAddress}, city: ${newCity}, country: ${newCountry}`, + }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// [3] Add a new POST endpoint /products to create a new product. +app.post("/products", async (req, res) => { + try { + const newProductName = req.body.productname; + + // this has issues because of blank spaces in user input... + // if (!isInjectionFree(newProductName)) { + // return res.status(400).json({ + // success: false, + // error: `${newProductName} is not a valid Product name (or you may be attempting SQL Injection)`, + // }); + // } + + if (!newProductName) { + return res.status(400).json({ + success: false, + error: `Please provide a product name.`, + }); + } + + const queryGetProductByName = await db.query( + `SELECT 1 + FROM products + WHERE product_name = $1;`, + [newProductName] + ); + + if (queryGetProductByName.rowCount > 0) { + return res.status(400).json({ + success: false, + error: `Product with name: ${newProductName} already exists`, + }); + } + + const queryAddProduct = await db.query( + `INSERT INTO products (product_name) + VALUES ($1) + RETURNING id;`, + [newProductName] + ); + + const newId = queryAddProduct.rows[0].id; + + res.status(200).json({ + success: true, + message: `Product CREATED, with id: ${newId}, with name: ${newProductName}`, + }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// [3] Add a new POST endpoint /availability to create a new product availability (with a price and a supplier id). Check that the price is a positive integer and that both the product and supplier ID's exist in the database, otherwise return an error. +app.post("/availability", async (req, res) => { + try { + const { + productid: productId, + supplierid: supplierId, + unitprice: unitPrice, + } = req.body; + + if (!isValidId(productId)) { + return res.status(400).json({ + success: false, + error: `The product id ${productId} must be a positive integer`, + }); + } + + if (!isValidId(supplierId)) { + return res.status(400).json({ + success: false, + error: `The supplier id ${supplierId} must be a positive integer`, + }); + } + + if (!parseInt(unitPrice) || unitPrice <= 0) { + return res.status(400).json({ + success: false, + error: `The unit price ${unitPrice} must be a positive integer (not a float)`, + }); + } + + const queryProductById = await db.query( + `SELECT p.id + FROM products p + WHERE p.id = $1;`, + [productId] + ); + + if (queryProductById.rowCount === 0) { + return res.status(400).json({ + success: false, + error: `The product id ${productId} does not exist`, + }); + } + + const querySupplierById = await db.query( + `SELECT s.id + FROM suppliers s + WHERE s.id = $1;`, + [supplierId] + ); + + if (querySupplierById.rowCount === 0) { + return res.status(400).json({ + success: false, + error: `The supplier id ${supplierId} does not exist`, + }); + } + + const queryProductByIdAndSupplierById = await db.query( + `SELECT * + FROM product_availability + WHERE prod_id = $1 + AND supp_id = $2;`, + [productId, supplierId] + ); + + if (queryProductByIdAndSupplierById.rowCount > 0) { + return res.status(400).json({ + success: false, + error: `The product id ${productId} already exists for the supplier id ${supplierId}`, + }); + } + + const queryAddProductAvailability = await db.query( + `INSERT INTO product_availability (prod_id, supp_id, unit_price) + VALUES ($1, $2, $3) + RETURNING id;`, + [productId, supplierId, unitPrice] + ); + + const newId = queryAddProductAvailability.rows[0].id; + + res.status(200).json({ + success: true, + message: `Product availablity CREATED, with id: ${newId} with product id: ${productId}, with supplier id: ${supplierId}, with unit price: ${unitPrice}`, + }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// [3] Add a new POST endpoint /customers/:customerId/orders to create a new order (including an order date, and an order reference) for a customer. Check that the customerId corresponds to an existing customer or return an error. +app.post("/customers/:customerId/orders", async (req, res) => { + try { + const customerId = req.params.customerId; + + const { orderdate: newOrderDate, orderreference: newOrderReference } = + req.body; + + if (!isValidId(customerId)) { + return res.status(400).json({ + success: false, + error: `Customer id: ${customerId} is not a valid integer`, + }); + } + + if (!isValidDate(newOrderDate)) { + return res.status(400).json({ + success: false, + error: `Order date: ${newOrderDate} is not a valid date format (YYYY-MM-DD)`, + }); + } + + if (!isValidOrderReference(newOrderReference)) { + return res.status(400).json({ + success: false, + error: `Order reference: ${newOrderReference} is not a valid format (ORD000)`, + }); + } + + const queryCustomerId = await db.query( + `SELECT * FROM customers WHERE id = $1;`, + [customerId] + ); + + if (queryCustomerId.rowCount === 0) { + return res.status(400).json({ + success: false, + error: `No Customer with id: ${customerId} found`, + }); + } + + const queryInsert = await db.query( + `INSERT INTO orders (order_date, order_reference, customer_id) + VALUES($1, $2, $3) + RETURNING id`, + [newOrderDate, newOrderReference, customerId] + ); + + const newId = queryInsert.rows[0].id; + + res.status(200).json({ + success: true, + message: `Customer order CREATED, with id: ${newId}, with order date: ${newOrderDate}, with order reference: ${newOrderReference}, with customer id: ${customerId}`, + }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// [3] Add a new PUT endpoint /customers/:customerId to update an existing customer (name, address, city and country). +app.put("/customers/:customerId", async (req, res) => { + try { + const customerId = req.params.customerId; + const { + name: newName, + address: newAddress, + city: newCity, + country: newCountry, + } = req.body; + + if (!isValidId(customerId)) { + return res.status(400).json({ + success: false, + error: `Customer id: ${customerId} is not a valid integer`, + }); + } + + if (!newName || !newAddress || !newCity || !newCountry) { + return res.status(400).json({ + success: false, + error: `One of: name, city, address or country are empty`, + }); + } + + // this has issues because of blank spaces in user input... + // if ( + // !isInjectionFree(newName) || + // !isInjectionFree(newAddress) || + // !isInjectionFree(newCity) || + // !isInjectionFree(newCountry) + // ) { + // return res.status(400).json({ + // success: false, + // error: `One of: name, city, address or country are not valid (or you may be attempting SQL Injections)`, + // }); + // } + + const queryCustomerId = await db.query( + `SELECT * FROM customers WHERE id = $1;`, + [customerId] + ); + + if (queryCustomerId.rowCount === 0) { + return res.status(400).json({ + success: false, + error: `No Customer with id: ${customerId} found`, + }); + } + + const insertQuery = await db.query( + `UPDATE customers + SET name = $1, address = $2, city = $3, country = $4 + WHERE id = $5`, + [newName, newAddress, newCity, newCountry, customerId] + ); + // console.log(insertQuery); + + res.status(200).json({ + success: true, + message: `Customer id: ${customerId} UPDATED, with name: ${newName}, with address: ${newAddress}, with city: ${newCity}, with country: ${newCountry}`, + }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// [3] Add a new DELETE endpoint /orders/:orderId to delete an existing order along with all the associated order items. +app.delete("/orders/:orderId", async (req, res) => { + try { + const orderId = req.params.orderId; + + if (!isValidId(orderId)) { + return res.status(400).json({ + success: false, + error: `Order id: ${orderId} is not a valid integer`, + }); + } + + const queryOrderId = await db.query( + `SELECT 1 + FROM orders + WHERE id = $1`, + [orderId] + ); + + if (queryOrderId.rowCount === 0) { + return res.status(400).json({ + success: false, + error: `No Order with id: ${orderId} found`, + }); + } + + const queryDeletOrderItems = await db.query( + `DELETE + FROM order_items + WHERE order_id = $1`, + [orderId] + ); + // console.log("queryDeletOrderItems:", queryDeletOrderItems); + + const queryDeleteOrder = await db.query( + `DELETE + FROM orders + WHERE id = $1`, + [orderId] + ); + // console.log("queryDeleteOrder:", queryDeleteOrder); + + res.status(200).json({ + success: true, + message: `Order id: ${orderId} DELETED, and associated order items`, + }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// [3] Add a new DELETE endpoint /customers/:customerId to delete an existing customer only if this customer doesn't have orders. +app.delete("/customers/:customerId", async (req, res) => { + try { + const customerId = req.params.customerId; + + if (!isValidId(customerId)) { + return res.status(400).json({ + success: false, + error: `Customer id: ${customerId} is not a valid integer`, + }); + } + + const queryCustomerId = await db.query( + `SELECT * + FROM customers + WHERE id = $1`, + [customerId] + ); + + if (queryCustomerId.rowCount === 0) { + return res.status(400).json({ + success: false, + error: `No Customer with id: ${customerId} found`, + }); + } + + const queryOrdersWithCustomerId = await db.query( + `SELECT * + FROM orders + WHERE customer_id = $1;`, + [customerId] + ); + // console.log("queryOrdersWithCustomerId:", queryOrdersWithCustomerId); + + if (queryOrdersWithCustomerId.rowCount > 0) { + return res.status(400).json({ + success: false, + error: `Customer id: ${customerId} has Orders`, + }); + } + + const queryDeleteCustomer = await db.query( + `DELETE + FROM customers + WHERE id = $1`, + [customerId] + ); + // console.log("queryDeleteCustomer:", queryDeleteCustomer); + + res.status(200).json({ + success: true, + message: `Customer id: ${customerId} DELETED`, + }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// [3] Add a new GET endpoint /customers/:customerId/orders to load all the orders along with the items in the orders of a specific customer. Especially, the following information should be returned: order references, order dates, product names, unit prices, suppliers and quantities. +app.get("/customers/:customerId/orders", async (req, res) => { + try { + const customerId = req.params.customerId; + + const queryCustomerOrders = await db.query( + `SELECT + o.customer_id, + o.order_reference, + o.order_date, + p.product_name, + s.supplier_name, + pa.unit_price, + oi.quantity + FROM orders o + INNER JOIN order_items oi + ON o.id = oi.order_id + INNER JOIN product_availability pa + ON oi.product_id = pa.prod_id + AND oi.supplier_id = pa.supp_id + INNER JOIN products p + ON pa.prod_id = p.id + INNER JOIN suppliers s + ON pa.supp_id = s.id + WHERE o.customer_id = $1;`, + [customerId] + ); + + if (queryCustomerOrders.rowCount === 0) { + return res.status(400).json({ + success: false, + error: `Customer id: ${customerId} has no orders`, + }); + } + + res.status(200).json({ success: true, data: queryCustomerOrders.rows }); + } catch (error) { + res.status(500).json({ success: false, error: error }); + } +}); + +// Root route serving the index page +app.get("/", (req, res) => { + res.sendFile(__dirname + "./public/index.html"); +}); + +app.listen(3000, () => { + console.log("The server is running on port 3000"); +}); diff --git a/2-api/cyf-ecommerce-api/utils.js b/2-api/cyf-ecommerce-api/utils.js new file mode 100644 index 00000000..9ff20a93 --- /dev/null +++ b/2-api/cyf-ecommerce-api/utils.js @@ -0,0 +1,34 @@ +function isValidId(id) { + if ( + /^[0-9]+$/.test(id) === false || + parseInt(id) === NaN || + parseInt(id) <= 0 + ) { + return false; + } else { + return true; + } +} + +// this really needs to include "space" to be a catchall for everything but that probably allows injection +function isInjectionFree(queryParameter) { + const regex = /^[a-zA-Z0-9_\-.,]+$/g; + return regex.test(queryParameter); +} + +function isValidDate(date) { + const regex = /^\d{4}-\d{2}-\d{2}$/; + return regex.test(date); +} + +function isValidOrderReference(orderReference) { + const regex = /^ORD\d{3}$/; + return regex.test(orderReference); +} + +module.exports = { + isValidId, + isInjectionFree, + isValidDate, + isValidOrderReference, +};