Source: app.js

  1. /**
  2. @namespace app
  3. @since 23/10/2020
  4. @version 1.0
  5. @author Brennan Wilkes
  6. */
  7. //Includes
  8. const path = require("path");
  9. const moment = require("moment");
  10. const {server} = require(path.join(__dirname,"backend","server.js"));
  11. const {database} = require(path.join(__dirname,"backend","database.js"));
  12. //Setup
  13. require('dotenv').config();
  14. const ADMIN_USER = process.env.ADMIN_USER;
  15. const ADMIN_PASS = process.env.ADMIN_PASS;
  16. /**
  17. Validates a number to be inserted into MariaDB
  18. @param {number} n Number to validate
  19. @param {number} d Default value to return in the case of invalid input. Defaults to 0
  20. @returns {number} A number which is guaranteed to be valid. Returns d in n is invalid
  21. @memberof app
  22. */
  23. const validateNum = (n,d=0) => (typeof(n)==="number" && !isNaN(n) ? n : d);
  24. /**
  25. Creates a new promise and immediately rejects it
  26. @returns {Promise} Always rejected promise which resolves to "Invalid credentials"
  27. @memberof app
  28. */
  29. const failedLogin = () => {
  30. return new Promise((res,rej)=> {
  31. rej("Invalid credentials");
  32. });
  33. }
  34. /**
  35. Search Query object to validate user input for SQL queries.
  36. Contains a list of all used parameters for all queries.
  37. User input is compared to this parameters, and validated.
  38. Data members of this object are guaranteed to be valid.
  39. Applies %% wrapping when appropriate.
  40. @class
  41. @memberof app
  42. */
  43. const searchQuery = {
  44. /**
  45. Name (used mainly for drink name, but can also refer to ingredient names)
  46. @type {string}
  47. */
  48. name : "",
  49. /**
  50. Array of all ingredients which must be required by a drink
  51. @type {string[]}
  52. */
  53. contains: [""],
  54. /**
  55. User who ordered a drink
  56. @type {string}
  57. */
  58. orderedBy: "",
  59. /**
  60. If a drink is on the rocks.
  61. "1" for ice.
  62. "0" for no ice.
  63. "" for either.
  64. @type {string}
  65. */
  66. onIce: "",
  67. /**
  68. Method of mixing drink.
  69. Can be either "shaken" or "stirred".
  70. "" for either.
  71. @type {string}
  72. */
  73. mixMethod: "",
  74. /**
  75. Minimum alcohol percentage.
  76. Defaults to 0.
  77. @type {number}
  78. */
  79. percentage: 0,
  80. /**
  81. Minimum drink rating
  82. Defaults to 0.
  83. @type {number}
  84. */
  85. rating: 0,
  86. /**
  87. Maximum drink price
  88. Defaults to 100.
  89. @type {number}
  90. */
  91. price: 100,
  92. /**
  93. If a drink contains a sweet ingredient.
  94. "1" for sweet.
  95. "0" for savoury.
  96. "" for either or anything.
  97. @type {string}
  98. */
  99. isSweet: "",
  100. /**
  101. If a drink contains a liquor or liquer.
  102. "1" for liquor.
  103. "0" for liquer.
  104. "" for either or anything.
  105. @type {string}
  106. */
  107. liquor: "",
  108. /**
  109. ID of the glass drink must be drunk from
  110. "" for any glass.
  111. @type {string}
  112. */
  113. glass: "",
  114. /**
  115. ID of the drink
  116. "" for any drink.
  117. @type {string}
  118. */
  119. id: "",
  120. /**
  121. Username of person ordering drink.
  122. Defaults to Unknown Name
  123. @type {string}
  124. */
  125. userName: "",
  126. /**
  127. Date of birth of person ordering drink.
  128. Defaults to Jan 1st 1970.
  129. @type {string}
  130. */
  131. userDob: "1970-01-01",
  132. /**
  133. ID of the drink, but with different validation.
  134. Defaults to 0.
  135. @type {number}
  136. */
  137. drinkId: 0,
  138. /**
  139. Resets all string datamembers to empty
  140. */
  141. reset(){
  142. Object.keys(this).forEach(key => {
  143. if(typeof(this[key])=="string"){
  144. this[key] = "";
  145. }
  146. });
  147. },
  148. /**
  149. Adds all given parameters of params as datamembers
  150. In practice this updates datamembers with all recognizable attributes
  151. in the given object. This means that extra clutter will not be injected.
  152. @param {object} params Object of key/value pairs to record.
  153. */
  154. update(params){
  155. this.reset();
  156. Object.keys(params).forEach(key => {
  157. this[key] = params[key];
  158. });
  159. },
  160. /**
  161. Where the magic happens. Iterates over all datamembers and applies both
  162. general and specific sanitzization. Inputs still must be injected via
  163. a prepared statement.
  164. */
  165. sanitzize(){
  166. //Wraps all string type datamembers who are not userDob or userName in %'s.
  167. //This is for LIKE sql comparison.
  168. //Attributes glass and id will only be wrapped if their length is 0.
  169. Object.keys(this).forEach(key => {
  170. if(typeof(this[key])=="string" && key!=="userDob" && key!=="userName" ){
  171. this[key] = key==="glass"||key==="id" ? (this[key].length > 0 ? this[key].toLowerCase() : "%%" ) : `%${this[key].toLowerCase()}%`;
  172. }
  173. });
  174. this.contains = this.contains.map(c => `%${c.toLowerCase()}%`);
  175. //Resets contains array to defaults if invalid data is given.
  176. if(!this.contains || typeof(this.contains)!=="object" || !this.contains.length){
  177. this.contains = [""];
  178. }
  179. //Validates userDob as a correctly formatted date.
  180. this.userDob = moment(this.userDob, 'YYYY/MM/DD').isValid() ? moment(this.userDob,'YYYY-MM-DD').format("YYYY-MM-DD") : "1970-01-01";
  181. //Applies default value to userName
  182. this.userName = this.userName.length > 0 ? this.userName : "Unknown Name";
  183. //Number validation for numerical attributes.
  184. this.rating = validateNum(this.rating);
  185. this.percentage = validateNum(this.percentage);
  186. this.price = validateNum(this.price,100);
  187. this.drinkId = validateNum(this.drinkId,0);
  188. },
  189. /**
  190. Generates an appropriate ordered list of parameters for the advanced search query.
  191. @returns {string[]} Ordered list of SQL parameters
  192. */
  193. getArgs(){
  194. let args = [
  195. searchQuery.mixMethod,
  196. searchQuery.onIce,
  197. searchQuery.name,
  198. searchQuery.rating,
  199. searchQuery.glass,
  200. searchQuery.price,
  201. ...searchQuery.contains
  202. ];
  203. if(searchQuery.percentage > 0 || searchQuery.liquor.length > 2){
  204. args.push(searchQuery.percentage);
  205. args.push(searchQuery.liquor);
  206. }
  207. if(searchQuery.orderedBy && searchQuery.orderedBy.length > 2){
  208. args.push(searchQuery.orderedBy);
  209. }
  210. if(searchQuery.isSweet.length > 2){
  211. args.push(searchQuery.isSweet);
  212. }
  213. return args;
  214. }
  215. }
  216. //Initialize DB connection
  217. database.init();
  218. //Initialize webserver
  219. server.init();
  220. //Get all drinks
  221. //Projection query 1
  222. server.route("drinks", req => database.get(`SELECT * FROM drinkRecipe LIMIT 100`));
  223. //Advanced search
  224. //Join query and division query
  225. server.route("drinks/advanced", req => {
  226. //Record and sanitzize user inputs
  227. searchQuery.update(req.body);
  228. searchQuery.sanitzize();
  229. //Initial contains ingredient SQL codeblock.
  230. let containsSQL = `
  231. SELECT drinkRequires.drinkId FROM drinkRequires
  232. INNER JOIN ingredient
  233. ON drinkRequires.ingredientId=ingredient.id
  234. WHERE ingredient.name LIKE ?
  235. `;
  236. //Recursively wrap for each additional queried ingredient.
  237. searchQuery.contains.slice(1).forEach((c, i) => {
  238. containsSQL = ` SELECT drinkRequires.drinkId FROM drinkRequires
  239. INNER JOIN ingredient
  240. ON drinkRequires.ingredientId=ingredient.id
  241. WHERE drinkRequires.drinkId IN
  242. (${containsSQL})
  243. AND ingredient.name LIKE ? `;
  244. });
  245. //Wrap in a group
  246. containsSQL = `(${containsSQL})groupContains`;
  247. //Advanced multi parameters search
  248. return database.get(`
  249. SELECT DISTINCT drinkRecipe.* FROM drinkRecipe
  250. INNER JOIN (
  251. SELECT DISTINCT drinkRecipe.* FROM drinkRecipe
  252. INNER JOIN (
  253. SELECT drinkRequires.* FROM drinkRequires
  254. INNER JOIN (
  255. SELECT DISTINCT drinkRecipe.* FROM drinkRecipe
  256. WHERE mixMethod LIKE ?
  257. AND onIce LIKE ?
  258. AND name LIKE ?
  259. AND rating>=?
  260. AND glassID LIKE ?
  261. AND price<=?
  262. )group1
  263. ON drinkRequires.drinkId=group1.id
  264. INNER JOIN ingredient
  265. ON ingredientId=ingredient.id `+ (searchQuery.percentage > 0 || searchQuery.liquor.length > 2 ? `
  266. INNER JOIN alcohol
  267. ON ingredient.id=alcohol.id
  268. INNER JOIN alcoholType
  269. ON alcohol.percentage=alcoholType.percentage ` : ` `) + `
  270. INNER JOIN ${containsSQL}
  271. ON groupContains.drinkId=drinkRequires.drinkId
  272. ` + (searchQuery.percentage > 0 || searchQuery.liquor.length > 2 ? `
  273. WHERE alcohol.percentage>=?
  274. AND alcoholType.liquor LIKE ?
  275. `: ` `) + `
  276. )group2
  277. ON group2.drinkId=drinkRecipe.id ` + (searchQuery.orderedBy && searchQuery.orderedBy.length > 2 ?
  278. `INNER JOIN transaction
  279. ON group2.drinkId=transaction.drinkId
  280. WHERE UPPER(transaction.customerName) LIKE UPPER(?)` :
  281. ` `)+`
  282. )group3
  283. ON group3.id=drinkRecipe.id
  284. INNER JOIN drinkRequires
  285. ON drinkRequires.drinkId=drinkRecipe.id` + (searchQuery.isSweet.length > 2 ? `
  286. INNER JOIN ingredient
  287. ON ingredientId=ingredient.id
  288. INNER JOIN juice
  289. ON ingredient.id=juice.id
  290. INNER JOIN juiceFruit
  291. ON juice.fruitName=juiceFruit.fruitName
  292. AND juiceFruit.isSweet LIKE ? `: ` `) + " LIMIT 100",searchQuery.getArgs());
  293. }, "post");
  294. //Search by name or id
  295. //Selection Query
  296. server.route("drinks", req => {
  297. searchQuery.update(req.body);
  298. searchQuery.sanitzize();
  299. return database.get(`SELECT drinkRecipe.*,
  300. parent.name as versionOfName FROM drinkRecipe
  301. LEFT JOIN (SELECT drinkRecipe.* FROM drinkRecipe)parent
  302. ON drinkRecipe.versionOf=parent.id
  303. WHERE drinkRecipe.name LIKE ? AND drinkRecipe.id LIKE ?`,[searchQuery.name,searchQuery.id]);
  304. }, "post");
  305. //ingredients by quantity
  306. //Selection Query 2
  307. server.route("ingredients", req => {
  308. return database.get(`SELECT ingredient.* from ingredient INNER JOIN ingredientAvailable ON ingredient.quantity=ingredientAvailable.quantity WHERE ingredientAvailable.isAvailable=false`);
  309. });
  310. //ingredients by drink Id
  311. //Selection Query 3
  312. server.route("ingredients", req => {
  313. searchQuery.update(req.body);
  314. searchQuery.sanitzize();
  315. return database.get(`SELECT drinkRequires.quantity AS quantityInDrink, ingredient.*, alcohol.glassId, alcoholType.*, juiceFruit.* FROM
  316. (SELECT drinkRecipe.id FROM drinkRecipe WHERE drinkRecipe.id=?)queriedDrink
  317. INNER JOIN drinkRequires ON queriedDrink.id=drinkRequires.drinkId
  318. INNER JOIN ingredient ON drinkRequires.ingredientId=ingredient.id
  319. LEFT JOIN alcohol ON ingredient.id=alcohol.id
  320. LEFT JOIN juice ON ingredient.id=juice.id
  321. LEFT JOIN juiceFruit ON juice.fruitName=juiceFruit.fruitName
  322. LEFT JOIN alcoholType ON alcohol.percentage=alcoholType.percentage`,[searchQuery.id]);
  323. }, "post");
  324. //ingredient by ingredient Id
  325. //Selection Query 4
  326. server.route("ingredient", req => {
  327. searchQuery.update(req.body);
  328. searchQuery.sanitzize();
  329. return database.get(`
  330. SELECT ingredient.*, alcohol.glassId, glass.name as glassName, alcoholType.*, juiceFruit.* from ingredient
  331. LEFT JOIN alcohol ON ingredient.id=alcohol.id
  332. LEFT JOIN juice ON ingredient.id=juice.id
  333. LEFT JOIN juiceFruit ON juice.fruitName=juiceFruit.fruitName
  334. LEFT JOIN alcoholType ON alcohol.percentage=alcoholType.percentage
  335. LEFT JOIN glass ON alcohol.glassId=glass.id
  336. WHERE ingredient.id=?`,[searchQuery.id]);
  337. }, "post");
  338. //Number of drinks ordered by specific user
  339. //Aggregation query 1
  340. server.route("drinks/orderCount", req => {
  341. searchQuery.update(req.body);
  342. searchQuery.sanitzize();
  343. return database.get(`SELECT COUNT(transaction.id) FROM transaction WHERE UPPER(transaction.customerName) UPPER(?)`,[searchQuery.orderedBy]);
  344. },"post");
  345. //Most ordered drink
  346. //Aggregation query 2
  347. server.route("popular/drinks", req => {
  348. return database.get(`SELECT drinkRecipe.* FROM (SELECT transaction.drinkId FROM transaction GROUP BY drinkId ORDER BY COUNT(drinkId) DESC LIMIT 10)popular INNER JOIN drinkRecipe ON drinkId=id`);
  349. });
  350. //Most ordered ingredient
  351. //Nested Aggregation query
  352. server.route("popular/ingredients", req => {
  353. return database.get(`SELECT ingredient.* FROM (SELECT drinkRequires.ingredientId FROM transaction INNER JOIN drinkRequires ON transaction.drinkId=drinkRequires.drinkId GROUP BY drinkRequires.ingredientId ORDER BY COUNT(drinkRequires.ingredientId) DESC LIMIT 25)popular INNER JOIN ingredient ON popular.ingredientId=ingredient.id`);
  354. });
  355. //Return list of glasses
  356. //Projection Query 2
  357. server.route("glasses", req => database.get(`SELECT * FROM glass`));
  358. server.route("orders", req => database.get(`
  359. SELECT transaction.*, drinkRecipe.name, drinkRecipe.price
  360. FROM transaction INNER JOIN drinkRecipe
  361. ON transaction.drinkId=drinkRecipe.id
  362. ORDER BY transaction.date DESC`));
  363. //Order ingredient by ingredient Id
  364. //UPDATE query 1
  365. server.route("order", req => {
  366. if(req.body.userName === ADMIN_USER && req.body.userPass === ADMIN_PASS){
  367. searchQuery.update(req.body);
  368. searchQuery.sanitzize();
  369. return database.get(`UPDATE ingredient SET ingredient.quantity=ingredient.quantity+10 WHERE ingredient.id=?`,[searchQuery.id]);
  370. }
  371. else{
  372. return failedLogin();
  373. }
  374. }, "post");
  375. //Delete ingredient by ingredient Id
  376. //DELETE query 1
  377. server.route("delete/ingredient", req => {
  378. if(req.body.userName === ADMIN_USER && req.body.userPass === ADMIN_PASS){
  379. searchQuery.update(req.body);
  380. searchQuery.sanitzize();
  381. return database.get(`DELETE FROM ingredient WHERE ingredient.id=?`,[searchQuery.id]);
  382. }
  383. else{
  384. return failedLogin();
  385. }
  386. }, "post");
  387. //Delete drink drinkRecipe by drinkRecipe Id
  388. //DELETE query 2
  389. server.route("delete/drink", req => {
  390. if(req.body.userName === ADMIN_USER && req.body.userPass === ADMIN_PASS){
  391. searchQuery.update(req.body);
  392. searchQuery.sanitzize();
  393. return database.get(`DELETE FROM drinkRecipe WHERE drinkRecipe.id=?`,[searchQuery.id]);
  394. }
  395. else{
  396. return failedLogin();
  397. }
  398. }, "post");
  399. //Add a new customer
  400. //INSERT / UPDATE query 3
  401. server.route("customer", req => {
  402. searchQuery.update(req.body);
  403. searchQuery.sanitzize();
  404. return database.insert("customer",{fullName:searchQuery.userName,dateOfBirth:searchQuery.userDob},true);
  405. }, "post");
  406. //Order a drink
  407. //INSERT query / UPDATE query 3
  408. server.route("purchase", req => {
  409. searchQuery.update(req.body);
  410. searchQuery.sanitzize();
  411. //Generate a custom Promise event
  412. return new Promise((resolve,reject)=> {
  413. //Get a list of ingredient of the requested drink
  414. database.get(`
  415. SELECT drinkRequires.drinkId, drinkRequires.ingredientId, drinkRequires.quantity as quantityInDrink, ingredientAvailable.* FROM drinkRequires
  416. INNER JOIN ingredient ON drinkRequires.ingredientId=ingredient.id
  417. INNER JOIN ingredientAvailable ON ingredient.quantity=ingredientAvailable.quantity
  418. WHERE drinkRequires.drinkId=?`,[searchQuery.drinkId])
  419. .then(getRes => {
  420. //Determine if all ingredients are available.
  421. if(getRes.reduce((status,nextIngr) => status * nextIngr.isAvailable,1)){
  422. //Insert the new transaction record
  423. database.insert("transaction",{
  424. date:moment().format("YYYY-MM-DD"),
  425. drinkId:searchQuery.drinkId,
  426. customerName:searchQuery.userName})
  427. .then(orderRes => {
  428. //Recursively decrement database quantity of all ingredients
  429. let sqlPromises = []
  430. let sqlQuery;
  431. getRes.forEach((ingr, i) => {
  432. sqlQuery = database.get(
  433. `UPDATE ingredient SET ingredient.quantity=? WHERE ingredient.id=?`,[
  434. Math.max(0,parseInt(Math.ceil(ingr.quantity)) - parseInt(Math.ceil(ingr.quantityInDrink))),
  435. ingr.ingredientId]);
  436. sqlPromises.push(sqlQuery);
  437. });
  438. //On resolution of all queries, resolve to the final data
  439. //On errors anywhere along the chain, reject the Promise with the error details
  440. Promise.all(sqlPromises).then(res => {
  441. resolve(orderRes);
  442. }).catch( err => {
  443. reject(err);
  444. });
  445. }).catch(err => {
  446. reject(err);
  447. });
  448. }
  449. //Return 409
  450. else{
  451. reject("Drink not available");
  452. }
  453. }).catch(err=>{
  454. reject(err);
  455. });
  456. });
  457. }, "post");
  458. //Start the webserver
  459. server.start();