diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-10-18 22:02:02 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-10-18 22:02:02 +0200 |
| commit | 0685e9ece66fbcb09c9457027cf7bb2107cc0171 (patch) | |
| tree | f6dabadf2dcf55f2847a48ada2e03e491f017dcc /src | |
| parent | c3c61894d2b119ae424bac605d4b3099e0b674e5 (diff) | |
refactor: inject the database into services via a constructor
Pilot on the categories service: a CategoriesService class holds the Drizzle
handle, and services/index.ts wires the real instance once for the server while
tests build their own on an in-memory database. AppDatabase moves from
groceries.ts to db/types.ts.
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/server/db/types.ts | 5 | ||||
| -rw-r--r-- | src/lib/server/services/categories.test.ts | 11 | ||||
| -rw-r--r-- | src/lib/server/services/categories.ts | 63 | ||||
| -rw-r--r-- | src/lib/server/services/groceries.ts | 4 | ||||
| -rw-r--r-- | src/lib/server/services/grocery-actions.ts | 4 | ||||
| -rw-r--r-- | src/lib/server/services/index.ts | 8 | ||||
| -rw-r--r-- | src/lib/server/services/projects.ts | 2 | ||||
| -rw-r--r-- | src/lib/server/services/references.ts | 2 | ||||
| -rw-r--r-- | src/lib/server/services/routines.ts | 2 | ||||
| -rw-r--r-- | src/routes/groceries/categories/+page.server.ts | 13 | ||||
| -rw-r--r-- | src/routes/grocery/[id]/+page.server.ts | 4 |
11 files changed, 61 insertions, 57 deletions
diff --git a/src/lib/server/db/types.ts b/src/lib/server/db/types.ts new file mode 100644 index 0000000..d842677 --- /dev/null +++ b/src/lib/server/db/types.ts @@ -0,0 +1,5 @@ +import type { LibSQLDatabase } from 'drizzle-orm/libsql'; +import type * as schema from './schema'; + +/** The Drizzle handle a service is built with. */ +export type AppDatabase = LibSQLDatabase<typeof schema>; diff --git a/src/lib/server/services/categories.test.ts b/src/lib/server/services/categories.test.ts index dbb21fb..164d798 100644 --- a/src/lib/server/services/categories.test.ts +++ b/src/lib/server/services/categories.test.ts @@ -1,25 +1,26 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { groceries, categories } from '$lib/server/db/schema'; -import { eq } from 'drizzle-orm'; import { createTestDb, type TestDb } from '$lib/server/db/test-helper'; -import { loadAll, add, deleteCategory } from './categories'; +import { CategoriesService } from './categories'; describe('categories service', () => { let db: TestDb['db']; + let service: CategoriesService; beforeEach(async () => { db = (await createTestDb()).db; + service = new CategoriesService(db); }); it('loadAll returns categories sorted by name', async () => { await db.insert(categories).values([{ name: 'Zebra' }, { name: 'Apple' }]); - const result = await loadAll(db); + const result = await service.loadAll(); expect(result.map((c) => c.name)).toEqual(['Apple', 'Zebra']); }); it('add creates a category', async () => { - const cat = await add('Beverages', db); + const cat = await service.add('Beverages'); expect(cat.name).toBe('Beverages'); const rows = await db.select().from(categories); @@ -41,7 +42,7 @@ describe('categories service', () => { categoryId: cat.id, }); - await deleteCategory(cat.id, db); + await service.deleteCategory(cat.id); const catRows = await db.select().from(categories); expect(catRows).toHaveLength(0); diff --git a/src/lib/server/services/categories.ts b/src/lib/server/services/categories.ts index d6fa2c5..2ee6d2b 100644 --- a/src/lib/server/services/categories.ts +++ b/src/lib/server/services/categories.ts @@ -1,42 +1,39 @@ -import { db as defaultDb } from '$lib/server/db'; import { categories } from '$lib/server/db/schema'; import { eq, asc, sql } from 'drizzle-orm'; -import type { AppDatabase } from './groceries'; +import type { AppDatabase } from '$lib/server/db/types'; import type { Category } from './types'; -export async function loadAll(db = defaultDb): Promise<Category[]> { - return db - .select() - .from(categories) - .orderBy(asc(categories.order), asc(categories.name)); -} +export class CategoriesService { + constructor(private readonly db: AppDatabase) {} -export async function add(name: string, db = defaultDb): Promise<Category> { - const [{ maxOrder }] = await db - .select({ maxOrder: sql<number>`COALESCE(MAX(${categories.order}), 0)` }) - .from(categories); - const [inserted] = await db - .insert(categories) - .values({ name, order: (maxOrder ?? 0) + 1 }) - .returning(); - return inserted; -} + async loadAll(): Promise<Category[]> { + return this.db + .select() + .from(categories) + .orderBy(asc(categories.order), asc(categories.name)); + } -export async function reorder( - orderedIds: number[], - db = defaultDb, -): Promise<void> { - for (let i = 0; i < orderedIds.length; i++) { - await db - .update(categories) - .set({ order: i + 1 }) - .where(eq(categories.id, orderedIds[i])); + async add(name: string): Promise<Category> { + const [{ maxOrder }] = await this.db + .select({ maxOrder: sql<number>`COALESCE(MAX(${categories.order}), 0)` }) + .from(categories); + const [inserted] = await this.db + .insert(categories) + .values({ name, order: (maxOrder ?? 0) + 1 }) + .returning(); + return inserted; } -} -export async function deleteCategory( - id: number, - db = defaultDb, -): Promise<void> { - await db.delete(categories).where(eq(categories.id, id)); + async reorder(orderedIds: number[]): Promise<void> { + for (let i = 0; i < orderedIds.length; i++) { + await this.db + .update(categories) + .set({ order: i + 1 }) + .where(eq(categories.id, orderedIds[i])); + } + } + + async deleteCategory(id: number): Promise<void> { + await this.db.delete(categories).where(eq(categories.id, id)); + } } diff --git a/src/lib/server/services/groceries.ts b/src/lib/server/services/groceries.ts index c56599c..092aba0 100644 --- a/src/lib/server/services/groceries.ts +++ b/src/lib/server/services/groceries.ts @@ -2,12 +2,8 @@ import { db as defaultDb } from '$lib/server/db'; import { groceries, categories, references } from '$lib/server/db/schema'; import { eq, inArray, asc, desc } from 'drizzle-orm'; import { referenceOrder } from './references'; -import type { LibSQLDatabase } from 'drizzle-orm/libsql'; -import type * as schema from '$lib/server/db/schema'; import type { Category, Grocery, Reference } from './types'; -export type AppDatabase = LibSQLDatabase<typeof schema>; - export interface EnrichedGrocery extends Grocery { categoryName: string | null; bestRef: Reference | null; diff --git a/src/lib/server/services/grocery-actions.ts b/src/lib/server/services/grocery-actions.ts index cedb46e..2eff205 100644 --- a/src/lib/server/services/grocery-actions.ts +++ b/src/lib/server/services/grocery-actions.ts @@ -1,6 +1,6 @@ import { fail, redirect, type ActionFailure } from '@sveltejs/kit'; import { addOrUncheck, loadUnchecked, toggle } from './groceries'; -import { add as addCategory } from './categories'; +import { categoriesService } from '$lib/server/services'; import { numericId, parseFormString, parseOptionalNumber } from './validation'; import { isUniqueViolation } from '$lib/server/db/errors'; @@ -52,7 +52,7 @@ export async function addCategoryAction(request: Request) { } try { - await addCategory(name); + await categoriesService.add(name); } catch (error) { if (isUniqueViolation(error)) { return fail(400, { error: 'Cette catégorie existe déjà' }); diff --git a/src/lib/server/services/index.ts b/src/lib/server/services/index.ts new file mode 100644 index 0000000..25a05be --- /dev/null +++ b/src/lib/server/services/index.ts @@ -0,0 +1,8 @@ +import { db } from '$lib/server/db'; +import { CategoriesService } from './categories'; + +/** + * Services wired once for the running server. Tests build their own instances + * with a test database instead of importing from here. + */ +export const categoriesService = new CategoriesService(db); diff --git a/src/lib/server/services/projects.ts b/src/lib/server/services/projects.ts index b58c2f1..443297b 100644 --- a/src/lib/server/services/projects.ts +++ b/src/lib/server/services/projects.ts @@ -1,7 +1,7 @@ import { db as defaultDb } from '$lib/server/db'; import { projects } from '$lib/server/db/schema'; import { asc, eq } from 'drizzle-orm'; -import type { AppDatabase } from './groceries'; +import type { AppDatabase } from '$lib/server/db/types'; import type { Project } from './types'; export async function loadAll(db = defaultDb): Promise<Project[]> { diff --git a/src/lib/server/services/references.ts b/src/lib/server/services/references.ts index d48fb9b..accf1ba 100644 --- a/src/lib/server/services/references.ts +++ b/src/lib/server/services/references.ts @@ -1,7 +1,7 @@ import { db as defaultDb } from '$lib/server/db'; import { references } from '$lib/server/db/schema'; import { eq, ne, and, desc, asc } from 'drizzle-orm'; -import type { AppDatabase } from './groceries'; +import type { AppDatabase } from '$lib/server/db/types'; import type { Reference } from './types'; /** Primary refs first, then best score, then insertion order. */ diff --git a/src/lib/server/services/routines.ts b/src/lib/server/services/routines.ts index b1e210b..809d3f3 100644 --- a/src/lib/server/services/routines.ts +++ b/src/lib/server/services/routines.ts @@ -1,7 +1,7 @@ import { db as defaultDb } from '$lib/server/db'; import { routines, routineCompletions, projects } from '$lib/server/db/schema'; import { eq, inArray, desc, sql } from 'drizzle-orm'; -import type { AppDatabase } from './groceries'; +import type { AppDatabase } from '$lib/server/db/types'; import type { Routine, RoutineCompletion } from './types'; import { computeRoutineState, diff --git a/src/routes/groceries/categories/+page.server.ts b/src/routes/groceries/categories/+page.server.ts index 6e0e3b7..e8794fa 100644 --- a/src/routes/groceries/categories/+page.server.ts +++ b/src/routes/groceries/categories/+page.server.ts @@ -1,9 +1,5 @@ import { fail } from '@sveltejs/kit'; -import { - loadAll, - deleteCategory, - reorder, -} from '$lib/server/services/categories'; +import { categoriesService } from '$lib/server/services'; import { addCategoryAction, idAction, @@ -17,13 +13,14 @@ export interface CategoriesOutput { } export const load: PageServerLoad = async () => { - return { categories: await loadAll() }; + return { categories: await categoriesService.loadAll() }; }; export const actions: Actions = { add: ({ request }) => addCategoryAction(request), - delete: ({ request }) => idAction(request, deleteCategory), + delete: ({ request }) => + idAction(request, (id) => categoriesService.deleteCategory(id)), reorder: async ({ request }) => { const data = await request.formData(); @@ -31,7 +28,7 @@ export const actions: Actions = { if (ids.length === 0) { return fail(400, { error: 'Aucune catégorie' }); } - await reorder(ids); + await categoriesService.reorder(ids); return { success: true }; }, }; diff --git a/src/routes/grocery/[id]/+page.server.ts b/src/routes/grocery/[id]/+page.server.ts index 232568c..0f386f3 100644 --- a/src/routes/grocery/[id]/+page.server.ts +++ b/src/routes/grocery/[id]/+page.server.ts @@ -2,7 +2,7 @@ import { error, fail, redirect } from '@sveltejs/kit'; import { db } from '$lib/server/db'; import { groceries, categories } from '$lib/server/db/schema'; import { eq } from 'drizzle-orm'; -import { loadAll as loadCategories } from '$lib/server/services/categories'; +import { categoriesService } from '$lib/server/services'; import { loadForGrocery, add as addRef, @@ -40,7 +40,7 @@ export const load: PageServerLoad = async ({ params, url }) => { : [null]; const refs = await loadForGrocery(id); - const cats = await loadCategories(); + const cats = await categoriesService.loadAll(); const from = url.searchParams.get('from') || '/'; return { |
