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
This commit is contained in:
@@ -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';
|
||||
|
||||
181
src/db/repositories/drizzle/cascade.test.ts
Normal file
181
src/db/repositories/drizzle/cascade.test.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user