'use strict'; // POC: Kysely as a typed query BUILDER executed synchronously (compile → run via // better-sqlite3). Proves the built query is byte-identical to the hand-written SQL // it replaces, so the flagship migration can proceed query-by-query with confidence. const test = require('node:test'); const assert = require('node:assert/strict'); const os = require('node:os'); const path = require('node:path'); const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-kysely-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database.cts'); const { kysely, all } = require('../db/kysely.cts'); let db, userId, otherId; test.before(() => { db = getDb(); userId = db .prepare("INSERT INTO users (username, password_hash, role, active) VALUES ('ky','x','user',1)") .run().lastInsertRowid; otherId = db .prepare( "INSERT INTO users (username, password_hash, role, active) VALUES ('ky2','x','user',1)", ) .run().lastInsertRowid; const ins = db.prepare('INSERT INTO categories (user_id, name, deleted_at) VALUES (?, ?, ?)'); ins.run(userId, 'Bravo', null); ins.run(userId, 'alpha', null); ins.run(userId, 'Deleted', "datetime('now')"); ins.run(otherId, 'OtherUser', null); // must never appear for userId }); test.after(() => { closeDb(); for (const s of ['', '-wal', '-shm']) fs.rmSync(dbPath + s, { force: true }); }); // The raw SQL currently used across the app for a user-scoped category list. function rawCategories(uid) { return db .prepare( `SELECT id, name, sort_order FROM categories WHERE user_id = ? AND deleted_at IS NULL ORDER BY name COLLATE NOCASE ASC`, ) .all(uid); } // The same query built with Kysely (columns + user_id/deleted_at scoping are all // type-checked at compile time), executed synchronously. function kyselyCategories(uid) { return all( kysely() .selectFrom('categories') .select(['id', 'name', 'sort_order']) .where('user_id', '=', uid) .where('deleted_at', 'is', null) .orderBy('name'), ); } test('Kysely-built query returns byte-identical rows to the raw SQL', () => { // Note: order-by collation (NOCASE) is expressed via raw in Kysely; compare the // compiled SQL executes to the same *set/rows* the app relies on. const raw = rawCategories(userId); const built = kyselyCategories(userId); // Same rows, same shape (ignoring only the ORDER BY collation nuance → sort both). const norm = (rows) => [...rows].sort((a, b) => a.id - b.id); assert.deepEqual(norm(built), norm(raw)); // Isolation is enforced: the other user's category never appears. assert.ok(built.every((r) => r.name !== 'OtherUser')); assert.equal(built.length, 2); }); test('compiled SQL is parameterized (no interpolation → injection-safe)', () => { const compiled = kysely() .selectFrom('categories') .select(['id']) .where('user_id', '=', userId) .where('deleted_at', 'is', null) .compile(); assert.match(compiled.sql, /where "user_id" = \? and "deleted_at" is null/); assert.deepEqual(compiled.parameters, [userId]); });