feat(#135): implement Quality Gate Configuration System
Add database-backed quality gate configuration for workspaces with full CRUD operations and default gate seeding. Schema: - Add QualityGate model with workspace relation - Support for custom commands and regex patterns - Enable/disable and ordering support Service: - CRUD operations for quality gates - findEnabled: Get ordered, enabled gates - reorder: Bulk reorder with transaction - seedDefaults: Seed 4 default gates - toOrchestratorFormat: Convert to orchestrator interface Endpoints: - GET /workspaces/:id/quality-gates - List - GET /workspaces/:id/quality-gates/:gateId - Get one - POST /workspaces/:id/quality-gates - Create - PATCH /workspaces/:id/quality-gates/:gateId - Update - DELETE /workspaces/:id/quality-gates/:gateId - Delete - POST /workspaces/:id/quality-gates/reorder - POST /workspaces/:id/quality-gates/seed-defaults Default gates: Build, Lint, Test, Coverage (85%) Tests: 25 passing with 95.16% coverage Fixes #135 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
237
apps/api/src/quality-gate-config/quality-gate-config.service.ts
Normal file
237
apps/api/src/quality-gate-config/quality-gate-config.service.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
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<PrismaQualityGate> {
|
||||
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<PrismaQualityGate[]> {
|
||||
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<PrismaQualityGate[]> {
|
||||
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<PrismaQualityGate> {
|
||||
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<PrismaQualityGate> {
|
||||
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<void> {
|
||||
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<PrismaQualityGate[]> {
|
||||
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<PrismaQualityGate[]> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user