diff --git a/task-1/books_library.db b/task-1/books_library.db index 88b0418..40e6fe2 100644 Binary files a/task-1/books_library.db and b/task-1/books_library.db differ diff --git a/task-1/queries.sql b/task-1/queries.sql index fecf7ad..77d187b 100644 --- a/task-1/queries.sql +++ b/task-1/queries.sql @@ -1,27 +1,60 @@ ----- Queries +-- Queries --- **Question 1** — List the title and published year of every book in the `'Science Fiction'` genre, ordered by published year (oldest first). +-- **Question 1** — List the title and published year of every book in the 'Science Fiction' genre, ordered by published year (oldest first). +SELECT title, published_year +FROM books +WHERE genre = 'Science Fiction' +ORDER BY published_year ASC; -- **Question 2** — Show every book published before 1950. Display the title and year only. - --- **Question 3** — Show every book in the database along with its author's full name. Combine `first_name` and `last_name` into a single column called `author`. _(Hint: you will need a JOIN.)_ - --- **Question 4** — List all books written by Stephen King. Show the title and published year, ordered by year. _(Hint: JOIN the two tables and filter on the author's name.)_ +SELECT title, published_year +FROM books +WHERE published_year < 1950; + +-- **Question 3** — Show every book in the database along with its author's full name. Combine `first_name` and `last_name` into a single column called `author`. (Hint: you will need a JOIN.) +SELECT b.title, b.published_year, a.first_name || ' ' || a.last_name AS author +FROM books b +JOIN authors a ON a.id = b.author_id; + +-- **Question 4** — List all books written by Stephen King. Show the title and published year, ordered by year. (Hint: JOIN the two tables and filter on the author's name.) +SELECT b.title, b.published_year +FROM books b +JOIN authors a ON a.id = b.author_id +WHERE a.first_name || ' ' || a.last_name = 'Stephen King' +ORDER BY b.published_year; -- **Question 5** — Add yourself as a new author. Use your real name, or make one up. Pick any nationality and birth year. +INSERT INTO authors (first_name, last_name, nationality, birth_year) +VALUES ('John', 'Doe', 'American', 1990); -- **Question 6** — Add one book for the author you just inserted. It can be a real book or a made-up one. +INSERT INTO books (title, published_year, genre, author_id) +VALUES ('My Amazing Book', 2024, 'Fiction', 25); + +-- **Question 7** — The genre for "The Dark Tower: The Gunslinger" was entered incorrectly as 'Fantasy'. It should be 'Horror'. Write an UPDATE to fix it, then verify the change with a SELECT. +UPDATE books +SET genre = 'Horror' +WHERE title = 'The Dark Tower: The Gunslinger'; --- **Question 7** — The genre for "The Dark Tower: The Gunslinger" was entered incorrectly as `'Fantasy'`. It should be `'Horror'`. Write an UPDATE to fix it, then verify the change with a SELECT. +SELECT title, genre +FROM books +WHERE title = 'The Dark Tower: The Gunslinger'; -- **Question 8** — Delete the book you added in Question 6. Make sure your query targets only that specific row. +DELETE FROM books +WHERE id = 101; --- - --- ### Bonus questions _(optional)_ - --- These cover topics slightly beyond the core material. Have a go if you finish early. +-- ### Bonus questions (optional) -- **Bonus A** — How many books are there per genre? Show the genre name and the count, ordered from most to fewest books. - --- **Bonus B** — Find any authors in the database who have no books at all. _(Hint: you will need a LEFT JOIN and check for NULL.) +SELECT genre, COUNT(*) AS count +FROM books +GROUP BY genre +ORDER BY count DESC; + +-- **Bonus B** — Find any authors in the database who have no books at all. (Hint: you will need a LEFT JOIN and check for NULL.) +SELECT a.id, a.first_name, a.last_name +FROM authors a +LEFT JOIN books b ON b.author_id = a.id +WHERE b.id IS NULL; \ No newline at end of file diff --git a/task-2/data/data.json b/task-2/data/data.json deleted file mode 100644 index 42a5b06..0000000 --- a/task-2/data/data.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "decks": [ - { - "id": 1, - "name": "JavaScript Basics", - "description": "Core JavaScript concepts" - }, - { - "id": 2, - "name": "SQL Fundamentals", - "description": "Writing database queries" - }, - { - "id": 3, - "name": "HTML & CSS", - "description": "Building web pages" - } - ], - "cards": [ - { - "id": 1, - "question": "What is a variable?", - "answer": "A named container for storing data values", - "learned": true, - "deckId": 1 - }, - { - "id": 2, - "question": "What does `const` mean?", - "answer": "Declares a variable that cannot be reassigned", - "learned": false, - "deckId": 1 - }, - { - "id": 3, - "question": "What is an array?", - "answer": "An ordered list of values", - "learned": true, - "deckId": 1 - }, - { - "id": 4, - "question": "What does `===` do?", - "answer": "Checks for strict equality (value AND type)", - "learned": false, - "deckId": 1 - }, - { - "id": 5, - "question": "What is SELECT used for?", - "answer": "Retrieving data from a database table", - "learned": false, - "deckId": 2 - }, - { - "id": 6, - "question": "What is a PRIMARY KEY?", - "answer": "A unique identifier for each row in a table", - "learned": false, - "deckId": 2 - }, - { - "id": 7, - "question": "What does WHERE do?", - "answer": "Filters rows in a query based on a condition", - "learned": true, - "deckId": 2 - }, - { - "id": 8, - "question": "What is a FOREIGN KEY?", - "answer": "A column that links to the primary key of another table", - "learned": false, - "deckId": 2 - }, - { - "id": 9, - "question": "What does a
do?", - "answer": "Creates a block-level container element", - "learned": false, - "deckId": 3 - }, - { - "id": 10, - "question": "What is the CSS box model?", - "answer": "Every element is a box with content, padding, border, and margin", - "learned": false, - "deckId": 3 - } - ] -} \ No newline at end of file diff --git a/task-2/data/flashcards.db b/task-2/data/flashcards.db new file mode 100644 index 0000000..8bd7991 Binary files /dev/null and b/task-2/data/flashcards.db differ diff --git a/task-2/package-lock.json b/task-2/package-lock.json new file mode 100644 index 0000000..f1e0eb0 --- /dev/null +++ b/task-2/package-lock.json @@ -0,0 +1,389 @@ +{ + "name": "flashcard-app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "flashcard-app", + "version": "1.0.0", + "dependencies": { + "better-sqlite3": "^11.8.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz", + "integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "license": "ISC" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "license": "ISC" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + } + } +} diff --git a/task-2/package.json b/task-2/package.json index 8fb2060..20ca4de 100644 --- a/task-2/package.json +++ b/task-2/package.json @@ -8,6 +8,6 @@ "start": "node app.js" }, "dependencies": { - "better-sqlite3": "^12.8.0" + "better-sqlite3": "^11.8.1" } } diff --git a/task-2/setup.sql b/task-2/setup.sql new file mode 100644 index 0000000..8ad3409 --- /dev/null +++ b/task-2/setup.sql @@ -0,0 +1,18 @@ +-- Create the flashcards database schema + +-- Decks table +CREATE TABLE IF NOT EXISTS decks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT +); + +-- Cards table +CREATE TABLE IF NOT EXISTS cards ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question TEXT NOT NULL, + answer TEXT NOT NULL, + learned INTEGER NOT NULL DEFAULT 0, + deck_id INTEGER NOT NULL, + FOREIGN KEY (deck_id) REFERENCES decks(id) +); diff --git a/task-2/src/database.js b/task-2/src/database.js index a737fcb..efc2fb8 100644 --- a/task-2/src/database.js +++ b/task-2/src/database.js @@ -20,24 +20,30 @@ const db = new Database(DB_FILE); // ---------------------------------------------------------------- export function getAllDecks() { - // TODO: return all rows from the decks table - throw new Error('Not implemented'); + return db.prepare('SELECT * FROM decks').all(); } export function getDeckById(id) { - // TODO: return the deck row with the given id, or null if not found - throw new Error('Not implemented'); + const aSingleDeck = db.prepare('SELECT * FROM decks WHERE id = ?').get(id); + if (aSingleDeck) { + return aSingleDeck; + } else { + return null; + } } export function addDeck(name, description) { - // TODO: insert a new deck and return the new row (including its id) - throw new Error('Not implemented'); + const insertResult = db.prepare('INSERT INTO decks (name, description) VALUES (?, ?)').run(name, description); + return { id: insertResult.lastInsertRowid, name, description }; } export function deleteDeck(deckId) { - // TODO: delete the deck with the given id - // return true if a row was deleted, false otherwise - throw new Error('Not implemented'); + const deleteResult = db.prepare('DELETE FROM decks WHERE id = ?').run(deckId); + if (deleteResult.changes > 0) { + return true; + } else { + return false; + } } // ---------------------------------------------------------------- @@ -45,23 +51,29 @@ export function deleteDeck(deckId) { // ---------------------------------------------------------------- export function getAllCardsForDeck(deckId) { - // TODO: return all card rows whose deckId matches - throw new Error('Not implemented'); + return db.prepare('SELECT id, question, answer, learned, deck_id AS deckId FROM cards WHERE deck_id = ?').all(deckId); } export function addCard(question, answer, deckId) { - // TODO: insert a new card and return the new row (including its id) - throw new Error('Not implemented'); + const insertResult = db.prepare('INSERT INTO cards (question, answer, deck_id) VALUES (?, ?, ?)').run(question, answer, deckId); + return { id: insertResult.lastInsertRowid, question, answer, learned: false, deckId }; } export function markCardLearned(cardId) { - // TODO: set learned = 1 for the card with the given id - // return the updated row, or null if not found - throw new Error('Not implemented'); + db.prepare('UPDATE cards SET learned = 1 WHERE id = ?').run(cardId); + const aSingleCard = db.prepare('SELECT id, question, answer, learned, deck_id AS deckId FROM cards WHERE id = ?').get(cardId); + if (aSingleCard) { + return aSingleCard; + } else { + return null; + } } export function deleteCard(cardId) { - // TODO: delete the card with the given id - // return true if a row was deleted, false otherwise - throw new Error('Not implemented'); + const deleteResult = db.prepare('DELETE FROM cards WHERE id = ?').run(cardId); + if (deleteResult.changes > 0) { + return true; + } else { + return false; + } } diff --git a/task-2/src/migrate.js b/task-2/src/migrate.js new file mode 100644 index 0000000..934b608 --- /dev/null +++ b/task-2/src/migrate.js @@ -0,0 +1,42 @@ +import Database from 'better-sqlite3'; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DB_FILE = join(__dirname, '../data/flashcards.db'); +const DATA_FILE = join(__dirname, '../data/data.json'); + +// Read the JSON file +const rawJsonData = readFileSync(DATA_FILE, 'utf-8'); +const parsedJsonData = JSON.parse(rawJsonData); + +// Open the database +const db = new Database(DB_FILE); + +// Prepare the SQL statements +const insertDeckStatement = db.prepare('INSERT INTO decks (id, name, description) VALUES (?, ?, ?)'); +const insertCardStatement = db.prepare('INSERT INTO cards (id, question, answer, learned, deck_id) VALUES (?, ?, ?, ?, ?)'); + +// Insert all decks from JSON into database +const allDecksFromJson = parsedJsonData.decks; +for (const eachDeck of allDecksFromJson) { + insertDeckStatement.run(eachDeck.id, eachDeck.name, eachDeck.description); +} + +// Insert all cards from JSON into database +const allCardsFromJson = parsedJsonData.cards; +for (const eachCard of allCardsFromJson) { + let learnedValue; + if (eachCard.learned === true) { + learnedValue = 1; + } else { + learnedValue = 0; + } + insertCardStatement.run(eachCard.id, eachCard.question, eachCard.answer, learnedValue, eachCard.deckId); +} + +console.log(`Migrated ${allDecksFromJson.length} decks and ${allCardsFromJson.length} cards`); + +// Close the database connection +db.close(); diff --git a/task-2/src/storage.js b/task-2/src/storage.js index 4e59b53..81eddc9 100644 --- a/task-2/src/storage.js +++ b/task-2/src/storage.js @@ -10,16 +10,7 @@ import { dirname, join } from 'path'; const __dirname = dirname(fileURLToPath(import.meta.url)); const DATA_FILE = join(__dirname, '../data/data.json'); -// --- helpers (you will remove these when you switch to SQLite) --- -function readData() { - const raw = readFileSync(DATA_FILE, 'utf-8'); - return JSON.parse(raw); -} - -function writeData(data) { - writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf-8'); -} // ---------------------------------------------------------------- // Decks @@ -87,4 +78,4 @@ export function deleteCard(cardId) { data.cards.splice(index, 1); writeData(data); return true; -} +} \ No newline at end of file