diff --git a/Week2/prep/Database ER diagram (crow's foot).png b/Week2/prep/Database ER diagram (crow's foot).png new file mode 100644 index 000000000..579c05848 Binary files /dev/null and b/Week2/prep/Database ER diagram (crow's foot).png differ diff --git a/Week2/prep/Lucid connection.png b/Week2/prep/Lucid connection.png new file mode 100644 index 000000000..8321abffd Binary files /dev/null and b/Week2/prep/Lucid connection.png differ diff --git a/Week2/prep/w1-prep-exe.sql b/Week2/prep/w1-prep-exe.sql new file mode 100644 index 000000000..584bf10ec --- /dev/null +++ b/Week2/prep/w1-prep-exe.sql @@ -0,0 +1,111 @@ +-- Очищаем существующие таблицы (для повторного запуска) -- Drop existing tables (for re-running the script) +DROP TABLE IF EXISTS cuisines CASCADE; +DROP TABLE IF EXISTS main_ingredients CASCADE; +DROP TABLE IF EXISTS ingredients CASCADE; +DROP TABLE IF EXISTS categories CASCADE; +DROP TABLE IF EXISTS cooking_methods CASCADE; +DROP TABLE IF EXISTS recipes CASCADE; +DROP TABLE IF EXISTS recipe_categories CASCADE; +DROP TABLE IF EXISTS recipe_ingredients CASCADE; +DROP TABLE IF EXISTS recipe_methods CASCADE; +DROP TABLE IF EXISTS recipe_ingredient_amounts CASCADE; + + + +--------------------------------------------------------- +-- 1. Таблица кухонь (Italian, Chinese, Japanese…) -- Table of Cuisines (Italian, Chinese, Japanese…) +--------------------------------------------------------- +CREATE TABLE cuisines ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE +); + +--------------------------------------------------------- +-- 2. Таблица основных ингредиентов (мясо, рыба, овощи) -- Table of Main Ingredients (meat, fish, vegetables) +--------------------------------------------------------- +CREATE TABLE main_ingredients ( + id SERIAL PRIMARY KEY, + name VARCHAR(150) NOT NULL UNIQUE +); + +--------------------------------------------------------- +-- 3. Таблица всех возможных ингредиентов (морковь, масло, соль) -- Table of All Possible Ingredients (carrot, oil, salt) +--------------------------------------------------------- +CREATE TABLE ingredients ( + id SERIAL PRIMARY KEY, + name VARCHAR(150) NOT NULL UNIQUE +); + +--------------------------------------------------------- +-- 4. Таблица категорий (суп, салат, десерт, завтрак) -- Table of Categories (soup, salad, dessert, breakfast) +--------------------------------------------------------- +CREATE TABLE categories ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE +); + +--------------------------------------------------------- +-- 5. Таблица методов приготовления (жарка, запекание, варка) -- Table of Cooking Methods (frying, baking, boiling) +--------------------------------------------------------- +CREATE TABLE cooking_methods ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE +); + +--------------------------------------------------------- +-- 6. Главная таблица рецептов -- Main Recipes Table +--------------------------------------------------------- +CREATE TABLE recipes ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + cuisine_id INT, + main_ingredient_id INT, + FOREIGN KEY (cuisine_id) REFERENCES cuisines(id), + FOREIGN KEY (main_ingredient_id) REFERENCES main_ingredients(id) +); + +--------------------------------------------------------- +-- 7. Связь рецепт ↔ категории (many-to-many) -- Recipe ↔ Categories Relationship (many-to-many) +--------------------------------------------------------- +CREATE TABLE recipe_categories ( + recipe_id INT NOT NULL, + category_id INT NOT NULL, + PRIMARY KEY (recipe_id, category_id), + FOREIGN KEY (recipe_id) REFERENCES recipes(id), + FOREIGN KEY (category_id) REFERENCES categories(id) +); + +--------------------------------------------------------- +-- 8. Связь рецепт ↔ ингредиенты (many-to-many) -- Recipe ↔ Ingredients Relationship (many-to-many) +--------------------------------------------------------- +CREATE TABLE recipe_ingredients ( + recipe_id INT NOT NULL, + ingredient_id INT NOT NULL, + amount VARCHAR(50), -- например "200 г", "1 ст.л." + PRIMARY KEY (recipe_id, ingredient_id), + FOREIGN KEY (recipe_id) REFERENCES recipes(id), + FOREIGN KEY (ingredient_id) REFERENCES ingredients(id) +); + +--------------------------------------------------------- +-- 9. Связь рецепт ↔ методы приготовления (many-to-many) -- Recipe ↔ Cooking Methods Relationship (many-to-many) +--------------------------------------------------------- +CREATE TABLE recipe_methods ( + recipe_id INT NOT NULL, + method_id INT NOT NULL, + PRIMARY KEY (recipe_id, method_id), + FOREIGN KEY (recipe_id) REFERENCES recipes(id), + FOREIGN KEY (method_id) REFERENCES cooking_methods(id) +); + +--------------------------------------------------------- +-- 10. кол-во ингредиентов в рецепте -- Quantity of Ingredients in Recipe +--------------------------------------------------------- +CREATE TABLE recipe_ingredient_amounts ( + recipe_id INT NOT NULL, + ingredient_id INT NOT NULL, + amount VARCHAR(50) NOT NULL, -- например "200 г", "1 ст.л." + PRIMARY KEY (recipe_id, ingredient_id), + FOREIGN KEY (recipe_id) REFERENCES recipes(id), + FOREIGN KEY (ingredient_id) REFERENCES ingredients(id) +); \ No newline at end of file diff --git a/Week2/prep/w2-prep-insert-mok-data.sql b/Week2/prep/w2-prep-insert-mok-data.sql new file mode 100644 index 000000000..05a9b7d77 --- /dev/null +++ b/Week2/prep/w2-prep-insert-mok-data.sql @@ -0,0 +1,98 @@ +-- ========================================== +-- Mock data inserts for Week 2 Prep Exercise +-- ========================================== + +-- 1. Cuisines +INSERT INTO cuisines (name) VALUES +('Japanese') +ON CONFLICT (name) DO NOTHING; + +-- 2. Main ingredients +INSERT INTO main_ingredients (name) VALUES +('Cheese'), +('Vegetables'), +('Pasta'), +('Eggs') +ON CONFLICT (name) DO NOTHING; + +-- 3. Categories +INSERT INTO categories (name) VALUES +('Cake'), +('No-Bake'), +('Vegetarian'), +('Vegan'), +('Gluten-Free'), +('Japanese') +ON CONFLICT (name) DO NOTHING; + +-- 4. Ingredients +INSERT INTO ingredients (name) VALUES +('Cheese'), +('Vegetables'), +('Pasta'), +('Eggs'), +('Condensed milk'), +('Cream Cheese'), +('Lemon Juice'), +('Pie Crust'), +('Cherry Jam'), +('Brussels Sprouts'), +('Sesame seeds'), +('Pepper'), +('Salt'), +('Olive oil'), +('Macaroni'), +('Butter'), +('Flour'), +('Milk'), +('Shredded Cheddar cheese'), +('Soy sauce'), +('Sugar') +ON CONFLICT (name) DO NOTHING; + +-- 5. Recipes +INSERT INTO recipes (name, cuisine_id, main_ingredient_id) VALUES +('No-Bake Cheesecake', NULL, 1), +('Roasted Brussels Sprouts', NULL, 2), +('Mac & Cheese', NULL, 3), +('Tamagoyaki Japanese Omelette', 1, 4) +ON CONFLICT DO NOTHING; + +-- 6. Recipe ↔ Categories +INSERT INTO recipe_categories (recipe_id, category_id) VALUES +(1, 1), (1, 2), (1, 3), +(2, 4), (2, 5), +(3, 3), +(4, 3), (4, 6) +ON CONFLICT DO NOTHING; + +-- 7. Recipe ↔ Ingredients +INSERT INTO recipe_ingredients (recipe_id, ingredient_id, amount) VALUES +-- No-Bake Cheesecake +(1, 5, '200 ml'), +(1, 6, '250 g'), +(1, 7, '1 tbsp'), +(1, 8, '1 crust'), +(1, 9, '2 tbsp'), +-- Roasted Brussels Sprouts +(2, 10, '500 g'), +(2, 7, '1 tbsp'), +(2, 11, '1 tsp'), +(2, 12, '1 tsp'), +(2, 13, '1 tsp'), +(2, 14, '2 tbsp'), +-- Mac & Cheese +(3, 15, '200 g'), +(3, 16, '50 g'), +(3, 17, '2 tbsp'), +(3, 13, '1 tsp'), +(3, 12, '1 tsp'), +(3, 18, '200 ml'), +(3, 19, '150 g'), +-- Tamagoyaki Japanese Omelette +(4, 4, '4'), +(4, 20, '1 tbsp'), +(4, 21, '1 tsp'), +(4, 13, '1 tsp'), +(4, 14, '1 tbsp') +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/Week2/prep/w2-request.sql b/Week2/prep/w2-request.sql new file mode 100644 index 000000000..6e076e107 --- /dev/null +++ b/Week2/prep/w2-request.sql @@ -0,0 +1,24 @@ +-- 1. All vegetarian recipes with potatoes (но картошки нет в данных) +SELECT r.name AS recipe_name +FROM recipes r +JOIN recipe_categories rc ON r.id = rc.recipe_id +JOIN categories c ON rc.category_id = c.id +WHERE c.name = 'Vegetarian'; + +-- 2. All cakes that do not need baking +SELECT r.name AS recipe_name +FROM recipes r +JOIN recipe_categories rc1 ON r.id = rc1.recipe_id +JOIN categories c1 ON rc1.category_id = c1.id +JOIN recipe_categories rc2 ON r.id = rc2.recipe_id +JOIN categories c2 ON rc2.category_id = c2.id +WHERE c1.name = 'Cake' AND c2.name = 'No-Bake'; + +-- 3. All vegan and Japanese recipes +SELECT r.name AS recipe_name +FROM recipes r +JOIN recipe_categories rc1 ON r.id = rc1.recipe_id +JOIN categories c1 ON rc1.category_id = c1.id +JOIN recipe_categories rc2 ON r.id = rc2.recipe_id +JOIN categories c2 ON rc2.category_id = c2.id +WHERE c1.name = 'Vegan' AND c2.name = 'Japanese'; \ No newline at end of file diff --git a/Week4/homework/task1/README.md b/Week4/homework/task1/README.md new file mode 100644 index 000000000..cc90d8d8b --- /dev/null +++ b/Week4/homework/task1/README.md @@ -0,0 +1,11 @@ +exercise 3.1 - folder task 1 +exercise 3.1 (part 1) import - file import_csv.js +exercises 3.1 (parts 2 and 3)- file aggregations.js + + +exercises 3.2 - folder taks 2 + + +## Why two containers? +1. Exercise 1: Simple MongoDB on port 27018 +2. Exercise 2: MongoDB with a replica set on port 27017 (required for transactions) \ No newline at end of file diff --git a/Week4/homework/task1/package-lock.json b/Week4/homework/task1/package-lock.json new file mode 100644 index 000000000..fa2232838 --- /dev/null +++ b/Week4/homework/task1/package-lock.json @@ -0,0 +1,173 @@ +{ + "name": "ex1-aggregation", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ex1-aggregation", + "dependencies": { + "csv-parser": "^3.0.0", + "mongodb": "^6.0.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.0.tgz", + "integrity": "sha512-ZHzx7Z3rdlWL1mECydvpryWN/ETXJiCxdgQKTAH+djzIPe77HdnSizKBDi1TVDXZjXyOj2IqEG/vPw71ULF06w==", + "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/csv-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", + "integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==", + "license": "MIT", + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, + "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.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "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/Week4/homework/task1/package.json b/Week4/homework/task1/package.json new file mode 100644 index 000000000..c8ffeb026 --- /dev/null +++ b/Week4/homework/task1/package.json @@ -0,0 +1,7 @@ +{ + "name": "ex1-aggregation", + "dependencies": { + "mongodb": "^6.0.0", + "csv-parser": "^3.0.0" + } +} \ No newline at end of file diff --git a/Week4/homework/task1/scripts/aggregations.js b/Week4/homework/task1/scripts/aggregations.js new file mode 100644 index 000000000..c579a1e03 --- /dev/null +++ b/Week4/homework/task1/scripts/aggregations.js @@ -0,0 +1,72 @@ +const { MongoClient } = require('mongodb'); + +// Функция 1: Население по годам для страны --- exercise asks for country +async function getPopulationByYear(country) { + const client = new MongoClient('mongodb://localhost:27018'); + + try { + await client.connect(); + const collection = client.db('population_db').collection('population'); + + const result = await collection.aggregate([ + { $match: { + Country: country, + Year: { $in: [1950, 1960, 1970, 1980, 1990, 2000, 2010, 2020, 2022] } + }}, + { $group: { + _id: "$Year", + countPopulation: { $sum: { $add: ["$M", "$F"] } } + }}, + { $sort: { _id: 1 } } + ]).toArray(); + + return result; + } finally { + await client.close(); + } +} + +// Функция 2: Данные по континентам --- exercise asks for continents but data uses regions +async function getContinentData(year, age) { + const client = new MongoClient('mongodb://localhost:27018'); + + try { + await client.connect(); + const collection = client.db('population_db').collection('population'); + + const result = await collection.aggregate([ + { $match: { + Year: year, + Age: age, + Country: { + $in: [ + "AFRICA", "ASIA", "EUROPE", + "LATIN AMERICA AND THE CARIBBEAN", + "NORTHERN AMERICA", "OCEANIA" + ] + } + }}, + { $addFields: { + TotalPopulation: { $add: ["$M", "$F"] } + }}, + { $sort: { Country: 1 } } + ]).toArray(); + + return result; + } finally { + await client.close(); + } +} + +// Тестирование обеих функций --- test if needed +async function main() { + console.log('=== Population by year for Netherlands ==='); + const netherlandsData = await getPopulationByYear("Netherlands"); + console.log(JSON.stringify(netherlandsData, null, 2)); + + console.log('\n=== Continent data for 2020, age 100+ ==='); + const continentData = await getContinentData(2020, "100+"); + console.log(JSON.stringify(continentData, null, 2)); +} + +main().catch(console.error); \ No newline at end of file diff --git a/Week4/homework/task1/scripts/import_csv.js b/Week4/homework/task1/scripts/import_csv.js new file mode 100644 index 000000000..20dde4858 --- /dev/null +++ b/Week4/homework/task1/scripts/import_csv.js @@ -0,0 +1,35 @@ +const { MongoClient } = require('mongodb'); +const fs = require('fs'); +const csv = require('csv-parser'); + +async function importData() { + const client = new MongoClient('mongodb://localhost:27018'); + + try { + await client.connect(); + const db = client.db('population_db'); + const collection = db.collection('population'); + + const documents = []; + fs.createReadStream('../../ex1-aggregation/population_pyramid_1950-2022.csv') + .pipe(csv()) + .on('data', (row) => { + documents.push({ + Country: row.Country, + Year: parseInt(row.Year), + Age: row.Age, + M: parseInt(row.M), + F: parseInt(row.F) + }); + }) + .on('end', async () => { + await collection.insertMany(documents); + console.log(`Imported ${documents.length} documents`); + await client.close(); + }); + } catch (error) { + console.error(error); + } +} + +importData(); \ No newline at end of file diff --git a/Week4/homework/task2/index.js b/Week4/homework/task2/index.js new file mode 100644 index 000000000..87f8b81a4 --- /dev/null +++ b/Week4/homework/task2/index.js @@ -0,0 +1,27 @@ +import { setupDatabase } from './setup.js'; +import { transferMoney } from './transfer.js'; + +async function main() { + // 1. Настраиваем базу данных --- setup the database + await setupDatabase(); + + // 2. Выполняем перевод --- perform a transfer + await transferMoney(101, 102, 1000, "Test transfer"); + + // 3. Проверяем результат --- check the result + const { MongoClient } = await import('mongodb'); + const client = new MongoClient('mongodb://localhost:27017/?replicaSet=rs0&directConnection=true'); + + await client.connect(); + const collection = client.db('bank_db').collection('accounts'); + + const account101 = await collection.findOne({ account_number: 101 }); + const account102 = await collection.findOne({ account_number: 102 }); + + console.log('\nAccount 101 balance:', account101.balance); + console.log('Account 102 balance:', account102.balance); + + await client.close(); +} + +main().catch(console.error); \ No newline at end of file diff --git a/Week4/homework/task2/package-lock.json b/Week4/homework/task2/package-lock.json new file mode 100644 index 000000000..35bd854e1 --- /dev/null +++ b/Week4/homework/task2/package-lock.json @@ -0,0 +1,160 @@ +{ + "name": "ex2-transactions", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ex2-transactions", + "dependencies": { + "mongodb": "^6.0.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.0.tgz", + "integrity": "sha512-ZHzx7Z3rdlWL1mECydvpryWN/ETXJiCxdgQKTAH+djzIPe77HdnSizKBDi1TVDXZjXyOj2IqEG/vPw71ULF06w==", + "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/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.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "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/Week4/homework/task2/package.json b/Week4/homework/task2/package.json new file mode 100644 index 000000000..1d6bb922a --- /dev/null +++ b/Week4/homework/task2/package.json @@ -0,0 +1,7 @@ +{ + "name": "ex2-transactions", + "type": "module", + "dependencies": { + "mongodb": "^6.0.0" + } +} \ No newline at end of file diff --git a/Week4/homework/task2/setup.js b/Week4/homework/task2/setup.js new file mode 100644 index 000000000..480b2cfbe --- /dev/null +++ b/Week4/homework/task2/setup.js @@ -0,0 +1,61 @@ +import { MongoClient } from 'mongodb'; + +export async function setupDatabase() { + const client = new MongoClient('mongodb://localhost:27017/?replicaSet=rs0&directConnection=true'); + + try { + await client.connect(); + const collection = client.db('bank_db').collection('accounts'); + + // Очищаем коллекцию --- clear the collection + await collection.deleteMany({}); + + // Создаем тестовые счета --- create test accounts + const accounts = [ + { + account_number: 101, + balance: 10000, + account_changes: [ + { + change_number: 1, + amount: 10000, + changed_date: new Date(), + remark: "Initial deposit" + } + ] + }, + { + account_number: 102, + balance: 5000, + account_changes: [ + { + change_number: 1, + amount: 5000, + changed_date: new Date(), + remark: "Initial deposit" + } + ] + }, + { + account_number: 103, + balance: 2000, + account_changes: [ + { + change_number: 1, + amount: 2000, + changed_date: new Date(), + remark: "Initial deposit" + } + ] + } + ]; + + await collection.insertMany(accounts); + console.log('Database setup completed'); + + } finally { + await client.close(); + } +} + +// setupDatabase(); \ No newline at end of file diff --git a/Week4/homework/task2/transfer.js b/Week4/homework/task2/transfer.js new file mode 100644 index 000000000..5c4892fe5 --- /dev/null +++ b/Week4/homework/task2/transfer.js @@ -0,0 +1,80 @@ +import { MongoClient } from 'mongodb'; + +export async function transferMoney(fromAccount, toAccount, amount, remark) { + const client = new MongoClient('mongodb://localhost:27017/?replicaSet=rs0&directConnection=true'); + + try { + await client.connect(); + const collection = client.db('bank_db').collection('accounts'); + + // Начинаем транзакцию --- start a transaction + const session = client.startSession(); + + try { + const result = await session.withTransaction(async () => { + // 1. Проверяем существование счетов --- check account existence + const fromAcc = await collection.findOne( + { account_number: fromAccount }, + { session } + ); + const toAcc = await collection.findOne( + { account_number: toAccount }, + { session } + ); + + if (!fromAcc || !toAcc) { + throw new Error('Account not found'); + } + + // 2. Проверяем достаточно ли денег на счете отправителя --- check sufficient funds + if (fromAcc.balance < amount) { + throw new Error('Insufficient funds'); + } + + // 3. Обновляем балансы и добавляем записи об изменениях --- update balances and add change records + const lastChangeFrom = fromAcc.account_changes.length; + const lastChangeTo = toAcc.account_changes.length; + + await collection.updateOne( + { account_number: fromAccount }, + { + $inc: { balance: -amount }, + $push: { + account_changes: { + change_number: lastChangeFrom + 1, + amount: -amount, + changed_date: new Date(), + remark: remark + } + } + }, + { session } + ); + + await collection.updateOne( + { account_number: toAccount }, + { + $inc: { balance: amount }, + $push: { + account_changes: { + change_number: lastChangeTo + 1, + amount: amount, + changed_date: new Date(), + remark: remark + } + } + }, + { session } + ); + + console.log(`Transfer successful: ${amount} from ${fromAccount} to ${toAccount}`); + }); + + } finally { + await session.endSession(); + } + + } finally { + await client.close(); + } +} \ No newline at end of file