From 112cc231c7526a7d5f1eb001ef17ece335f7f498 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Fri, 30 Jan 2026 19:11:31 +0100 Subject: [PATCH] feat(02-02): update exports and add cascade delete tests - src/db/index.ts exports repository interfaces and adapters - cascade.test.ts tests full hierarchy cascade behavior: - Delete initiative removes all phases, plans, tasks - Delete phase removes only its plans and tasks - Delete plan removes only its tasks - All 45 repository tests passing --- src/db/index.ts | 6 + src/db/repositories/drizzle/cascade.test.ts | 181 ++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/db/repositories/drizzle/cascade.test.ts diff --git a/src/db/index.ts b/src/db/index.ts index 01c7833..49b7d8c 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -41,3 +41,9 @@ export { getDbPath, ensureDbDirectory } from './config.js'; // Re-export schema and types export * from './schema.js'; + +// Re-export repository interfaces (ports) +export * from './repositories/index.js'; + +// Re-export Drizzle adapters +export * from './repositories/drizzle/index.js'; diff --git a/src/db/repositories/drizzle/cascade.test.ts b/src/db/repositories/drizzle/cascade.test.ts new file mode 100644 index 0000000..e3e7886 --- /dev/null +++ b/src/db/repositories/drizzle/cascade.test.ts @@ -0,0 +1,181 @@ +/** + * Cascade Delete Tests + * + * Tests that cascade deletes work correctly through the repository layer. + * Verifies the SQLite foreign key cascade behavior configured in schema.ts. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { DrizzleInitiativeRepository } from './initiative.js'; +import { DrizzlePhaseRepository } from './phase.js'; +import { DrizzlePlanRepository } from './plan.js'; +import { DrizzleTaskRepository } from './task.js'; +import { createTestDatabase } from './test-helpers.js'; +import type { DrizzleDatabase } from '../../index.js'; + +describe('Cascade Deletes', () => { + let db: DrizzleDatabase; + let initiativeRepo: DrizzleInitiativeRepository; + let phaseRepo: DrizzlePhaseRepository; + let planRepo: DrizzlePlanRepository; + let taskRepo: DrizzleTaskRepository; + + beforeEach(() => { + db = createTestDatabase(); + initiativeRepo = new DrizzleInitiativeRepository(db); + phaseRepo = new DrizzlePhaseRepository(db); + planRepo = new DrizzlePlanRepository(db); + taskRepo = new DrizzleTaskRepository(db); + }); + + /** + * Helper to create a full hierarchy for testing. + */ + async function createFullHierarchy() { + const initiative = await initiativeRepo.create({ + name: 'Test Initiative', + }); + + const phase1 = await phaseRepo.create({ + initiativeId: initiative.id, + number: 1, + name: 'Phase 1', + }); + + const phase2 = await phaseRepo.create({ + initiativeId: initiative.id, + number: 2, + name: 'Phase 2', + }); + + const plan1 = await planRepo.create({ + phaseId: phase1.id, + number: 1, + name: 'Plan 1-1', + }); + + const plan2 = await planRepo.create({ + phaseId: phase1.id, + number: 2, + name: 'Plan 1-2', + }); + + const plan3 = await planRepo.create({ + phaseId: phase2.id, + number: 1, + name: 'Plan 2-1', + }); + + const task1 = await taskRepo.create({ + planId: plan1.id, + name: 'Task 1-1-1', + order: 1, + }); + + const task2 = await taskRepo.create({ + planId: plan1.id, + name: 'Task 1-1-2', + order: 2, + }); + + const task3 = await taskRepo.create({ + planId: plan2.id, + name: 'Task 1-2-1', + order: 1, + }); + + const task4 = await taskRepo.create({ + planId: plan3.id, + name: 'Task 2-1-1', + order: 1, + }); + + return { + initiative, + phases: { phase1, phase2 }, + plans: { plan1, plan2, plan3 }, + tasks: { task1, task2, task3, task4 }, + }; + } + + describe('delete initiative', () => { + it('should cascade delete all phases, plans, and tasks', async () => { + const { initiative, phases, plans, tasks } = await createFullHierarchy(); + + // Verify everything exists + expect(await initiativeRepo.findById(initiative.id)).not.toBeNull(); + expect(await phaseRepo.findById(phases.phase1.id)).not.toBeNull(); + expect(await phaseRepo.findById(phases.phase2.id)).not.toBeNull(); + expect(await planRepo.findById(plans.plan1.id)).not.toBeNull(); + expect(await planRepo.findById(plans.plan2.id)).not.toBeNull(); + expect(await planRepo.findById(plans.plan3.id)).not.toBeNull(); + expect(await taskRepo.findById(tasks.task1.id)).not.toBeNull(); + expect(await taskRepo.findById(tasks.task2.id)).not.toBeNull(); + expect(await taskRepo.findById(tasks.task3.id)).not.toBeNull(); + expect(await taskRepo.findById(tasks.task4.id)).not.toBeNull(); + + // Delete initiative + await initiativeRepo.delete(initiative.id); + + // Verify everything is gone + expect(await initiativeRepo.findById(initiative.id)).toBeNull(); + expect(await phaseRepo.findById(phases.phase1.id)).toBeNull(); + expect(await phaseRepo.findById(phases.phase2.id)).toBeNull(); + expect(await planRepo.findById(plans.plan1.id)).toBeNull(); + expect(await planRepo.findById(plans.plan2.id)).toBeNull(); + expect(await planRepo.findById(plans.plan3.id)).toBeNull(); + expect(await taskRepo.findById(tasks.task1.id)).toBeNull(); + expect(await taskRepo.findById(tasks.task2.id)).toBeNull(); + expect(await taskRepo.findById(tasks.task3.id)).toBeNull(); + expect(await taskRepo.findById(tasks.task4.id)).toBeNull(); + }); + }); + + describe('delete phase', () => { + it('should cascade delete plans and tasks under that phase only', async () => { + const { initiative, phases, plans, tasks } = await createFullHierarchy(); + + // Delete phase 1 + await phaseRepo.delete(phases.phase1.id); + + // Initiative still exists + expect(await initiativeRepo.findById(initiative.id)).not.toBeNull(); + + // Phase 1 and its children are gone + expect(await phaseRepo.findById(phases.phase1.id)).toBeNull(); + expect(await planRepo.findById(plans.plan1.id)).toBeNull(); + expect(await planRepo.findById(plans.plan2.id)).toBeNull(); + expect(await taskRepo.findById(tasks.task1.id)).toBeNull(); + expect(await taskRepo.findById(tasks.task2.id)).toBeNull(); + expect(await taskRepo.findById(tasks.task3.id)).toBeNull(); + + // Phase 2 and its children still exist + expect(await phaseRepo.findById(phases.phase2.id)).not.toBeNull(); + expect(await planRepo.findById(plans.plan3.id)).not.toBeNull(); + expect(await taskRepo.findById(tasks.task4.id)).not.toBeNull(); + }); + }); + + describe('delete plan', () => { + it('should cascade delete tasks under that plan only', async () => { + const { phases, plans, tasks } = await createFullHierarchy(); + + // Delete plan 1 + await planRepo.delete(plans.plan1.id); + + // Phase still exists + expect(await phaseRepo.findById(phases.phase1.id)).not.toBeNull(); + + // Plan 1 and its tasks are gone + expect(await planRepo.findById(plans.plan1.id)).toBeNull(); + expect(await taskRepo.findById(tasks.task1.id)).toBeNull(); + expect(await taskRepo.findById(tasks.task2.id)).toBeNull(); + + // Other plans and tasks still exist + expect(await planRepo.findById(plans.plan2.id)).not.toBeNull(); + expect(await planRepo.findById(plans.plan3.id)).not.toBeNull(); + expect(await taskRepo.findById(tasks.task3.id)).not.toBeNull(); + expect(await taskRepo.findById(tasks.task4.id)).not.toBeNull(); + }); + }); +});