All files / dal pipeline-stages.dal.ts

100% Statements 20/20
100% Branches 6/6
100% Functions 11/11
100% Lines 18/18

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              23x     2x                         2x         2x       1x       1x               4x                           3x         3x       3x         3x       3x                 3x       3x                   3x         2x 3x 1x                            
// Data Access Layer for Pipeline Stages
import { eq, and, isNull, sql, asc, inArray } from "drizzle-orm";
import type { DrizzleD1Database } from "drizzle-orm/d1";
import * as schema from "../db/schema";
import type { PipelineStage, NewPipelineStage } from "../db/schema";
 
export class PipelineStagesDal {
	constructor(private db: DrizzleD1Database<typeof schema>) {}
 
	async findByProId(proId: string): Promise<PipelineStage[]> {
		return this.db
			.select()
			.from(schema.pipelineStages)
			.where(
				and(
					eq(schema.pipelineStages.proId, proId),
					isNull(schema.pipelineStages.deletedAt),
				),
			)
			.orderBy(asc(schema.pipelineStages.position));
	}
 
	async findById(id: number): Promise<PipelineStage | undefined> {
		const result = await this.db
			.select()
			.from(schema.pipelineStages)
			.where(eq(schema.pipelineStages.id, id))
			.limit(1);
		return result[0];
	}
 
	async create(data: NewPipelineStage): Promise<PipelineStage> {
		const result = await this.db
			.insert(schema.pipelineStages)
			.values(data)
			.returning();
		return result[0];
	}
 
	/**
	 * Insert if no row with the same (pro_id, stage_type, name) exists.
	 * Uses a WHERE NOT EXISTS sub-query so it works even without the unique index.
	 */
	async createIfNotExists(data: NewPipelineStage): Promise<void> {
		await this.db.run(
			sql`INSERT INTO pipeline_stages (pro_id, name, position, stage_type, date_created, date_updated)
				SELECT ${data.proId}, ${data.name}, ${data.position}, ${data.stageType}, unixepoch(), unixepoch()
				WHERE NOT EXISTS (
					SELECT 1 FROM pipeline_stages
					WHERE pro_id = ${data.proId} AND stage_type = ${data.stageType} AND name = ${data.name}
				)`,
		);
	}
 
	async update(
		id: number,
		data: Partial<Omit<PipelineStage, "id" | "dateCreated">>,
	): Promise<PipelineStage | undefined> {
		const result = await this.db
			.update(schema.pipelineStages)
			.set({ ...data, dateUpdated: new Date() })
			.where(eq(schema.pipelineStages.id, id))
			.returning();
		return result[0];
	}
 
	async softDelete(id: number): Promise<PipelineStage | undefined> {
		const result = await this.db
			.update(schema.pipelineStages)
			.set({ deletedAt: new Date(), dateUpdated: new Date() })
			.where(eq(schema.pipelineStages.id, id))
			.returning();
		return result[0];
	}
 
	async countByProId(proId: string): Promise<number> {
		const result = await this.db
			.select({ count: sql<number>`count(*)` })
			.from(schema.pipelineStages)
			.where(
				and(
					eq(schema.pipelineStages.proId, proId),
					isNull(schema.pipelineStages.deletedAt),
				),
			);
		return result[0]?.count ?? 0;
	}
 
	async countIntermediateByProId(proId: string): Promise<number> {
		const result = await this.db
			.select({ count: sql<number>`count(*)` })
			.from(schema.pipelineStages)
			.where(
				and(
					eq(schema.pipelineStages.proId, proId),
					isNull(schema.pipelineStages.deletedAt),
					sql`${schema.pipelineStages.stageType} NOT IN ('system_entry', 'system_terminal_won', 'system_terminal_lost')`,
				),
			);
		return result[0]?.count ?? 0;
	}
 
	async reorder(proId: string, stageIds: number[]): Promise<void> {
		// Batch update positions using a single CASE WHEN statement
		if (stageIds.length === 0) return;
		const cases = stageIds.map((id, i) => sql`WHEN ${id} THEN ${i}`);
		await this.db
			.update(schema.pipelineStages)
			.set({
				position: sql`CASE ${schema.pipelineStages.id} ${sql.join(cases, sql` `)} END`,
				dateUpdated: new Date(),
			})
			.where(
				and(
					inArray(schema.pipelineStages.id, stageIds),
					eq(schema.pipelineStages.proId, proId),
				),
			);
	}
}