# All files / src/db/repositories/drizzle project.ts

55.26% Statements 21/38
25% Branches 3/12
50% Functions 8/16
55.88% Lines 19/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155

    84x

    4x
    4x

    4x

    4x

    1x

    1x

    2x

    2x

    4x

    4x
    4x

    4x
    4x

    4x

    4x
    4x
    4x
    4x

     /**
     * 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<Project> {
        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<Project | null> {
        const result = await this.db
          .select()
          .from(projects)
          .where(eq(projects.id, id))
          .limit(1);

        return result[0] ?? null;
      }

      async findByName(name: string): Promise<Project | null> {
        const result = await this.db
          .select()
          .from(projects)
          .where(eq(projects.name, name))
          .limit(1);

        return result[0] ?? null;
      }

      async findAll(): Promise<Project[]> {
        return this.db.select().from(projects);
      }

      async update(id: string, data: UpdateProjectData): Promise<Project> {
        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<void> {
        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<void> {
        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<void> {
        await this.db
          .delete(initiativeProjects)
          .where(
            and(
              eq(initiativeProjects.initiativeId, initiativeId),
              eq(initiativeProjects.projectId, projectId),
            ),
          );
      }

      async findProjectsByInitiativeId(initiativeId: string): Promise<Project[]> {
        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<void> {
        // 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
        Iif (toRemove.length > 0) {
          await this.db
            .delete(initiativeProjects)
            .where(
              and(
                eq(initiativeProjects.initiativeId, initiativeId),
                inArray(initiativeProjects.projectId, toRemove),
              ),
            );
        }

        // Add
        Eif (toAdd.length > 0) {
          const now = new Date();
          await this.db.insert(initiativeProjects).values(
            toAdd.map((projectId) => ({
              id: nanoid(),
              initiativeId,
              projectId,
              createdAt: now,
            })),
          );
        }
      }
    }