diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-10-18 22:22:55 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-10-18 22:22:55 +0200 |
| commit | 4efed80a2f8dc238b420eadf97d56c3eccaadb76 (patch) | |
| tree | 7732a80745622c097a9e75dfb056603bceb10a23 /src | |
| parent | 0685e9ece66fbcb09c9457027cf7bb2107cc0171 (diff) | |
refactor: inject the database into every servicemain
Groceries, projects, references and routines become classes holding the
Drizzle handle, wired once in services/index.ts alongside categories. Tests
build their own instance on an in-memory database. Drops the unused
projects.loadAll and projects.rename.
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/server/services/groceries.test.ts | 30 | ||||
| -rw-r--r-- | src/lib/server/services/groceries.ts | 282 | ||||
| -rw-r--r-- | src/lib/server/services/grocery-actions.ts | 11 | ||||
| -rw-r--r-- | src/lib/server/services/index.ts | 8 | ||||
| -rw-r--r-- | src/lib/server/services/projects.ts | 53 | ||||
| -rw-r--r-- | src/lib/server/services/references.test.ts | 20 | ||||
| -rw-r--r-- | src/lib/server/services/references.ts | 98 | ||||
| -rw-r--r-- | src/lib/server/services/routine-actions.ts | 16 | ||||
| -rw-r--r-- | src/lib/server/services/routines.test.ts | 104 | ||||
| -rw-r--r-- | src/lib/server/services/routines.ts | 441 | ||||
| -rw-r--r-- | src/routes/+page.server.ts | 7 | ||||
| -rw-r--r-- | src/routes/groceries/+page.server.ts | 6 | ||||
| -rw-r--r-- | src/routes/groceries/catalog/+page.server.ts | 9 | ||||
| -rw-r--r-- | src/routes/grocery/[id]/+page.server.ts | 34 | ||||
| -rw-r--r-- | src/routes/projects/[id]/+page.server.ts | 23 | ||||
| -rw-r--r-- | src/routes/projects/[projectId]/routine/[routineId]/+page.server.ts | 9 |
16 files changed, 566 insertions, 585 deletions
diff --git a/src/lib/server/services/groceries.test.ts b/src/lib/server/services/groceries.test.ts index dbf46b7..a37e549 100644 --- a/src/lib/server/services/groceries.test.ts +++ b/src/lib/server/services/groceries.test.ts @@ -2,19 +2,15 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { groceries, categories, references } from '$lib/server/db/schema'; import { eq } from 'drizzle-orm'; import { createTestDb, type TestDb } from '$lib/server/db/test-helper'; -import { - loadUnchecked, - loadAll, - toggle, - addOrUncheck, - deleteGrocery, -} from './groceries'; +import { GroceriesService } from './groceries'; describe('groceries service', () => { let db: TestDb['db']; + let service: GroceriesService; beforeEach(async () => { db = (await createTestDb()).db; + service = new GroceriesService(db); }); it('loadUnchecked returns only unchecked items', async () => { @@ -26,7 +22,7 @@ describe('groceries service', () => { { name: 'bread', checked: true, createdAt: now }, ]); - const result = await loadUnchecked(db); + const result = await service.loadUnchecked(); expect(result.items).toHaveLength(1); expect(result.items[0].name).toBe('milk'); }); @@ -40,7 +36,7 @@ describe('groceries service', () => { { name: 'bread', checked: true, createdAt: now }, ]); - const result = await loadAll(db); + const result = await service.loadAll(); expect(result.items).toHaveLength(2); }); @@ -53,10 +49,10 @@ describe('groceries service', () => { { name: 'bread', checked: false, createdAt: now }, ]); - const unchecked = await loadUnchecked(db); + const unchecked = await service.loadUnchecked(); expect(unchecked.items.map((i) => i.name)).toEqual(['bread', 'milk']); - const all = await loadAll(db); + const all = await service.loadAll(); expect(all.items.map((i) => i.name)).toEqual(['bread', 'milk']); }); @@ -73,7 +69,7 @@ describe('groceries service', () => { { groceryId: g.id, reference: 'Brand B', qualityScore: 80 }, ]); - const result = await loadAll(db); + const result = await service.loadAll(); expect(result.items[0].bestRef?.reference).toBe('Brand A'); }); @@ -97,7 +93,7 @@ describe('groceries service', () => { .set({ isPrimary: true }) .where(eq(references.id, b.id)); - const result = await loadAll(db); + const result = await service.loadAll(); expect(result.items[0].bestRef?.reference).toBe('Brand B'); }); @@ -110,7 +106,7 @@ describe('groceries service', () => { .values({ name: 'milk', checked: false, createdAt: now }); const [item] = await db.select().from(groceries); - await toggle(item.id, db); + await service.toggle(item.id); const [updated] = await db .select() @@ -120,7 +116,7 @@ describe('groceries service', () => { }); it('addOrUncheck creates a new grocery', async () => { - const { item, created } = await addOrUncheck('eggs', null, db); + const { item, created } = await service.addOrUncheck('eggs', null); expect(created).toBe(true); expect(item.name).toBe('eggs'); @@ -137,7 +133,7 @@ describe('groceries service', () => { .insert(groceries) .values({ name: 'milk', checked: true, createdAt: now }); - const { item, created } = await addOrUncheck('milk', null, db); + const { item, created } = await service.addOrUncheck('milk', null); expect(created).toBe(false); expect(item.checked).toBe(false); @@ -156,7 +152,7 @@ describe('groceries service', () => { .values({ name: 'milk', checked: false, createdAt: now }); const [item] = await db.select().from(groceries); - await deleteGrocery(item.id, db); + await service.deleteGrocery(item.id); const rows = await db.select().from(groceries); expect(rows).toHaveLength(0); diff --git a/src/lib/server/services/groceries.ts b/src/lib/server/services/groceries.ts index 092aba0..9f4722b 100644 --- a/src/lib/server/services/groceries.ts +++ b/src/lib/server/services/groceries.ts @@ -1,7 +1,7 @@ -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 { AppDatabase } from '$lib/server/db/types'; import type { Category, Grocery, Reference } from './types'; export interface EnrichedGrocery extends Grocery { @@ -9,159 +9,167 @@ export interface EnrichedGrocery extends Grocery { bestRef: Reference | null; } -export async function loadUnchecked( - db = defaultDb, -): Promise<{ items: EnrichedGrocery[]; categories: Category[] }> { - const [items, cats, refs] = await Promise.all([ - db - .select() - .from(groceries) - .where(eq(groceries.checked, false)) - .orderBy(desc(groceries.createdAt), desc(groceries.id)), - db - .select() - .from(categories) - .orderBy(asc(categories.order), asc(categories.name)), - db - .select() - .from(references) - .orderBy(...referenceOrder), - ]); +function enrichItems( + rawItems: Grocery[], + rawCategories: Category[], + rawReferences: Reference[], +): EnrichedGrocery[] { + const catMap = new Map(rawCategories.map((c) => [c.id, c.name])); + const bestRef = new Map<number, Reference>(); + for (const ref of rawReferences) { + if (!bestRef.has(ref.groceryId)) { + bestRef.set(ref.groceryId, ref); + } + } - return { - items: enrichItems(items, cats, refs), - categories: cats, - }; + return rawItems.map((i) => ({ + ...i, + categoryName: i.categoryId ? (catMap.get(i.categoryId) ?? null) : null, + bestRef: bestRef.get(i.id) ?? null, + })); } -export async function loadAll( - db = defaultDb, -): Promise<{ items: EnrichedGrocery[]; categories: Category[] }> { - const [items, cats, refs] = await Promise.all([ - db - .select() - .from(groceries) - .orderBy(desc(groceries.createdAt), desc(groceries.id)), - db - .select() - .from(categories) - .orderBy(asc(categories.order), asc(categories.name)), - db - .select() - .from(references) - .orderBy(...referenceOrder), - ]); - - return { - items: enrichItems(items, cats, refs), - categories: cats, - }; -} +export class GroceriesService { + constructor(private readonly db: AppDatabase) {} -export async function toggle(id: number, db = defaultDb): Promise<void> { - const [item] = await db.select().from(groceries).where(eq(groceries.id, id)); - if (!item) throw new Error('Grocery not found'); + async loadUnchecked(): Promise<{ + items: EnrichedGrocery[]; + categories: Category[]; + }> { + const [items, cats, refs] = await Promise.all([ + this.db + .select() + .from(groceries) + .where(eq(groceries.checked, false)) + .orderBy(desc(groceries.createdAt), desc(groceries.id)), + this.db + .select() + .from(categories) + .orderBy(asc(categories.order), asc(categories.name)), + this.db + .select() + .from(references) + .orderBy(...referenceOrder), + ]); - await db - .update(groceries) - .set({ checked: !item.checked }) - .where(eq(groceries.id, id)); -} + return { + items: enrichItems(items, cats, refs), + categories: cats, + }; + } -export async function addOrUncheck( - name: string, - categoryId: number | null, - db = defaultDb, -): Promise<{ item: Grocery; created: boolean }> { - const [existing] = await db - .select() - .from(groceries) - .where(eq(groceries.name, name)) - .limit(1); + async loadAll(): Promise<{ + items: EnrichedGrocery[]; + categories: Category[]; + }> { + const [items, cats, refs] = await Promise.all([ + this.db + .select() + .from(groceries) + .orderBy(desc(groceries.createdAt), desc(groceries.id)), + this.db + .select() + .from(categories) + .orderBy(asc(categories.order), asc(categories.name)), + this.db + .select() + .from(references) + .orderBy(...referenceOrder), + ]); - if (existing) { - await db - .update(groceries) - .set({ - checked: false, - categoryId: categoryId ?? existing.categoryId, - }) - .where(eq(groceries.id, existing.id)); return { - item: { - ...existing, - checked: false, - categoryId: categoryId ?? existing.categoryId, - }, - created: false, + items: enrichItems(items, cats, refs), + categories: cats, }; } - const [inserted] = await db - .insert(groceries) - .values({ - name, - categoryId, - createdAt: Temporal.Now.instant().toString({ - smallestUnit: 'millisecond', - }), - }) - .returning(); + async toggle(id: number): Promise<void> { + const [item] = await this.db + .select() + .from(groceries) + .where(eq(groceries.id, id)); + if (!item) throw new Error('Grocery not found'); - return { item: inserted, created: true }; -} + await this.db + .update(groceries) + .set({ checked: !item.checked }) + .where(eq(groceries.id, id)); + } -export async function updateGrocery( - id: number, - name: string, - categoryId: number | null, - db = defaultDb, -): Promise<void> { - await db - .update(groceries) - .set({ name, categoryId }) - .where(eq(groceries.id, id)); -} + async addOrUncheck( + name: string, + categoryId: number | null, + ): Promise<{ item: Grocery; created: boolean }> { + const [existing] = await this.db + .select() + .from(groceries) + .where(eq(groceries.name, name)) + .limit(1); -export async function updateGroceryCategory( - id: number, - categoryId: number | null, - db = defaultDb, -): Promise<void> { - await db.update(groceries).set({ categoryId }).where(eq(groceries.id, id)); -} + if (existing) { + await this.db + .update(groceries) + .set({ + checked: false, + categoryId: categoryId ?? existing.categoryId, + }) + .where(eq(groceries.id, existing.id)); + return { + item: { + ...existing, + checked: false, + categoryId: categoryId ?? existing.categoryId, + }, + created: false, + }; + } -export async function bulkUpdateGroceryCategory( - ids: number[], - categoryId: number | null, - db = defaultDb, -): Promise<void> { - await db - .update(groceries) - .set({ categoryId }) - .where(inArray(groceries.id, ids)); -} + const [inserted] = await this.db + .insert(groceries) + .values({ + name, + categoryId, + createdAt: Temporal.Now.instant().toString({ + smallestUnit: 'millisecond', + }), + }) + .returning(); -export async function deleteGrocery(id: number, db = defaultDb): Promise<void> { - await db.delete(groceries).where(eq(groceries.id, id)); -} + return { item: inserted, created: true }; + } -function enrichItems( - rawItems: Grocery[], - rawCategories: Category[], - rawReferences: Reference[], -): EnrichedGrocery[] { - const catMap = new Map(rawCategories.map((c) => [c.id, c.name])); - const bestRef = new Map<number, Reference>(); - for (const ref of rawReferences) { - if (!bestRef.has(ref.groceryId)) { - bestRef.set(ref.groceryId, ref); - } + async updateGrocery( + id: number, + name: string, + categoryId: number | null, + ): Promise<void> { + await this.db + .update(groceries) + .set({ name, categoryId }) + .where(eq(groceries.id, id)); } - return rawItems.map((i) => ({ - ...i, - categoryName: i.categoryId ? (catMap.get(i.categoryId) ?? null) : null, - bestRef: bestRef.get(i.id) ?? null, - })); + async updateGroceryCategory( + id: number, + categoryId: number | null, + ): Promise<void> { + await this.db + .update(groceries) + .set({ categoryId }) + .where(eq(groceries.id, id)); + } + + async bulkUpdateGroceryCategory( + ids: number[], + categoryId: number | null, + ): Promise<void> { + await this.db + .update(groceries) + .set({ categoryId }) + .where(inArray(groceries.id, ids)); + } + + async deleteGrocery(id: number): Promise<void> { + await this.db.delete(groceries).where(eq(groceries.id, id)); + } } diff --git a/src/lib/server/services/grocery-actions.ts b/src/lib/server/services/grocery-actions.ts index 2eff205..c2b8852 100644 --- a/src/lib/server/services/grocery-actions.ts +++ b/src/lib/server/services/grocery-actions.ts @@ -1,6 +1,5 @@ import { fail, redirect, type ActionFailure } from '@sveltejs/kit'; -import { addOrUncheck, loadUnchecked, toggle } from './groceries'; -import { categoriesService } from '$lib/server/services'; +import { categoriesService, groceriesService } from '$lib/server/services'; import { numericId, parseFormString, parseOptionalNumber } from './validation'; import { isUniqueViolation } from '$lib/server/db/errors'; @@ -8,7 +7,7 @@ export async function toggleGroceryAction(request: Request) { const data = await request.formData(); try { const id = numericId(data.get('id')); - await toggle(id); + await groceriesService.toggle(id); } catch { return fail(400, { error: 'ID invalide' }); } @@ -70,8 +69,8 @@ export async function addGroceryAction(request: Request) { } const categoryId = parseOptionalNumber(data.get('category_id')); - await addOrUncheck(name, categoryId); - const { items } = await loadUnchecked(); + await groceriesService.addOrUncheck(name, categoryId); + const { items } = await groceriesService.loadUnchecked(); return { success: true, items }; } @@ -83,6 +82,6 @@ export async function addGroceryAndEditAction(request: Request) { } const categoryId = parseOptionalNumber(data.get('category_id')); - const { item } = await addOrUncheck(name, categoryId); + const { item } = await groceriesService.addOrUncheck(name, categoryId); throw redirect(303, `/grocery/${item.id}`); } diff --git a/src/lib/server/services/index.ts b/src/lib/server/services/index.ts index 25a05be..372e0d3 100644 --- a/src/lib/server/services/index.ts +++ b/src/lib/server/services/index.ts @@ -1,8 +1,16 @@ import { db } from '$lib/server/db'; import { CategoriesService } from './categories'; +import { GroceriesService } from './groceries'; +import { ProjectsService } from './projects'; +import { ReferencesService } from './references'; +import { RoutinesService } from './routines'; /** * 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); +export const groceriesService = new GroceriesService(db); +export const projectsService = new ProjectsService(db); +export const referencesService = new ReferencesService(db); +export const routinesService = new RoutinesService(db); diff --git a/src/lib/server/services/projects.ts b/src/lib/server/services/projects.ts index 443297b..d389aa5 100644 --- a/src/lib/server/services/projects.ts +++ b/src/lib/server/services/projects.ts @@ -1,38 +1,31 @@ -import { db as defaultDb } from '$lib/server/db'; import { projects } from '$lib/server/db/schema'; -import { asc, eq } from 'drizzle-orm'; +import { eq } from 'drizzle-orm'; import type { AppDatabase } from '$lib/server/db/types'; import type { Project } from './types'; -export async function loadAll(db = defaultDb): Promise<Project[]> { - return db - .select() - .from(projects) - .where(eq(projects.archived, false)) - .orderBy(asc(projects.name)); -} +export class ProjectsService { + constructor(private readonly db: AppDatabase) {} -export async function add(name: string, db = defaultDb): Promise<Project> { - const [inserted] = await db.insert(projects).values({ name }).returning(); - return inserted; -} + async add(name: string): Promise<Project> { + const [inserted] = await this.db + .insert(projects) + .values({ name }) + .returning(); + return inserted; + } -export async function rename( - id: number, - name: string, - db = defaultDb, -): Promise<void> { - await db.update(projects).set({ name }).where(eq(projects.id, id)); -} - -export async function archive(id: number, db = defaultDb): Promise<void> { - await db.update(projects).set({ archived: true }).where(eq(projects.id, id)); -} + async archive(id: number): Promise<void> { + await this.db + .update(projects) + .set({ archived: true }) + .where(eq(projects.id, id)); + } -export async function getById( - id: number, - db = defaultDb, -): Promise<Project | undefined> { - const [project] = await db.select().from(projects).where(eq(projects.id, id)); - return project; + async getById(id: number): Promise<Project | undefined> { + const [project] = await this.db + .select() + .from(projects) + .where(eq(projects.id, id)); + return project; + } } diff --git a/src/lib/server/services/references.test.ts b/src/lib/server/services/references.test.ts index fa3fc97..1f5d172 100644 --- a/src/lib/server/services/references.test.ts +++ b/src/lib/server/services/references.test.ts @@ -2,13 +2,15 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { groceries, references } from '$lib/server/db/schema'; import { eq } from 'drizzle-orm'; import { createTestDb, type TestDb } from '$lib/server/db/test-helper'; -import { loadForGrocery, add, promote, deleteReference } from './references'; +import { ReferencesService } from './references'; describe('references service', () => { let db: TestDb['db']; + let service: ReferencesService; beforeEach(async () => { db = (await createTestDb()).db; + service = new ReferencesService(db); }); it('loadForGrocery returns refs for the given grocery', async () => { @@ -29,7 +31,7 @@ describe('references service', () => { { groceryId: g2.id, reference: 'Brand B' }, ]); - const refs = await loadForGrocery(g1.id, db); + const refs = await service.loadForGrocery(g1.id); expect(refs).toHaveLength(1); expect(refs[0].reference).toBe('Brand A'); }); @@ -49,7 +51,7 @@ describe('references service', () => { { groceryId: g.id, reference: 'D', qualityScore: 50 }, ]); - const refs = await loadForGrocery(g.id, db); + const refs = await service.loadForGrocery(g.id); expect(refs.map((r) => r.reference)).toEqual(['B', 'D', 'A', 'C']); }); @@ -62,7 +64,7 @@ describe('references service', () => { .values({ name: 'pasta', checked: false, createdAt: now }) .returning(); - const ref = await add(g.id, 'Barilla', 2.5, '€/kg', 85, db); + const ref = await service.add(g.id, 'Barilla', 2.5, '€/kg', 85); expect(ref.reference).toBe('Barilla'); expect(ref.price).toBe(2.5); @@ -90,12 +92,12 @@ describe('references service', () => { .values({ groceryId: g.id, reference: 'B' }) .returning(); - await promote(b.id, db); - let refs = await loadForGrocery(g.id, db); + await service.promote(b.id); + let refs = await service.loadForGrocery(g.id); expect(refs.map((r) => r.reference)).toEqual(['B', 'A']); - await promote(a.id, db); - refs = await loadForGrocery(g.id, db); + await service.promote(a.id); + refs = await service.loadForGrocery(g.id); expect(refs.map((r) => r.reference)).toEqual(['A', 'B']); const rows = await db @@ -119,7 +121,7 @@ describe('references service', () => { .values({ groceryId: g.id, reference: 'Brand X' }) .returning(); - await deleteReference(ref.id, db); + await service.deleteReference(ref.id); const rows = await db.select().from(references); expect(rows).toHaveLength(0); diff --git a/src/lib/server/services/references.ts b/src/lib/server/services/references.ts index accf1ba..60dcc01 100644 --- a/src/lib/server/services/references.ts +++ b/src/lib/server/services/references.ts @@ -1,4 +1,3 @@ -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 '$lib/server/db/types'; @@ -11,58 +10,55 @@ export const referenceOrder = [ asc(references.id), ]; -export async function loadForGrocery( - groceryId: number, - db = defaultDb, -): Promise<Reference[]> { - return db - .select() - .from(references) - .where(eq(references.groceryId, groceryId)) - .orderBy(...referenceOrder); -} +export class ReferencesService { + constructor(private readonly db: AppDatabase) {} -/** - * Marks `id` as the primary ref of its grocery, clearing any sibling primary - * so at most one ref per grocery is promoted. - */ -export async function promote(id: number, db = defaultDb): Promise<void> { - const [target] = await db - .select() - .from(references) - .where(eq(references.id, id)); - if (!target) throw new Error('Reference not found'); + async loadForGrocery(groceryId: number): Promise<Reference[]> { + return this.db + .select() + .from(references) + .where(eq(references.groceryId, groceryId)) + .orderBy(...referenceOrder); + } - await db - .update(references) - .set({ isPrimary: true }) - .where(eq(references.id, id)); - await db - .update(references) - .set({ isPrimary: false }) - .where( - and(eq(references.groceryId, target.groceryId), ne(references.id, id)), - ); -} + /** + * Marks `id` as the primary ref of its grocery, clearing any sibling primary + * so at most one ref per grocery is promoted. + */ + async promote(id: number): Promise<void> { + const [target] = await this.db + .select() + .from(references) + .where(eq(references.id, id)); + if (!target) throw new Error('Reference not found'); -export async function add( - groceryId: number, - reference: string, - price: number | null, - unit: string | null, - qualityScore: number | null, - db = defaultDb, -): Promise<Reference> { - const [inserted] = await db - .insert(references) - .values({ groceryId, reference, price, unit, qualityScore }) - .returning(); - return inserted; -} + await this.db + .update(references) + .set({ isPrimary: true }) + .where(eq(references.id, id)); + await this.db + .update(references) + .set({ isPrimary: false }) + .where( + and(eq(references.groceryId, target.groceryId), ne(references.id, id)), + ); + } + + async add( + groceryId: number, + reference: string, + price: number | null, + unit: string | null, + qualityScore: number | null, + ): Promise<Reference> { + const [inserted] = await this.db + .insert(references) + .values({ groceryId, reference, price, unit, qualityScore }) + .returning(); + return inserted; + } -export async function deleteReference( - id: number, - db = defaultDb, -): Promise<void> { - await db.delete(references).where(eq(references.id, id)); + async deleteReference(id: number): Promise<void> { + await this.db.delete(references).where(eq(references.id, id)); + } } diff --git a/src/lib/server/services/routine-actions.ts b/src/lib/server/services/routine-actions.ts index 1d7f8b5..ea4e91e 100644 --- a/src/lib/server/services/routine-actions.ts +++ b/src/lib/server/services/routine-actions.ts @@ -1,5 +1,5 @@ import { fail, type ActionFailure } from '@sveltejs/kit'; -import { archive, complete, edit, loadCurrent } from './routines'; +import { routinesService } from '$lib/server/services'; import { parseFormString } from './validation'; import { instantFromDay } from '$lib/shared/temporal'; import { Frequency } from '$lib/shared/frequency'; @@ -14,7 +14,7 @@ export async function editRoutineAction( routineId: number, extra?: { type: 'edit' }, ): Promise<EditRoutineResult> { - const current = await loadCurrent(routineId); + const current = await routinesService.loadCurrent(routineId); if (!current) { return fail(400, { error: 'Routine introuvable' }); } @@ -47,7 +47,7 @@ export async function editRoutineAction( } try { - await edit(routineId, fields); + await routinesService.edit(routineId, fields); } catch { return fail(400, { error: 'Routine introuvable' }); } @@ -55,7 +55,7 @@ export async function editRoutineAction( } export async function completeRoutineAction(data: FormData, routineId: number) { - const current = await loadCurrent(routineId); + const current = await routinesService.loadCurrent(routineId); if (!current) { return fail(400, { error: 'Routine introuvable' }); } @@ -73,7 +73,11 @@ export async function completeRoutineAction(data: FormData, routineId: number) { } try { - await complete(routineId, parseFormString(data.get('note')), doneAt); + await routinesService.complete( + routineId, + parseFormString(data.get('note')), + doneAt, + ); } catch { return fail(400, { error: 'Routine introuvable' }); } @@ -82,7 +86,7 @@ export async function completeRoutineAction(data: FormData, routineId: number) { export async function archiveRoutineAction(routineId: number) { try { - await archive(routineId); + await routinesService.archive(routineId); } catch { return fail(400, { error: 'Routine introuvable' }); } diff --git a/src/lib/server/services/routines.test.ts b/src/lib/server/services/routines.test.ts index 2ad409a..b92db25 100644 --- a/src/lib/server/services/routines.test.ts +++ b/src/lib/server/services/routines.test.ts @@ -1,108 +1,103 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { createTestDb } from '$lib/server/db/test-helper'; -import { add as addProject } from './projects'; -import { - add, - edit, - archive, - complete, - loadAllVersions, - loadArchived, - loadProjectDashboard, - loadOverview, -} from './routines'; +import { createTestDb, type TestDb } from '$lib/server/db/test-helper'; +import { ProjectsService } from './projects'; +import { RoutinesService } from './routines'; describe('routines service', () => { - let db: Awaited<ReturnType<typeof createTestDb>>['db']; + let db: TestDb['db']; + let service: RoutinesService; let projectId: number; beforeEach(async () => { const testDb = await createTestDb(); db = testDb.db; - const project = await addProject('Household', db); + service = new RoutinesService(db); + const project = await new ProjectsService(db).add('Household'); projectId = project.id; }); it('creates a routine with version 1', async () => { - const r = await add(projectId, 'clean kitchen', '7d', db); + const r = await service.add(projectId, 'clean kitchen', '7d'); expect(r.id).toBe(1); expect(r.version).toBe(1); expect(r.frequency).toBe('7d'); }); it('edit inserts a new snapshot version and keeps history', async () => { - const r = await add(projectId, 'clean kitchen', '7d', db); - const updated = await edit( - r.id, - { name: 'clean kitchen deeply', frequency: '3d' }, - db, - ); + const r = await service.add(projectId, 'clean kitchen', '7d'); + const updated = await service.edit(r.id, { + name: 'clean kitchen deeply', + frequency: '3d', + }); expect(updated.version).toBe(2); expect(updated.frequency).toBe('3d'); - const versions = await loadAllVersions(r.id, db); + const versions = await service.loadAllVersions(r.id); expect(versions).toHaveLength(2); expect(versions[0].frequency).toBe('7d'); expect(versions[1].frequency).toBe('3d'); }); it('new routines start with an empty description', async () => { - const r = await add(projectId, 'clean kitchen', '7d', db); + const r = await service.add(projectId, 'clean kitchen', '7d'); expect(r.description).toBe(''); }); it('edit is partial: name/frequency edits keep the description and vice-versa', async () => { - const r = await add(projectId, 'clean kitchen', '7d', db); - const described = await edit(r.id, { description: 'wax on, wax off' }, db); + const r = await service.add(projectId, 'clean kitchen', '7d'); + const described = await service.edit(r.id, { + description: 'wax on, wax off', + }); expect(described.name).toBe('clean kitchen'); expect(described.frequency).toBe('7d'); expect(described.description).toBe('wax on, wax off'); - const renamed = await edit(r.id, { name: 'clean the whole kitchen' }, db); + const renamed = await service.edit(r.id, { + name: 'clean the whole kitchen', + }); expect(renamed.version).toBe(3); expect(renamed.description).toBe('wax on, wax off'); expect(renamed.frequency).toBe('7d'); - const refreq = await edit(r.id, { frequency: '14d' }, db); + const refreq = await service.edit(r.id, { frequency: '14d' }); expect(refreq.version).toBe(4); expect(refreq.name).toBe('clean the whole kitchen'); expect(refreq.description).toBe('wax on, wax off'); - const cleared = await edit(r.id, { description: '' }, db); + const cleared = await service.edit(r.id, { description: '' }); expect(cleared.version).toBe(5); expect(cleared.description).toBe(''); expect(cleared.name).toBe('clean the whole kitchen'); }); it('archive inserts an archived snapshot without removing history', async () => { - const r = await add(projectId, 'clean kitchen', '7d', db); - await archive(r.id, db); - const versions = await loadAllVersions(r.id, db); + const r = await service.add(projectId, 'clean kitchen', '7d'); + await service.archive(r.id); + const versions = await service.loadAllVersions(r.id); expect(versions).toHaveLength(2); expect(versions[0].archived).toBe(false); expect(versions[1].archived).toBe(true); - const dashboard = await loadProjectDashboard( + const dashboard = await service.loadProjectDashboard( projectId, Temporal.Now.instant(), - db, ); expect(dashboard.routines).toHaveLength(0); expect(dashboard.healthScore).toBe(100); }); it('loadArchived lists archived routines; edits keep the archive date', async () => { - const a = await add(projectId, 'clean kitchen', '7d', db); - await add(projectId, 'water plants', '3d', db); - await archive(a.id, db); - const stamped = (await loadAllVersions(a.id, db))[1].archivedAt; + const a = await service.add(projectId, 'clean kitchen', '7d'); + await service.add(projectId, 'water plants', '3d'); + await service.archive(a.id); + const stamped = (await service.loadAllVersions(a.id))[1].archivedAt; expect(stamped).toBeTruthy(); - const edited = await edit(a.id, { name: 'clean kitchen deeply' }, db); + const edited = await service.edit(a.id, { name: 'clean kitchen deeply' }); expect(edited.archived).toBe(true); expect(edited.archivedAt).toBe(stamped); - const archived = await loadArchived(projectId, db); + const archived = await service.loadArchived(projectId); expect(archived.map((r) => r.id)).toEqual([a.id]); expect(archived[0].name).toBe('clean kitchen deeply'); expect(archived[0].frequency).toBe('7d'); @@ -110,12 +105,12 @@ describe('routines service', () => { }); it('loadArchived sorts most recently archived first', async () => { - const a = await add(projectId, 'first', '1d', db); - const b = await add(projectId, 'second', '1d', db); - await archive(a.id, db); - await archive(b.id, db); + const a = await service.add(projectId, 'first', '1d'); + const b = await service.add(projectId, 'second', '1d'); + await service.archive(a.id); + await service.archive(b.id); - const archived = await loadArchived(projectId, db); + const archived = await service.loadArchived(projectId); expect(archived).toHaveLength(2); // Descending archive date; equal timestamps keep insertion order. expect( @@ -126,28 +121,27 @@ describe('routines service', () => { }); it('complete records the snapshot version it was made against', async () => { - const r = await add(projectId, 'clean kitchen', '7d', db); - await edit(r.id, { frequency: '5d' }, db); + const r = await service.add(projectId, 'clean kitchen', '7d'); + await service.edit(r.id, { frequency: '5d' }); const doneAt = Temporal.Now.instant().toString({ smallestUnit: 'millisecond', }); - const completion = await complete(r.id, 'with vinegar', doneAt, db); + const completion = await service.complete(r.id, 'with vinegar', doneAt); expect(completion.routineVersion).toBe(2); expect(completion.note).toBe('with vinegar'); }); it('dashboard computes urgency and health from completions', async () => { - const r = await add(projectId, 'clean kitchen', '7d', db); + const r = await service.add(projectId, 'clean kitchen', '7d'); const created = Temporal.Instant.from(r.createdAt); - await complete( + await service.complete( r.id, null, created.toString({ smallestUnit: 'millisecond' }), - db, ); // 22 days after completion with a 7-day routine => two periods overdue const now = created.toZonedDateTimeISO('UTC').add({ days: 22 }).toInstant(); - const dashboard = await loadProjectDashboard(projectId, now, db); + const dashboard = await service.loadProjectDashboard(projectId, now); expect(dashboard.routines).toHaveLength(1); expect(dashboard.routines[0].status).toBe('veryLate'); expect(dashboard.healthScore).toBe(0); @@ -157,12 +151,12 @@ describe('routines service', () => { }); it('overview aggregates per-project health and a global score', async () => { - await add(projectId, 'clean kitchen', '7d', db); - const growth = await addProject('Personal growth', db); - await add(growth.id, 'read 30 minutes', '1d', db); + await service.add(projectId, 'clean kitchen', '7d'); + const growth = await new ProjectsService(db).add('Personal growth'); + await service.add(growth.id, 'read 30 minutes', '1d'); const now = Temporal.Now.instant(); - const overview = await loadOverview(now, db); + const overview = await service.loadOverview(now); expect(overview.projects).toHaveLength(2); expect(overview.globalScore).toBe( Math.round(overview.projects.reduce((s, p) => s + p.healthScore, 0) / 2), diff --git a/src/lib/server/services/routines.ts b/src/lib/server/services/routines.ts index 809d3f3..0114296 100644 --- a/src/lib/server/services/routines.ts +++ b/src/lib/server/services/routines.ts @@ -1,4 +1,3 @@ -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 '$lib/server/db/types'; @@ -11,88 +10,6 @@ import { type HealthPoint, } from '$lib/shared/routine-metrics'; -// ponytail: next routine id via MAX(id)+1; fine while single-user. - -export async function nextRoutineId(db: AppDatabase): Promise<number> { - const [{ maxId }] = await db - .select({ maxId: sql<number>`COALESCE(MAX(${routines.id}), 0)` }) - .from(routines); - return (maxId ?? 0) + 1; -} - -export async function add( - projectId: number, - name: string, - frequency: string, - db = defaultDb, -): Promise<Routine> { - const id = await nextRoutineId(db); - const [inserted] = await db - .insert(routines) - .values({ - id, - version: 1, - projectId, - name, - frequency, - createdAt: Temporal.Now.instant().toString({ - smallestUnit: 'millisecond', - }), - }) - .returning(); - return inserted; -} - -/** Edit = insert a version+1 snapshot, copying unchanged fields forward. */ -export async function edit( - id: number, - fields: Partial<Pick<Routine, 'name' | 'frequency' | 'description'>>, - db = defaultDb, -): Promise<Routine> { - const [current] = await db - .select() - .from(routines) - .where(eq(routines.id, id)) - .orderBy(sql`${routines.version} DESC`) - .limit(1); - if (!current) throw new Error('Routine not found'); - - const [inserted] = await db - .insert(routines) - .values({ - id: current.id, - version: current.version + 1, - projectId: current.projectId, - name: fields.name ?? current.name, - frequency: fields.frequency ?? current.frequency, - description: fields.description ?? current.description, - archived: current.archived, - archivedAt: current.archivedAt, - createdAt: current.createdAt, - }) - .returning(); - return inserted; -} - -export async function archive(id: number, db = defaultDb): Promise<void> { - const [current] = await db - .select() - .from(routines) - .where(eq(routines.id, id)) - .orderBy(sql`${routines.version} DESC`) - .limit(1); - if (!current) throw new Error('Routine not found'); - - await db.insert(routines).values({ - ...current, - version: current.version + 1, - archived: true, - archivedAt: Temporal.Now.instant().toString({ - smallestUnit: 'millisecond', - }), - }); -} - export interface ArchivedRoutine { id: number; name: string; @@ -101,168 +18,234 @@ export interface ArchivedRoutine { archivedAt: string; } -/** Archived routines of a project, most recently archived first. */ -export async function loadArchived( - projectId: number, - db = defaultDb, -): Promise<ArchivedRoutine[]> { - const versions = await db - .select() - .from(routines) - .where(eq(routines.projectId, projectId)); - - // Max-version row per routine id; archived = max snapshot archived. - const current = new Map<number, Routine>(); - for (const r of versions) { - const existing = current.get(r.id); - if (!existing || r.version > existing.version) current.set(r.id, r); - } - return ( - [...current.values()] - .filter((r) => r.archived) - // Rows archived before archived_at existed fall back to creation date. - .sort((a, b) => ((a.archivedAt ?? '') < (b.archivedAt ?? '') ? 1 : -1)) - .map((r) => ({ - id: r.id, - name: r.name, - frequency: r.frequency, - createdAt: r.createdAt, - archivedAt: r.archivedAt ?? r.createdAt, - })) - ); +export interface ProjectDashboard { + routines: RoutineState[]; + healthScore: number; + healthSeries: HealthPoint[]; } -export async function loadAllVersions( - routineId: number, - db = defaultDb, -): Promise<Routine[]> { - return db - .select() - .from(routines) - .where(eq(routines.id, routineId)) - .orderBy(routines.version); +export interface ProjectsOverview { + projects: { project: { id: number; name: string }; healthScore: number }[]; + globalScore: number; } -/** Current (max-version) snapshot of a routine, or undefined when unknown. */ -export async function loadCurrent( - routineId: number, - db = defaultDb, -): Promise<Routine | undefined> { - const [row] = await db - .select() - .from(routines) - .where(eq(routines.id, routineId)) - .orderBy(sql`${routines.version} DESC`) - .limit(1); - return row; -} +export class RoutinesService { + constructor(private readonly db: AppDatabase) {} -export async function loadCompletions( - routineId: number, - db = defaultDb, -): Promise<RoutineCompletion[]> { - return db - .select() - .from(routineCompletions) - .where(eq(routineCompletions.routineId, routineId)) - .orderBy(desc(routineCompletions.doneAt)); -} + // ponytail: next routine id via MAX(id)+1; fine while single-user. + private async nextRoutineId(): Promise<number> { + const [{ maxId }] = await this.db + .select({ maxId: sql<number>`COALESCE(MAX(${routines.id}), 0)` }) + .from(routines); + return (maxId ?? 0) + 1; + } -export async function complete( - routineId: number, - note: string | null, - doneAt: string, - db = defaultDb, -): Promise<RoutineCompletion> { - const [current] = await db - .select() - .from(routines) - .where(eq(routines.id, routineId)) - .orderBy(sql`${routines.version} DESC`) - .limit(1); - if (!current) throw new Error('Routine not found'); + async add( + projectId: number, + name: string, + frequency: string, + ): Promise<Routine> { + const id = await this.nextRoutineId(); + const [inserted] = await this.db + .insert(routines) + .values({ + id, + version: 1, + projectId, + name, + frequency, + createdAt: Temporal.Now.instant().toString({ + smallestUnit: 'millisecond', + }), + }) + .returning(); + return inserted; + } - const [inserted] = await db - .insert(routineCompletions) - .values({ - routineId: current.id, - routineVersion: current.version, - doneAt, - note, - }) - .returning(); - return inserted; -} + /** Edit = insert a version+1 snapshot, copying unchanged fields forward. */ + async edit( + id: number, + fields: Partial<Pick<Routine, 'name' | 'frequency' | 'description'>>, + ): Promise<Routine> { + const [current] = await this.db + .select() + .from(routines) + .where(eq(routines.id, id)) + .orderBy(sql`${routines.version} DESC`) + .limit(1); + if (!current) throw new Error('Routine not found'); -export interface ProjectDashboard { - routines: RoutineState[]; - healthScore: number; - healthSeries: HealthPoint[]; -} + const [inserted] = await this.db + .insert(routines) + .values({ + id: current.id, + version: current.version + 1, + projectId: current.projectId, + name: fields.name ?? current.name, + frequency: fields.frequency ?? current.frequency, + description: fields.description ?? current.description, + archived: current.archived, + archivedAt: current.archivedAt, + createdAt: current.createdAt, + }) + .returning(); + return inserted; + } -export async function loadProjectDashboard( - projectId: number, - now: Temporal.Instant, - db = defaultDb, -): Promise<ProjectDashboard> { - const versions = await db - .select() - .from(routines) - .where(eq(routines.projectId, projectId)); + async archive(id: number): Promise<void> { + const [current] = await this.db + .select() + .from(routines) + .where(eq(routines.id, id)) + .orderBy(sql`${routines.version} DESC`) + .limit(1); + if (!current) throw new Error('Routine not found'); - // Max-version row per routine id, then drop archived ones. - const current = new Map<number, Routine>(); - for (const r of versions) { - const existing = current.get(r.id); - if (!existing || r.version > existing.version) current.set(r.id, r); + await this.db.insert(routines).values({ + ...current, + version: current.version + 1, + archived: true, + archivedAt: Temporal.Now.instant().toString({ + smallestUnit: 'millisecond', + }), + }); } - const routines_ = [...current.values()].filter((r) => !r.archived); - if (routines_.length === 0) - return { routines: [], healthScore: 100, healthSeries: [] }; - const completions = await db - .select() - .from(routineCompletions) - .where( - inArray( - routineCompletions.routineId, - routines_.map((r) => r.id), - ), + /** Archived routines of a project, most recently archived first. */ + async loadArchived(projectId: number): Promise<ArchivedRoutine[]> { + const versions = await this.db + .select() + .from(routines) + .where(eq(routines.projectId, projectId)); + + // Max-version row per routine id; archived = max snapshot archived. + const current = new Map<number, Routine>(); + for (const r of versions) { + const existing = current.get(r.id); + if (!existing || r.version > existing.version) current.set(r.id, r); + } + return ( + [...current.values()] + .filter((r) => r.archived) + // Rows archived before archived_at existed fall back to creation date. + .sort((a, b) => ((a.archivedAt ?? '') < (b.archivedAt ?? '') ? 1 : -1)) + .map((r) => ({ + id: r.id, + name: r.name, + frequency: r.frequency, + createdAt: r.createdAt, + archivedAt: r.archivedAt ?? r.createdAt, + })) ); + } - const states = routines_.map((r) => computeRoutineState(r, completions, now)); - return { - routines: states, - healthScore: computeHealthScore(states), - healthSeries: computeProjectHealthSeries(routines_, completions, now), - }; -} + async loadAllVersions(routineId: number): Promise<Routine[]> { + return this.db + .select() + .from(routines) + .where(eq(routines.id, routineId)) + .orderBy(routines.version); + } -export interface ProjectsOverview { - projects: { project: { id: number; name: string }; healthScore: number }[]; - globalScore: number; -} + /** Current (max-version) snapshot of a routine, or undefined when unknown. */ + async loadCurrent(routineId: number): Promise<Routine | undefined> { + const [row] = await this.db + .select() + .from(routines) + .where(eq(routines.id, routineId)) + .orderBy(sql`${routines.version} DESC`) + .limit(1); + return row; + } + + async loadCompletions(routineId: number): Promise<RoutineCompletion[]> { + return this.db + .select() + .from(routineCompletions) + .where(eq(routineCompletions.routineId, routineId)) + .orderBy(desc(routineCompletions.doneAt)); + } + + async complete( + routineId: number, + note: string | null, + doneAt: string, + ): Promise<RoutineCompletion> { + const [current] = await this.db + .select() + .from(routines) + .where(eq(routines.id, routineId)) + .orderBy(sql`${routines.version} DESC`) + .limit(1); + if (!current) throw new Error('Routine not found'); + + const [inserted] = await this.db + .insert(routineCompletions) + .values({ + routineId: current.id, + routineVersion: current.version, + doneAt, + note, + }) + .returning(); + return inserted; + } + + async loadProjectDashboard( + projectId: number, + now: Temporal.Instant, + ): Promise<ProjectDashboard> { + const versions = await this.db + .select() + .from(routines) + .where(eq(routines.projectId, projectId)); -export async function loadOverview( - now: Temporal.Instant, - db = defaultDb, -): Promise<ProjectsOverview> { - const activeProjects = await db - .select({ id: projects.id, name: projects.name }) - .from(projects) - .where(eq(projects.archived, false)); + // Max-version row per routine id, then drop archived ones. + const current = new Map<number, Routine>(); + for (const r of versions) { + const existing = current.get(r.id); + if (!existing || r.version > existing.version) current.set(r.id, r); + } + const live = [...current.values()].filter((r) => !r.archived); + if (live.length === 0) + return { routines: [], healthScore: 100, healthSeries: [] }; - const entries = await Promise.all( - activeProjects.map(async (p) => ({ - project: p, - healthScore: (await loadProjectDashboard(p.id, now, db)).healthScore, - })), - ); + const completions = await this.db + .select() + .from(routineCompletions) + .where( + inArray( + routineCompletions.routineId, + live.map((r) => r.id), + ), + ); - return { - projects: entries, - globalScore: computeHealthScore( - entries.map((e) => ({ health: e.healthScore })), - ), - }; + const states = live.map((r) => computeRoutineState(r, completions, now)); + return { + routines: states, + healthScore: computeHealthScore(states), + healthSeries: computeProjectHealthSeries(live, completions, now), + }; + } + + async loadOverview(now: Temporal.Instant): Promise<ProjectsOverview> { + const activeProjects = await this.db + .select({ id: projects.id, name: projects.name }) + .from(projects) + .where(eq(projects.archived, false)); + + const entries = await Promise.all( + activeProjects.map(async (p) => ({ + project: p, + healthScore: (await this.loadProjectDashboard(p.id, now)).healthScore, + })), + ); + + return { + projects: entries, + globalScore: computeHealthScore( + entries.map((e) => ({ health: e.healthScore })), + ), + }; + } } diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 008f7f3..ac00aeb 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,12 +1,11 @@ import { fail } from '@sveltejs/kit'; -import { loadOverview } from '$lib/server/services/routines'; -import { add as addProject } from '$lib/server/services/projects'; +import { projectsService, routinesService } from '$lib/server/services'; import { parseFormString } from '$lib/server/services/validation'; import { isUniqueViolation } from '$lib/server/db/errors'; import type { Actions, PageServerLoad } from './$types'; export const load: PageServerLoad = async () => { - return loadOverview(Temporal.Now.instant()); + return routinesService.loadOverview(Temporal.Now.instant()); }; export const actions: Actions = { @@ -16,7 +15,7 @@ export const actions: Actions = { return fail(400, { error: 'Le nom est requis' }); } try { - await addProject(name); + await projectsService.add(name); } catch (error) { if (isUniqueViolation(error)) { return fail(400, { error: 'Ce projet existe déjà' }); diff --git a/src/routes/groceries/+page.server.ts b/src/routes/groceries/+page.server.ts index 1cae275..d33c6a5 100644 --- a/src/routes/groceries/+page.server.ts +++ b/src/routes/groceries/+page.server.ts @@ -1,4 +1,4 @@ -import { loadUnchecked, loadAll } from '$lib/server/services/groceries'; +import { groceriesService } from '$lib/server/services'; import { addGroceryAction, addGroceryAndEditAction, @@ -15,8 +15,8 @@ export interface GroceriesOutput { } export const load: PageServerLoad = async () => { - const unchecked = await loadUnchecked(); - const all = await loadAll(); + const unchecked = await groceriesService.loadUnchecked(); + const all = await groceriesService.loadAll(); return { items: unchecked.items, categories: unchecked.categories, diff --git a/src/routes/groceries/catalog/+page.server.ts b/src/routes/groceries/catalog/+page.server.ts index fd71e30..8d94154 100644 --- a/src/routes/groceries/catalog/+page.server.ts +++ b/src/routes/groceries/catalog/+page.server.ts @@ -1,8 +1,5 @@ import { fail } from '@sveltejs/kit'; -import { - loadAll, - bulkUpdateGroceryCategory, -} from '$lib/server/services/groceries'; +import { groceriesService } from '$lib/server/services'; import { toggleGroceryAction } from '$lib/server/services/grocery-actions'; import { numericId, @@ -18,7 +15,7 @@ export interface CatalogOutput { } export const load: PageServerLoad = async () => { - return loadAll(); + return groceriesService.loadAll(); }; export const actions: Actions = { @@ -34,7 +31,7 @@ export const actions: Actions = { return fail(400, { error: 'Aucun article sélectionné' }); } - await bulkUpdateGroceryCategory(ids, categoryId); + await groceriesService.bulkUpdateGroceryCategory(ids, categoryId); return { success: true, type: 'bulkUpdate' as const }; }, }; diff --git a/src/routes/grocery/[id]/+page.server.ts b/src/routes/grocery/[id]/+page.server.ts index 0f386f3..dbfecff 100644 --- a/src/routes/grocery/[id]/+page.server.ts +++ b/src/routes/grocery/[id]/+page.server.ts @@ -2,14 +2,11 @@ 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 { categoriesService } from '$lib/server/services'; import { - loadForGrocery, - add as addRef, - promote, - deleteReference, -} from '$lib/server/services/references'; -import { updateGrocery, deleteGrocery } from '$lib/server/services/groceries'; + categoriesService, + groceriesService, + referencesService, +} from '$lib/server/services'; import { idAction } from '$lib/server/services/grocery-actions'; import { numericId, @@ -39,7 +36,7 @@ export const load: PageServerLoad = async ({ params, url }) => { .where(eq(categories.id, item.categoryId)) : [null]; - const refs = await loadForGrocery(id); + const refs = await referencesService.loadForGrocery(id); const cats = await categoriesService.loadAll(); const from = url.searchParams.get('from') || '/'; @@ -67,14 +64,25 @@ export const actions: Actions = { return fail(400, { error: 'La référence est requise' }); } - await addRef(groceryId, reference, price, unit, qualityScore); + await referencesService.add( + groceryId, + reference, + price, + unit, + qualityScore, + ); return { success: true, type: 'add' as const }; }, delete: ({ request }) => - idAction(request, deleteReference, { type: 'delete' }), + idAction(request, (id) => referencesService.deleteReference(id), { + type: 'delete', + }), - promote: ({ request }) => idAction(request, promote, { type: 'promote' }), + promote: ({ request }) => + idAction(request, (id) => referencesService.promote(id), { + type: 'promote', + }), update: async ({ request, params }) => { const data = await request.formData(); @@ -88,7 +96,7 @@ export const actions: Actions = { } try { - await updateGrocery(groceryId, name, categoryId); + await groceriesService.updateGrocery(groceryId, name, categoryId); } catch (error) { if (isUniqueViolation(error)) { return fail(400, { error: 'Un article porte déjà ce nom' }); @@ -100,7 +108,7 @@ export const actions: Actions = { deleteItem: async ({ params, request }) => { const groceryId = numericId(params.id); - await deleteGrocery(groceryId); + await groceriesService.deleteGrocery(groceryId); const data = await request.formData(); const from = parseFormString(data.get('from')) || '/'; throw redirect(303, from); diff --git a/src/routes/projects/[id]/+page.server.ts b/src/routes/projects/[id]/+page.server.ts index 83d7571..1df28c2 100644 --- a/src/routes/projects/[id]/+page.server.ts +++ b/src/routes/projects/[id]/+page.server.ts @@ -1,13 +1,5 @@ import { error, fail, redirect } from '@sveltejs/kit'; -import { - archive as archiveProject, - getById, -} from '$lib/server/services/projects'; -import { - add, - loadProjectDashboard, - loadArchived, -} from '$lib/server/services/routines'; +import { projectsService, routinesService } from '$lib/server/services'; import { completeRoutineAction } from '$lib/server/services/routine-actions'; import { numericId, parseFormString } from '$lib/server/services/validation'; import { Frequency } from '$lib/shared/frequency'; @@ -15,12 +7,15 @@ import type { Actions, PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ params }) => { const id = numericId(params.id); - const project = await getById(id); + const project = await projectsService.getById(id); if (!project || project.archived) { throw error(404, 'Projet introuvable'); } - const dashboard = await loadProjectDashboard(id, Temporal.Now.instant()); - const archived = await loadArchived(id); + const dashboard = await routinesService.loadProjectDashboard( + id, + Temporal.Now.instant(), + ); + const archived = await routinesService.loadArchived(id); return { project: { id: project.id, name: project.name }, routines: dashboard.routines, @@ -38,7 +33,7 @@ export const actions: Actions = { return fail(400, { error: 'Le nom est requis' }); } try { - await add( + await routinesService.add( numericId(params.id), name, Frequency.parse(data.get('frequency')?.toString() ?? '').toSpec(), @@ -55,7 +50,7 @@ export const actions: Actions = { }, archiveProject: async ({ params }) => { - await archiveProject(numericId(params.id)); + await projectsService.archive(numericId(params.id)); throw redirect(303, '/'); }, }; diff --git a/src/routes/projects/[projectId]/routine/[routineId]/+page.server.ts b/src/routes/projects/[projectId]/routine/[routineId]/+page.server.ts index cb14e0d..4ace86f 100644 --- a/src/routes/projects/[projectId]/routine/[routineId]/+page.server.ts +++ b/src/routes/projects/[projectId]/routine/[routineId]/+page.server.ts @@ -1,6 +1,5 @@ import { error } from '@sveltejs/kit'; -import { getById } from '$lib/server/services/projects'; -import { loadCurrent, loadCompletions } from '$lib/server/services/routines'; +import { projectsService, routinesService } from '$lib/server/services'; import { archiveRoutineAction, completeRoutineAction, @@ -14,17 +13,17 @@ export const load: PageServerLoad = async ({ params }) => { const projectId = numericId(params.projectId); const routineId = numericId(params.routineId); - const project = await getById(projectId); + const project = await projectsService.getById(projectId); if (!project || project.archived) { throw error(404, 'Projet introuvable'); } - const routine = await loadCurrent(routineId); + const routine = await routinesService.loadCurrent(routineId); if (!routine || routine.projectId !== projectId) { throw error(404, 'Routine introuvable'); } - const completions = await loadCompletions(routineId); + const completions = await routinesService.loadCompletions(routineId); // Archived routines are frozen at their archive instant, not now. const at = routine.archivedAt ? Temporal.Instant.from(routine.archivedAt) |
