From 394aa22df19f6190406aa9f1cad3996b4a3ae1a0 Mon Sep 17 00:00:00 2001 From: Paul Huliganga Date: Tue, 24 Mar 2026 00:01:41 -0400 Subject: [PATCH] feat(db): enable schema migration using sql.js and export to data/recipes.db --- TODO.md | 2 +- src/backend/db/migrate.ts | 36 ++++++++++++++++++++++++++++++++++++ src/backend/db/schema.sql | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/backend/db/migrate.ts create mode 100644 src/backend/db/schema.sql diff --git a/TODO.md b/TODO.md index dd92f1a..e9328e4 100644 --- a/TODO.md +++ b/TODO.md @@ -10,7 +10,7 @@ ### Backend Setup - [x] Initialize Node.js backend: Create src/backend/, package.json with express, better-sqlite3, zod, vitest. Create src/backend/index.ts with "Hello World" server on port 3000. Verify: npm install && npm run dev (server starts). Commit as "feat(backend): initialize Node.js project with Express" - [x] Set up TypeScript: Create tsconfig.json (strict mode, ES2022, Node16 module resolution). Add build script to package.json. Verify: npm run build succeeds. Commit. -- [ ] Create SQLite schema: Create src/backend/db/schema.sql with recipes, tags, recipe_tags tables per ARCHITECTURE.md. Create src/backend/db/migrate.ts to apply schema. Verify: npm run migrate creates data/recipes.db. Commit. +- [x] Create SQLite schema: Create src/backend/db/schema.sql with recipes, tags, recipe_tags tables per ARCHITECTURE.md. Create src/backend/db/migrate.ts to apply schema using sql.js (see ADR-001). Verify: npm run migrate creates data/recipes.db. Commit. - [ ] Implement recipe CRUD API endpoints - [ ] Add Zod validation schemas - [ ] Write API integration tests diff --git a/src/backend/db/migrate.ts b/src/backend/db/migrate.ts new file mode 100644 index 0000000..6635184 --- /dev/null +++ b/src/backend/db/migrate.ts @@ -0,0 +1,36 @@ +import initSqlJs from 'sql.js'; +import fs from 'fs'; +import path from 'path'; + +const DATA_DIR = path.resolve(process.cwd(), 'data'); +const DB_PATH = path.join(DATA_DIR, 'recipes.db'); +const SCHEMA_PATH = path.resolve(process.cwd(), 'src/backend/db/schema.sql'); + +async function ensureDataDir() { + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + console.log(`Created data directory at ${DATA_DIR}`); + } +} + +async function applyMigrations() { + await ensureDataDir(); + + const schema = fs.readFileSync(SCHEMA_PATH, 'utf8'); + const SQL = await initSqlJs(); + const db = new SQL.Database(); + + // Run all SQL statements + db.exec(schema); + + // Export and save to file + const data = db.export(); + fs.writeFileSync(DB_PATH, Buffer.from(data)); + console.log(`Database migrated: ${DB_PATH}`); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + applyMigrations(); +} + +export { applyMigrations }; diff --git a/src/backend/db/schema.sql b/src/backend/db/schema.sql new file mode 100644 index 0000000..b07b007 --- /dev/null +++ b/src/backend/db/schema.sql @@ -0,0 +1,38 @@ +-- Create recipes table +CREATE TABLE recipes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + ingredients TEXT NOT NULL, -- JSON array + instructions TEXT NOT NULL, -- JSON array of steps + source_url TEXT, + notes TEXT, + servings INTEGER, + prep_time_minutes INTEGER, + cook_time_minutes INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + last_cooked_at INTEGER +); + +-- Create tags table +CREATE TABLE tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + color TEXT -- Hex color for UI +); + +-- Create recipe_tags join table +CREATE TABLE recipe_tags ( + recipe_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (recipe_id, tag_id), + FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); + +-- Indexes for efficiency +CREATE INDEX idx_recipes_title ON recipes(title); +CREATE INDEX idx_recipes_created_at ON recipes(created_at DESC); +CREATE INDEX idx_recipe_tags_recipe ON recipe_tags(recipe_id); +CREATE INDEX idx_recipe_tags_tag ON recipe_tags(tag_id);