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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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:
+
+
+
+## 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,
+};