/** * Drizzle Project Repository Adapter * * Implements ProjectRepository interface using Drizzle ORM. */ import { eq, and, inArray } from 'drizzle-orm'; import { nanoid } from 'nanoid'; import type { DrizzleDatabase } from '../../index.js'; import { projects, initiativeProjects, type Project } from '../../schema.js'; import type { ProjectRepository, CreateProjectData, UpdateProjectData, } from '../project-repository.js'; export class DrizzleProjectRepository implements ProjectRepository { constructor(private db: DrizzleDatabase) {} async create(data: CreateProjectData): Promise { const id = nanoid(); const now = new Date(); const [created] = await this.db.insert(projects).values({ id, ...data, createdAt: now, updatedAt: now, }).returning(); return created; } async findById(id: string): Promise { const result = await this.db .select() .from(projects) .where(eq(projects.id, id)) .limit(1); return result[0] ?? null; } async findByName(name: string): Promise { const result = await this.db .select() .from(projects) .where(eq(projects.name, name)) .limit(1); return result[0] ?? null; } async findAll(): Promise { return this.db.select().from(projects); } async update(id: string, data: UpdateProjectData): Promise { const [updated] = await this.db .update(projects) .set({ ...data, updatedAt: new Date() }) .where(eq(projects.id, id)) .returning(); if (!updated) { throw new Error(`Project not found: ${id}`); } return updated; } async delete(id: string): Promise { const [deleted] = await this.db.delete(projects).where(eq(projects.id, id)).returning(); if (!deleted) { throw new Error(`Project not found: ${id}`); } } // Junction ops async addProjectToInitiative(initiativeId: string, projectId: string): Promise { const id = nanoid(); const now = new Date(); await this.db.insert(initiativeProjects).values({ id, initiativeId, projectId, createdAt: now, }); } async removeProjectFromInitiative(initiativeId: string, projectId: string): Promise { await this.db .delete(initiativeProjects) .where( and( eq(initiativeProjects.initiativeId, initiativeId), eq(initiativeProjects.projectId, projectId), ), ); } async findProjectsByInitiativeId(initiativeId: string): Promise { const rows = await this.db .select({ project: projects }) .from(initiativeProjects) .innerJoin(projects, eq(initiativeProjects.projectId, projects.id)) .where(eq(initiativeProjects.initiativeId, initiativeId)); return rows.map((r) => r.project); } async setInitiativeProjects(initiativeId: string, projectIds: string[]): Promise { // Get current associations const currentRows = await this.db .select({ projectId: initiativeProjects.projectId }) .from(initiativeProjects) .where(eq(initiativeProjects.initiativeId, initiativeId)); const currentIds = new Set(currentRows.map((r) => r.projectId)); const desiredIds = new Set(projectIds); // Compute diff const toRemove = [...currentIds].filter((id) => !desiredIds.has(id)); const toAdd = [...desiredIds].filter((id) => !currentIds.has(id)); // Remove if (toRemove.length > 0) { await this.db .delete(initiativeProjects) .where( and( eq(initiativeProjects.initiativeId, initiativeId), inArray(initiativeProjects.projectId, toRemove), ), ); } // Add if (toAdd.length > 0) { const now = new Date(); await this.db.insert(initiativeProjects).values( toAdd.map((projectId) => ({ id: nanoid(), initiativeId, projectId, createdAt: now, })), ); } } }