import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { CreateQualityGateDto, UpdateQualityGateDto } from "./dto"; import type { QualityGate as PrismaQualityGate } from "@prisma/client"; import type { QualityGate } from "../quality-orchestrator/interfaces"; /** * Default quality gates to seed for new workspaces */ const DEFAULT_GATES = [ { name: "Build Check", type: "build", command: "pnpm build", required: true, order: 1, }, { name: "Lint Check", type: "lint", command: "pnpm lint", required: true, order: 2, }, { name: "Test Suite", type: "test", command: "pnpm test", required: true, order: 3, }, { name: "Coverage Check", type: "coverage", command: "pnpm test:coverage", expectedOutput: "85", required: false, order: 4, }, ]; /** * Service for managing quality gate configurations per workspace */ @Injectable() export class QualityGateConfigService { private readonly logger = new Logger(QualityGateConfigService.name); constructor(private readonly prisma: PrismaService) {} /** * Create a quality gate for a workspace */ async create(workspaceId: string, dto: CreateQualityGateDto): Promise { this.logger.log(`Creating quality gate "${dto.name}" for workspace ${workspaceId}`); return this.prisma.qualityGate.create({ data: { workspaceId, name: dto.name, description: dto.description ?? null, type: dto.type, command: dto.command ?? null, expectedOutput: dto.expectedOutput ?? null, isRegex: dto.isRegex ?? false, required: dto.required ?? true, order: dto.order ?? 0, isEnabled: true, }, }); } /** * Get all gates for a workspace */ async findAll(workspaceId: string): Promise { this.logger.debug(`Finding all quality gates for workspace ${workspaceId}`); return this.prisma.qualityGate.findMany({ where: { workspaceId }, orderBy: { order: "asc" }, }); } /** * Get enabled gates ordered by priority */ async findEnabled(workspaceId: string): Promise { this.logger.debug(`Finding enabled quality gates for workspace ${workspaceId}`); return this.prisma.qualityGate.findMany({ where: { workspaceId, isEnabled: true, }, orderBy: { order: "asc" }, }); } /** * Get a specific gate */ async findOne(workspaceId: string, id: string): Promise { this.logger.debug(`Finding quality gate ${id} for workspace ${workspaceId}`); const gate = await this.prisma.qualityGate.findUnique({ where: { id, workspaceId, }, }); if (!gate) { throw new NotFoundException(`Quality gate with ID ${id} not found`); } return gate; } /** * Update a gate */ async update( workspaceId: string, id: string, dto: UpdateQualityGateDto ): Promise { this.logger.log(`Updating quality gate ${id} for workspace ${workspaceId}`); // Verify gate exists and belongs to workspace await this.findOne(workspaceId, id); return this.prisma.qualityGate.update({ where: { id }, data: dto, }); } /** * Delete a gate */ async delete(workspaceId: string, id: string): Promise { this.logger.log(`Deleting quality gate ${id} for workspace ${workspaceId}`); // Verify gate exists and belongs to workspace await this.findOne(workspaceId, id); await this.prisma.qualityGate.delete({ where: { id }, }); } /** * Reorder gates */ async reorder(workspaceId: string, gateIds: string[]): Promise { this.logger.log(`Reordering quality gates for workspace ${workspaceId}`); await this.prisma.$transaction(async (tx) => { for (let i = 0; i < gateIds.length; i++) { const gateId = gateIds[i]; if (!gateId) continue; await tx.qualityGate.update({ where: { id: gateId }, data: { order: i }, }); } }); return this.findAll(workspaceId); } /** * Seed default gates for a workspace */ async seedDefaults(workspaceId: string): Promise { this.logger.log(`Seeding default quality gates for workspace ${workspaceId}`); await this.prisma.$transaction(async (tx) => { for (const gate of DEFAULT_GATES) { await tx.qualityGate.create({ data: { workspaceId, name: gate.name, type: gate.type, command: gate.command, expectedOutput: gate.expectedOutput ?? null, required: gate.required, order: gate.order, isEnabled: true, }, }); } }); return this.findAll(workspaceId); } /** * Convert database gates to orchestrator format */ toOrchestratorFormat(gates: PrismaQualityGate[]): QualityGate[] { return gates.map((gate) => { const result: QualityGate = { id: gate.id, name: gate.name, description: gate.description ?? "", type: gate.type as "test" | "lint" | "build" | "coverage" | "custom", required: gate.required, order: gate.order, }; // Only add optional properties if they exist if (gate.command) { result.command = gate.command; } if (gate.expectedOutput) { if (gate.isRegex) { // Safe regex construction with try-catch - pattern is from trusted database source try { // eslint-disable-next-line security/detect-non-literal-regexp result.expectedOutput = new RegExp(gate.expectedOutput); } catch { this.logger.warn(`Invalid regex pattern for gate ${gate.id}: ${gate.expectedOutput}`); result.expectedOutput = gate.expectedOutput; } } else { result.expectedOutput = gate.expectedOutput; } } return result; }); } }