refactor(categories): adopt Kysely for the /groups query (real production use)

Proves the POC in production: the category-groups list is now built with
the typed Kysely builder (category_groups added to the schema; COLLATE
NOCASE via sql``) and executed synchronously via all(). Byte-identical
behavior — categoryGroups test 4/4, suite 248/248, typecheck:server clean.
Establishes the migration recipe for rolling typed SQL out query-by-query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-06 16:25:30 -05:00
parent 35a0e4262e
commit d9545d6fd5
2 changed files with 22 additions and 12 deletions

View File

@ -27,8 +27,19 @@ export interface CategoriesTable {
updated_at: string; updated_at: string;
} }
export interface CategoryGroupsTable {
id: number;
user_id: number;
name: string;
sort_order: number;
is_seeded: number;
created_at: string;
updated_at: string;
}
export interface Database { export interface Database {
categories: CategoriesTable; categories: CategoriesTable;
category_groups: CategoryGroupsTable;
} }
// Build-only Kysely instance. The dialect supplies the SQLite compiler; its driver // Build-only Kysely instance. The dialect supplies the SQLite compiler; its driver

View File

@ -5,6 +5,8 @@ const { ValidationError, NotFoundError, ConflictError } = require('../utils/apiE
const router = express.Router(); const router = express.Router();
const { getDb, ensureUserDefaultCategories } = require('../db/database.cts'); const { getDb, ensureUserDefaultCategories } = require('../db/database.cts');
const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
const { kysely, all } = require('../db/kysely.cts');
const { sql } = require('kysely');
// Maps a SQLite UNIQUE-constraint violation to a friendly 409; re-throws anything // Maps a SQLite UNIQUE-constraint violation to a friendly 409; re-throws anything
// else so the terminal handler returns a safe 500. // else so the terminal handler returns a safe 500.
@ -198,19 +200,16 @@ router.put('/:id', (req: Req, res: Res) => {
// ── Category groups ────────────────────────────────────────────────────────── // ── Category groups ──────────────────────────────────────────────────────────
// GET /api/category-groups // GET /api/category-groups — typed via Kysely (built, then run synchronously).
router.get('/groups', (req: Req, res: Res) => { router.get('/groups', (req: Req, res: Res) => {
const db = getDb(); const groups = all(
const groups = db kysely()
.prepare( .selectFrom('category_groups')
` .select(['id', 'name', 'sort_order', 'created_at', 'updated_at'])
SELECT id, name, sort_order, created_at, updated_at .where('user_id', '=', req.user.id)
FROM category_groups .orderBy('sort_order', 'asc')
WHERE user_id = ? .orderBy(sql`name collate nocase`, 'asc'),
ORDER BY sort_order ASC, name COLLATE NOCASE ASC );
`,
)
.all(req.user.id);
res.json(groups); res.json(groups);
}); });