Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Week3/Ex2transactions/data.js
Original file line number Diff line number Diff line change
@@ -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 },
];
Comment on lines +1 to +7

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Same suggestion as week 2, better to use JSON file for pure data. You don't have to change your code but it is better to know.


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",
},
];
48 changes: 48 additions & 0 deletions Week3/Ex2transactions/transaction.js
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +16 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: in real life situation it is better to check whether the account exists and the balance is enough before doing actual transactions.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree once again here. Limitations should be enforced in the DB, for example, allowing the balance only a positive integer. Then, we can rely on the RDBMS to throw an error if some of the transactional statements can not be executed and abort the whole transaction. No check inside the code is needed.

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);
Comment on lines +41 to +42

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: although client.end() will also roll back the changes in case of errors, I will recommend to add await client.query("ROLLBACK") in the catch block to clearly state a rollback operation will be done. It has several advantages:

  • When I read the code, I know in case of errors the changes will be rolled back instead of guessing whether rollback will be done.
  • client.end() will roll back changes in PostgreSQL, but not necessarily in other types of databases or in other versions. We shouldn't rely on this kind of implicit operations.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to save a couple of code lines and rely on the behavior of the Postgres database management system, which should be known to the developer's team. On top of that, I do not expect it to be changed to let's say, MySQL, which will require the rewrite of many functions, where a transaction block is just one of them.
Although I understand that you have more experience, your argument is not convincing for now. This approach creates safeguards twice: in the RDBMS and in the app's code.

} finally {
await client.end();
}
}

seedDatabase(client);
44 changes: 44 additions & 0 deletions Week3/Ex2transactions/transactions-create-tables.js
Original file line number Diff line number Diff line change
@@ -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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the bank is really small using SMALLINT as PK makes sense. It is not common to use a small type for PK.

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);
39 changes: 39 additions & 0 deletions Week3/Ex2transactions/transactions-insert-values.js
Original file line number Diff line number Diff line change
@@ -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);
75 changes: 75 additions & 0 deletions Week3/Ex3sqlinjection/Ex3sqlinjection.js
Original file line number Diff line number Diff line change
@@ -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();
}
);
13 changes: 13 additions & 0 deletions Week3/MAKEME.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Loading