diff --git a/Week3/Ex2transactions/data.js b/Week3/Ex2transactions/data.js new file mode 100644 index 000000000..18544b385 --- /dev/null +++ b/Week3/Ex2transactions/data.js @@ -0,0 +1,40 @@ +export const accounts = [ + { account_number: 101, balance: 5000 }, + { account_number: 102, balance: 12000 }, + { account_number: 103, balance: 7500 }, + { account_number: 104, balance: 3000 }, + { account_number: 105, balance: 9500 }, +]; + +export const account_changes = [ + { + account_number: 101, + amount: 1000, + changed_date: "2025-09-01", + remark: "Salary deposit", + }, + { + account_number: 102, + amount: -500, + changed_date: "2025-09-02", + remark: "Grocery shopping", + }, + { + account_number: 103, + amount: 2000, + changed_date: "2025-09-03", + remark: "Freelance payment", + }, + { + account_number: 104, + amount: -1000, + changed_date: "2025-09-04", + remark: "Rent payment", + }, + { + account_number: 105, + amount: 1500, + changed_date: "2025-09-05", + remark: "Gift received", + }, +]; diff --git a/Week3/Ex2transactions/transaction.js b/Week3/Ex2transactions/transaction.js new file mode 100644 index 000000000..93f4e8f91 --- /dev/null +++ b/Week3/Ex2transactions/transaction.js @@ -0,0 +1,48 @@ +import { Client } from "pg"; +const config = { + host: "localhost", + user: "hyfuser", + password: "hyfpassword", + database: "transactions_week3", + port: 5432, +}; +const client = new Client(config); + +async function seedDatabase(client) { + try { + await client.connect(); + console.log("Connected to PostgreSQL database!"); + + const donator_account_number = 101; + const receiver_account_number = 102; + const amount = 1000; + const changed_date = new Date().toISOString().slice(0, 10); // 'YYYY-MM-DD' + const remark = `Transfer from 101 to 102`; + + await client.query("BEGIN"); + await client.query( + "UPDATE ACCOUNT SET balance = balance - $1 WHERE account_number = $2", + [amount, donator_account_number] + ); + await client.query( + "UPDATE ACCOUNT SET balance = balance + $1 WHERE account_number = $2", + [amount, receiver_account_number] + ); + await client.query( + "INSERT INTO account_changes(account_number, amount, changed_date, remark) VALUES($1, $2, $3, $4)", + [donator_account_number, -amount, changed_date, remark] + ); + await client.query( + "INSERT INTO account_changes(account_number, amount, changed_date, remark) VALUES($1, $2, $3, $4)", + [receiver_account_number, amount, changed_date, remark] + ); + await client.query("COMMIT"); + console.log("Transaction completed!"); + } catch (error) { + console.error("Error seeding database:", error); + } finally { + await client.end(); + } +} + +seedDatabase(client); diff --git a/Week3/Ex2transactions/transactions-create-tables.js b/Week3/Ex2transactions/transactions-create-tables.js new file mode 100644 index 000000000..d49b4d05f --- /dev/null +++ b/Week3/Ex2transactions/transactions-create-tables.js @@ -0,0 +1,44 @@ +import { Client } from "pg"; + +// Database connection configuration +const config = { + host: "localhost", + user: "hyfuser", + password: "hyfpassword", + database: "transactions_week3", + port: 5432, +}; + +const client = new Client(config); + +async function createTables(client) { + const CREATE_ACCOUNT_TABLE = ` + CREATE TABLE IF NOT EXISTS account ( + account_number SMALLINT PRIMARY KEY, + balance INTEGER + )`; + + const CREATE_ACCOUNT_CHANGES_TABLE = ` + CREATE TABLE IF NOT EXISTS account_changes ( + change_number SERIAL PRIMARY KEY, + account_number SMALLINT, + amount INTEGER, + changed_date DATE, + remark VARCHAR(100), + CONSTRAINT fk_account_number FOREIGN KEY (account_number) REFERENCES account(account_number) ON DELETE CASCADE + )`; + try { + await client.connect(); + console.log("Connected to PostgreSQL database!"); + + // Create tables + await client.query(CREATE_ACCOUNT_TABLE); + await client.query(CREATE_ACCOUNT_CHANGES_TABLE); + } catch (error) { + console.error("Error creating tables:", error); + } finally { + await client.end(); + } +} + +createTables(client); diff --git a/Week3/Ex2transactions/transactions-insert-values.js b/Week3/Ex2transactions/transactions-insert-values.js new file mode 100644 index 000000000..fab7e3e85 --- /dev/null +++ b/Week3/Ex2transactions/transactions-insert-values.js @@ -0,0 +1,39 @@ +import { accounts, account_changes } from "./data.js"; +import { Client } from "pg"; +const config = { + host: "localhost", + user: "hyfuser", + password: "hyfpassword", + database: "transactions_week3", + port: 5432, +}; +const client = new Client(config); + +async function seedDatabase(client, accounts, account_changes) { + try { + await client.connect(); + console.log("Connected to PostgreSQL database!"); + + for (const account of accounts) { + const INSERT_ACCOUNT_QUERY = { + text: "INSERT INTO account(account_number, balance) VALUES($1, $2) ON CONFLICT (account_number) DO NOTHING", + values: Object.values(account), + }; + await client.query(INSERT_ACCOUNT_QUERY); + } + + for (const change of account_changes) { + const INSERT_CHANGE_QUERY = { + text: `INSERT INTO account_changes(account_number, amount, changed_date, remark) VALUES($1, $2, $3, $4)`, + values: Object.values(change), + }; + await client.query(INSERT_CHANGE_QUERY); + } + } catch (error) { + console.error("Error seeding database:", error); + } finally { + await client.end(); + } +} + +seedDatabase(client, accounts, account_changes); diff --git a/Week3/Ex3sqlinjection/Ex3sqlinjection.js b/Week3/Ex3sqlinjection/Ex3sqlinjection.js new file mode 100644 index 000000000..a3f28fd85 --- /dev/null +++ b/Week3/Ex3sqlinjection/Ex3sqlinjection.js @@ -0,0 +1,75 @@ +import { Client } from "pg"; +const config = { + host: "localhost", + user: "hyfuser", + password: "hyfpassword", + database: "world", + port: 5432, +}; +const conn = new Client(config); +await conn.connect(); +console.log("Connected to PostgreSQL database!"); + +function getPopulation(Country, name, code, cb) { + // assuming that connection to the database is established and stored as conn + conn.query( + `SELECT Population FROM ${Country} WHERE Name = '${name}' and code = '${code}'`, + function (err, result) { + if (err) cb(err); + if (result.length == 0) cb(new Error("Not found")); + cb(null, result.rows[0].population); + } + ); +} + +getPopulation("country", "Netherlands", "NLD", function (err, population) { + if (err) { + console.error("Error:", err); + } else { + console.log("Netherland's population (the regular query):", population); + } +}); + +// The resulting query becomes: SELECT Population FROM country WHERE Name = = '' OR 1=1 --' and code = '' OR 1=1 --' +// Since 1=1 is always true, this condition matches all rows in the country table. However, because the code only retrieves the first row from the result set, it returns only the population of the first country in the table. +getPopulation( + "country", + "Netherlands", + "' OR 1=1 --", + function (err, population) { + if (err) { + console.error("Error:", err); + } else { + console.log("the hacker's query 1:", population); + } + } +); + +// The user's input is treated as data only and not executable code here. However, it is not possible to demonstrate the advantages of this approach, as the function getPopulation can only handle results that contain an array of objects with the population property. Although the SQL code is executed and yields the desired result, it is not possible to extract it without modifying the initial function, so it serves as an SQL injection prevention of some sort. +function getPopulationSQLinjectProof(Country, name, code, cb) { + const allowedTables = new Set(["country"]); + const table = String(Country).toLowerCase(); + if (!allowedTables.has(table)) return cb(new Error("Invalid table")); + + const sql = `SELECT population FROM ${table} WHERE Name = $1 AND Code = $2`; + + conn.query(sql, [name, code], function (err, result) { + if (err) return cb(err); + if (!result) return cb(new Error("Not found")); + cb(null, result.rows); + }); +} + +getPopulationSQLinjectProof( + "country", + "' OR 1=1 --", + "' OR 1=1 --", + function (err, population) { + if (err) { + console.error("Error:", err); + } else { + console.log("the hacker's query 2:", population); + } + conn.end(); + } +); diff --git a/Week3/MAKEME.md b/Week3/MAKEME.md index 6e5f12e77..6e0b0bf94 100644 --- a/Week3/MAKEME.md +++ b/Week3/MAKEME.md @@ -49,9 +49,22 @@ Please help the manger by using the knowledge of database normal forms. Save all answers in a text file / MD file. 1. What columns violate 1NF? + +food_code, food_description + 2. What entities do you recognize that could be extracted? + +member, dinner, venue, food + 3. Name all the tables and columns that would make a 3NF compliant solution. +- member: member_id, member_name, member_address +- dinner: dinner_id, dinner_date, venue_code +- dinner_member: dinner_id, member_id +- venue: venue_code, venue_description +- dinner_food: dinner_id, food_code +- food: food_code, food_description + ``` +-----------+---------------+----------------+-----------+-------------+------------+-------------------+-----------+------------------+ | member_id | member_name | member_address | dinner_id | dinner_date | venue_code | venue_description | food_code | food_description | diff --git a/Week3/homework/mongodb/index.js b/Week3/homework/mongodb/index.js index 41ee8b618..c4917df56 100644 --- a/Week3/homework/mongodb/index.js +++ b/Week3/homework/mongodb/index.js @@ -1,85 +1,119 @@ const { MongoClient, ServerApiVersion } = require("mongodb"); - +const { config: configDotenv } = require("dotenv"); +configDotenv({ silent: true }); const { seedDatabase } = require("./seedDatabase.js"); +let req; +let res; async function createEpisodeExercise(client) { - /** - * We forgot to add the last episode of season 9. It has this information: - * - * episode: S09E13 - * title: MOUNTAIN HIDE-AWAY - * elements: ["CIRRUS", "CLOUDS", "CONIFER", "DECIDIOUS", "GRASS", "MOUNTAIN", "MOUNTAINS", "RIVER", "SNOWY_MOUNTAIN", "TREE", "TREES"] - */ - - // Write code that will add this to the collection! + const bobRossCollection = await client + .db("databaseWeek3") + .collection("bob_ross_episodes"); + + req = { + episode: "S09E13", + title: "MOUNTAIN HIDE-AWAY", + elements: [ + "CIRRUS", + "CLOUDS", + "CONIFER", + "DECIDIOUS", + "GRASS", + "MOUNTAIN", + "MOUNTAINS", + "RIVER", + "SNOWY_MOUNTAIN", + "TREE", + "TREES", + ], + }; + + res = await bobRossCollection.insertOne(req); console.log( - `Created season 9 episode 13 and the document got the id ${"TODO: fill in variable here"}` + `Created season 9 episode 13 and the document got the id ${res.insertedId}` ); } async function findEpisodesExercises(client) { - /** - * Complete the following exercises. - * The comments indicate what to do and what the result should be! - */ - + const bobRossCollection = await client + .db("databaseWeek3") + .collection("bob_ross_episodes"); + req = { episode: "S02E02" }; + res = await bobRossCollection.findOne(req); // Find the title of episode 2 in season 2 [Should be: WINTER SUN] + console.log(`The title of episode 2 in season 2 is ${res.title}`); - console.log( - `The title of episode 2 in season 2 is ${"TODO: fill in variable here"}` - ); - + req = { title: "BLACK RIVER" }; + res = await bobRossCollection.findOne(req); // Find the season and episode number of the episode called "BLACK RIVER" [Should be: S02E06] - console.log( - `The season and episode number of the "BLACK RIVER" episode is ${"TODO: fill in variable here"}` + `The season and episode number of the "BLACK RIVER" episode is ${res.episode}` ); + req = { elements: "CLIFF" }; + res = await bobRossCollection.find(req).toArray(); // Find all of the episode titles where Bob Ross painted a CLIFF [Should be: NIGHT LIGHT, EVENING SEASCAPE, SURF'S UP, CLIFFSIDE, BY THE SEA, DEEP WILDERNESS HOME, CRIMSON TIDE, GRACEFUL WATERFALL] - console.log( - `The episodes that Bob Ross painted a CLIFF are ${"TODO: fill in variable here"}` + `The episodes that Bob Ross painted a CLIFF are ${res + .map((item) => item.title) + .join(", ")}` ); + req = { elements: { $all: ["CLIFF", "LIGHTHOUSE"] } }; + res = await bobRossCollection.find(req).toArray(); // Find all of the episode titles where Bob Ross painted a CLIFF and a LIGHTHOUSE [Should be: NIGHT LIGHT] - console.log( - `The episodes that Bob Ross painted a CLIFF and a LIGHTHOUSE are ${"TODO: fill in variable here"}` + `The episodes that Bob Ross painted a CLIFF and a LIGHTHOUSE are ${res + .map((item) => item.title) + .join(", ")}` ); } async function updateEpisodeExercises(client) { - /** - * There are some problems in the initial data that was filled in. - * Let's use update functions to update this information. - * - * Note: do NOT change the data.json file - */ - - // Episode 13 in season 30 should be called BLUE RIDGE FALLS, yet it is called BLUE RIDGE FALLERS now. Fix that + const bobRossCollection = await client + .db("databaseWeek3") + .collection("bob_ross_episodes"); + res = await bobRossCollection.updateOne( + { episode: "S30E13" }, + { $set: { title: "BLUE RIDGE FALLS" } } + ); + // Episode 13 in season 30 should be called BLUE RIDGE FALLS. console.log( - `Ran a command to update episode 13 in season 30 and it updated ${"TODO: fill in variable here"} episodes` + `Ran a command to update episode 13 in season 30 and it updated ${res.modifiedCount} episodes` ); - // Unfortunately we made a mistake in the arrays and the element type called 'BUSHES' should actually be 'BUSH' as sometimes only one bush was painted. - // Update all of the documents in the collection that have `BUSHES` in the elements array to now have `BUSH` + res = await bobRossCollection.updateMany({ elements: "BUSHES" }, [ + { + $set: { + elements: { + $map: { + input: "$elements", + as: "el", + in: { + $cond: [{ $eq: ["$$el", "BUSHES"] }, "BUSH", "$$el"], + }, + }, + }, + }, + }, + ]); // It should update 120 episodes! - console.log( - `Ran a command to update all the BUSHES to BUSH and it updated ${"TODO: fill in variable here"} episodes` + `Ran a command to update all the BUSHES to BUSH and it updated ${res.modifiedCount} episodes` ); } async function deleteEpisodeExercise(client) { - /** - * It seems an errand episode has gotten into our data. - * This is episode 14 in season 31. Please remove it and verify that it has been removed! - */ - + const bobRossCollection = await client + .db("databaseWeek3") + .collection("bob_ross_episodes"); + req = { episode: "S31E14" }; + res = await bobRossCollection.deleteMany(req); + // This is episode 14 in season 31. Please remove it and verify that it has been removed! console.log( - `Ran a command to delete episode and it deleted ${"TODO: fill in variable here"} episodes` + `Ran a command to delete episode and it deleted ${res.deletedCount} episodes` ); } @@ -133,5 +167,4 @@ The episodes that Bob Ross painted a CLIFF and a LIGHTHOUSE are NIGHT LIGHT Ran a command to update episode 13 in season 30 and it updated 1 episodes Ran a command to update all the BUSHES to BUSH and it updated 120 episodes Ran a command to delete episode and it deleted 1 episodes - */ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..5759ad78c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,172 @@ +{ + "name": "databases-Cohort53", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "dotenv": "^17.2.2", + "mongodb": "^6.19.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", + "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/mongodb": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.19.0.tgz", + "integrity": "sha512-H3GtYujOJdeKIMLKBT9PwlDhGrQfplABNF1G904w6r5ZXKWyv77aB0X9B+rhmaAwjtllHzaEkvi9mkGVZxs2Bw==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..85fe40b7a --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "dotenv": "^17.2.2", + "mongodb": "^6.19.0" + } +}