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:
2026-01-31 13:33:04 -06:00
parent a25e9048be
commit 4a2909ce1e
10 changed files with 980 additions and 0 deletions

View File

@@ -215,6 +215,7 @@ model Workspace {
cronSchedules CronSchedule[]
personalities Personality[]
llmSettings WorkspaceLlmSettings?
qualityGates QualityGate[]
@@index([ownerId])
@@map("workspaces")
@@ -1003,3 +1004,29 @@ model WorkspaceLlmSettings {
@@index([defaultPersonalityId])
@@map("workspace_llm_settings")
}
// ============================================
// QUALITY GATE MODULE
// ============================================
model QualityGate {
id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
name String
description String?
type String // 'build' | 'lint' | 'test' | 'coverage' | 'custom'
command String?
expectedOutput String? @map("expected_output")
isRegex Boolean @default(false) @map("is_regex")
required Boolean @default(true)
order Int @default(0)
isEnabled Boolean @default(true) @map("is_enabled")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
@@unique([workspaceId, name])
@@index([workspaceId])
@@index([workspaceId, isEnabled])
@@map("quality_gates")
}

View File

@@ -0,0 +1,37 @@
import { IsString, IsOptional, IsBoolean, IsIn, IsInt, IsNotEmpty } from "class-validator";
/**
* DTO for creating a new quality gate
*/
export class CreateQualityGateDto {
@IsString()
@IsNotEmpty()
name!: string;
@IsOptional()
@IsString()
description?: string;
@IsIn(["build", "lint", "test", "coverage", "custom"])
type!: string;
@IsOptional()
@IsString()
command?: string;
@IsOptional()
@IsString()
expectedOutput?: string;
@IsOptional()
@IsBoolean()
isRegex?: boolean;
@IsOptional()
@IsBoolean()
required?: boolean;
@IsOptional()
@IsInt()
order?: number;
}

View File

@@ -0,0 +1,2 @@
export * from "./create-quality-gate.dto";
export * from "./update-quality-gate.dto";

View File

@@ -0,0 +1,42 @@
import { IsString, IsOptional, IsBoolean, IsIn, IsInt } from "class-validator";
/**
* DTO for updating an existing quality gate
*/
export class UpdateQualityGateDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsIn(["build", "lint", "test", "coverage", "custom"])
type?: string;
@IsOptional()
@IsString()
command?: string;
@IsOptional()
@IsString()
expectedOutput?: string;
@IsOptional()
@IsBoolean()
isRegex?: boolean;
@IsOptional()
@IsBoolean()
required?: boolean;
@IsOptional()
@IsInt()
order?: number;
@IsOptional()
@IsBoolean()
isEnabled?: boolean;
}

View File

@@ -0,0 +1,4 @@
export * from "./quality-gate-config.module";
export * from "./quality-gate-config.service";
export * from "./quality-gate-config.controller";
export * from "./dto";

View File

@@ -0,0 +1,164 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { QualityGateConfigController } from "./quality-gate-config.controller";
import { QualityGateConfigService } from "./quality-gate-config.service";
describe("QualityGateConfigController", () => {
let controller: QualityGateConfigController;
let service: QualityGateConfigService;
const mockService = {
create: vi.fn(),
findAll: vi.fn(),
findEnabled: vi.fn(),
findOne: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
reorder: vi.fn(),
seedDefaults: vi.fn(),
};
const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440001";
const mockGateId = "550e8400-e29b-41d4-a716-446655440002";
const mockQualityGate = {
id: mockGateId,
workspaceId: mockWorkspaceId,
name: "Build Check",
description: "Verify code compiles without errors",
type: "build",
command: "pnpm build",
expectedOutput: null,
isRegex: false,
required: true,
order: 1,
isEnabled: true,
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [QualityGateConfigController],
providers: [
{
provide: QualityGateConfigService,
useValue: mockService,
},
],
}).compile();
controller = module.get<QualityGateConfigController>(QualityGateConfigController);
service = module.get<QualityGateConfigService>(QualityGateConfigService);
vi.clearAllMocks();
});
it("should be defined", () => {
expect(controller).toBeDefined();
});
describe("findAll", () => {
it("should return all quality gates for a workspace", async () => {
const mockGates = [mockQualityGate];
mockService.findAll.mockResolvedValue(mockGates);
const result = await controller.findAll(mockWorkspaceId);
expect(result).toEqual(mockGates);
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId);
});
});
describe("findOne", () => {
it("should return a single quality gate", async () => {
mockService.findOne.mockResolvedValue(mockQualityGate);
const result = await controller.findOne(mockWorkspaceId, mockGateId);
expect(result).toEqual(mockQualityGate);
expect(service.findOne).toHaveBeenCalledWith(mockWorkspaceId, mockGateId);
});
});
describe("create", () => {
it("should create a new quality gate", async () => {
const createDto = {
name: "Build Check",
description: "Verify code compiles without errors",
type: "build" as const,
command: "pnpm build",
required: true,
order: 1,
};
mockService.create.mockResolvedValue(mockQualityGate);
const result = await controller.create(mockWorkspaceId, createDto);
expect(result).toEqual(mockQualityGate);
expect(service.create).toHaveBeenCalledWith(mockWorkspaceId, createDto);
});
});
describe("update", () => {
it("should update a quality gate", async () => {
const updateDto = {
name: "Updated Build Check",
required: false,
};
const updatedGate = {
...mockQualityGate,
...updateDto,
};
mockService.update.mockResolvedValue(updatedGate);
const result = await controller.update(mockWorkspaceId, mockGateId, updateDto);
expect(result).toEqual(updatedGate);
expect(service.update).toHaveBeenCalledWith(mockWorkspaceId, mockGateId, updateDto);
});
});
describe("delete", () => {
it("should delete a quality gate", async () => {
mockService.delete.mockResolvedValue(undefined);
await controller.delete(mockWorkspaceId, mockGateId);
expect(service.delete).toHaveBeenCalledWith(mockWorkspaceId, mockGateId);
});
});
describe("reorder", () => {
it("should reorder quality gates", async () => {
const gateIds = ["gate1", "gate2", "gate3"];
const mockGates = gateIds.map((id, index) => ({
...mockQualityGate,
id,
order: index,
}));
mockService.reorder.mockResolvedValue(mockGates);
const result = await controller.reorder(mockWorkspaceId, { gateIds });
expect(result).toEqual(mockGates);
expect(service.reorder).toHaveBeenCalledWith(mockWorkspaceId, gateIds);
});
});
describe("seedDefaults", () => {
it("should seed default quality gates", async () => {
const mockGates = [mockQualityGate];
mockService.seedDefaults.mockResolvedValue(mockGates);
const result = await controller.seedDefaults(mockWorkspaceId);
expect(result).toEqual(mockGates);
expect(service.seedDefaults).toHaveBeenCalledWith(mockWorkspaceId);
});
});
});

View File

@@ -0,0 +1,90 @@
import { Controller, Get, Post, Patch, Delete, Body, Param, Logger } from "@nestjs/common";
import { QualityGateConfigService } from "./quality-gate-config.service";
import { CreateQualityGateDto, UpdateQualityGateDto } from "./dto";
import type { QualityGate } from "@prisma/client";
/**
* Controller for managing quality gate configurations per workspace
*/
@Controller("workspaces/:workspaceId/quality-gates")
export class QualityGateConfigController {
private readonly logger = new Logger(QualityGateConfigController.name);
constructor(private readonly qualityGateConfigService: QualityGateConfigService) {}
/**
* Get all quality gates for a workspace
*/
@Get()
async findAll(@Param("workspaceId") workspaceId: string): Promise<QualityGate[]> {
this.logger.debug(`GET /workspaces/${workspaceId}/quality-gates`);
return this.qualityGateConfigService.findAll(workspaceId);
}
/**
* Get a specific quality gate
*/
@Get(":id")
async findOne(
@Param("workspaceId") workspaceId: string,
@Param("id") id: string
): Promise<QualityGate> {
this.logger.debug(`GET /workspaces/${workspaceId}/quality-gates/${id}`);
return this.qualityGateConfigService.findOne(workspaceId, id);
}
/**
* Create a new quality gate
*/
@Post()
async create(
@Param("workspaceId") workspaceId: string,
@Body() createDto: CreateQualityGateDto
): Promise<QualityGate> {
this.logger.log(`POST /workspaces/${workspaceId}/quality-gates`);
return this.qualityGateConfigService.create(workspaceId, createDto);
}
/**
* Update a quality gate
*/
@Patch(":id")
async update(
@Param("workspaceId") workspaceId: string,
@Param("id") id: string,
@Body() updateDto: UpdateQualityGateDto
): Promise<QualityGate> {
this.logger.log(`PATCH /workspaces/${workspaceId}/quality-gates/${id}`);
return this.qualityGateConfigService.update(workspaceId, id, updateDto);
}
/**
* Delete a quality gate
*/
@Delete(":id")
async delete(@Param("workspaceId") workspaceId: string, @Param("id") id: string): Promise<void> {
this.logger.log(`DELETE /workspaces/${workspaceId}/quality-gates/${id}`);
return this.qualityGateConfigService.delete(workspaceId, id);
}
/**
* Reorder quality gates
*/
@Post("reorder")
async reorder(
@Param("workspaceId") workspaceId: string,
@Body() body: { gateIds: string[] }
): Promise<QualityGate[]> {
this.logger.log(`POST /workspaces/${workspaceId}/quality-gates/reorder`);
return this.qualityGateConfigService.reorder(workspaceId, body.gateIds);
}
/**
* Seed default quality gates for a workspace
*/
@Post("seed-defaults")
async seedDefaults(@Param("workspaceId") workspaceId: string): Promise<QualityGate[]> {
this.logger.log(`POST /workspaces/${workspaceId}/quality-gates/seed-defaults`);
return this.qualityGateConfigService.seedDefaults(workspaceId);
}
}

View File

@@ -0,0 +1,15 @@
import { Module } from "@nestjs/common";
import { QualityGateConfigController } from "./quality-gate-config.controller";
import { QualityGateConfigService } from "./quality-gate-config.service";
import { PrismaModule } from "../prisma/prisma.module";
/**
* Module for managing quality gate configurations
*/
@Module({
imports: [PrismaModule],
controllers: [QualityGateConfigController],
providers: [QualityGateConfigService],
exports: [QualityGateConfigService],
})
export class QualityGateConfigModule {}

View File

@@ -0,0 +1,362 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { QualityGateConfigService } from "./quality-gate-config.service";
import { PrismaService } from "../prisma/prisma.service";
import { NotFoundException, ConflictException } from "@nestjs/common";
describe("QualityGateConfigService", () => {
let service: QualityGateConfigService;
let prisma: PrismaService;
const mockPrismaService = {
qualityGate: {
create: vi.fn(),
findMany: vi.fn(),
findUnique: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
count: vi.fn(),
},
$transaction: vi.fn(),
};
const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440001";
const mockGateId = "550e8400-e29b-41d4-a716-446655440002";
const mockQualityGate = {
id: mockGateId,
workspaceId: mockWorkspaceId,
name: "Build Check",
description: "Verify code compiles without errors",
type: "build",
command: "pnpm build",
expectedOutput: null,
isRegex: false,
required: true,
order: 1,
isEnabled: true,
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
QualityGateConfigService,
{
provide: PrismaService,
useValue: mockPrismaService,
},
],
}).compile();
service = module.get<QualityGateConfigService>(QualityGateConfigService);
prisma = module.get<PrismaService>(PrismaService);
vi.clearAllMocks();
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("create", () => {
it("should create a quality gate successfully", async () => {
const createDto = {
name: "Build Check",
description: "Verify code compiles without errors",
type: "build" as const,
command: "pnpm build",
required: true,
order: 1,
};
mockPrismaService.qualityGate.create.mockResolvedValue(mockQualityGate);
const result = await service.create(mockWorkspaceId, createDto);
expect(result).toEqual(mockQualityGate);
expect(prisma.qualityGate.create).toHaveBeenCalledWith({
data: {
workspaceId: mockWorkspaceId,
name: createDto.name,
description: createDto.description,
type: createDto.type,
command: createDto.command,
expectedOutput: null,
isRegex: false,
required: true,
order: 1,
isEnabled: true,
},
});
});
it("should use default values when optional fields are not provided", async () => {
const createDto = {
name: "Test Gate",
type: "test" as const,
};
mockPrismaService.qualityGate.create.mockResolvedValue({
...mockQualityGate,
name: createDto.name,
type: createDto.type,
});
await service.create(mockWorkspaceId, createDto);
expect(prisma.qualityGate.create).toHaveBeenCalledWith({
data: {
workspaceId: mockWorkspaceId,
name: createDto.name,
description: null,
type: createDto.type,
command: null,
expectedOutput: null,
isRegex: false,
required: true,
order: 0,
isEnabled: true,
},
});
});
});
describe("findAll", () => {
it("should return all quality gates for a workspace", async () => {
const mockGates = [mockQualityGate];
mockPrismaService.qualityGate.findMany.mockResolvedValue(mockGates);
const result = await service.findAll(mockWorkspaceId);
expect(result).toEqual(mockGates);
expect(prisma.qualityGate.findMany).toHaveBeenCalledWith({
where: { workspaceId: mockWorkspaceId },
orderBy: { order: "asc" },
});
});
it("should return empty array when no gates exist", async () => {
mockPrismaService.qualityGate.findMany.mockResolvedValue([]);
const result = await service.findAll(mockWorkspaceId);
expect(result).toEqual([]);
});
});
describe("findEnabled", () => {
it("should return only enabled quality gates ordered by priority", async () => {
const mockGates = [mockQualityGate];
mockPrismaService.qualityGate.findMany.mockResolvedValue(mockGates);
const result = await service.findEnabled(mockWorkspaceId);
expect(result).toEqual(mockGates);
expect(prisma.qualityGate.findMany).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId,
isEnabled: true,
},
orderBy: { order: "asc" },
});
});
});
describe("findOne", () => {
it("should return a quality gate by id", async () => {
mockPrismaService.qualityGate.findUnique.mockResolvedValue(mockQualityGate);
const result = await service.findOne(mockWorkspaceId, mockGateId);
expect(result).toEqual(mockQualityGate);
expect(prisma.qualityGate.findUnique).toHaveBeenCalledWith({
where: {
id: mockGateId,
workspaceId: mockWorkspaceId,
},
});
});
it("should throw NotFoundException when gate does not exist", async () => {
mockPrismaService.qualityGate.findUnique.mockResolvedValue(null);
await expect(service.findOne(mockWorkspaceId, mockGateId)).rejects.toThrow(NotFoundException);
});
});
describe("update", () => {
it("should update a quality gate successfully", async () => {
const updateDto = {
name: "Updated Build Check",
required: false,
};
const updatedGate = {
...mockQualityGate,
...updateDto,
};
mockPrismaService.qualityGate.findUnique.mockResolvedValue(mockQualityGate);
mockPrismaService.qualityGate.update.mockResolvedValue(updatedGate);
const result = await service.update(mockWorkspaceId, mockGateId, updateDto);
expect(result).toEqual(updatedGate);
expect(prisma.qualityGate.update).toHaveBeenCalledWith({
where: { id: mockGateId },
data: updateDto,
});
});
it("should throw NotFoundException when gate does not exist", async () => {
const updateDto = { name: "Updated" };
mockPrismaService.qualityGate.findUnique.mockResolvedValue(null);
await expect(service.update(mockWorkspaceId, mockGateId, updateDto)).rejects.toThrow(
NotFoundException
);
});
});
describe("delete", () => {
it("should delete a quality gate successfully", async () => {
mockPrismaService.qualityGate.findUnique.mockResolvedValue(mockQualityGate);
mockPrismaService.qualityGate.delete.mockResolvedValue(mockQualityGate);
await service.delete(mockWorkspaceId, mockGateId);
expect(prisma.qualityGate.delete).toHaveBeenCalledWith({
where: { id: mockGateId },
});
});
it("should throw NotFoundException when gate does not exist", async () => {
mockPrismaService.qualityGate.findUnique.mockResolvedValue(null);
await expect(service.delete(mockWorkspaceId, mockGateId)).rejects.toThrow(NotFoundException);
});
});
describe("reorder", () => {
it("should reorder gates successfully", async () => {
const gateIds = ["gate1", "gate2", "gate3"];
const mockGates = gateIds.map((id, index) => ({
...mockQualityGate,
id,
order: index,
}));
mockPrismaService.$transaction.mockImplementation((callback) => {
return callback(mockPrismaService);
});
mockPrismaService.qualityGate.update.mockResolvedValue(mockGates[0]);
mockPrismaService.qualityGate.findMany.mockResolvedValue(mockGates);
const result = await service.reorder(mockWorkspaceId, gateIds);
expect(result).toEqual(mockGates);
expect(prisma.$transaction).toHaveBeenCalled();
});
});
describe("seedDefaults", () => {
it("should seed default quality gates for a workspace", async () => {
const mockDefaultGates = [
{
id: "1",
workspaceId: mockWorkspaceId,
name: "Build Check",
description: null,
type: "build",
command: "pnpm build",
expectedOutput: null,
isRegex: false,
required: true,
order: 1,
isEnabled: true,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "2",
workspaceId: mockWorkspaceId,
name: "Lint Check",
description: null,
type: "lint",
command: "pnpm lint",
expectedOutput: null,
isRegex: false,
required: true,
order: 2,
isEnabled: true,
createdAt: new Date(),
updatedAt: new Date(),
},
];
mockPrismaService.$transaction.mockImplementation((callback) => {
return callback(mockPrismaService);
});
mockPrismaService.qualityGate.create.mockImplementation((args) =>
Promise.resolve({
...mockDefaultGates[0],
...args.data,
})
);
mockPrismaService.qualityGate.findMany.mockResolvedValue(mockDefaultGates);
const result = await service.seedDefaults(mockWorkspaceId);
expect(result.length).toBeGreaterThan(0);
expect(prisma.$transaction).toHaveBeenCalled();
});
});
describe("toOrchestratorFormat", () => {
it("should convert database gates to orchestrator format", () => {
const gates = [mockQualityGate];
const result = service.toOrchestratorFormat(gates);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
id: mockQualityGate.id,
name: mockQualityGate.name,
description: mockQualityGate.description,
type: mockQualityGate.type,
command: mockQualityGate.command,
required: mockQualityGate.required,
order: mockQualityGate.order,
});
});
it("should handle regex patterns correctly", () => {
const gateWithRegex = {
...mockQualityGate,
expectedOutput: "Coverage: (\\d+)%",
isRegex: true,
};
const result = service.toOrchestratorFormat([gateWithRegex]);
expect(result[0]?.expectedOutput).toBeInstanceOf(RegExp);
});
it("should handle string patterns correctly", () => {
const gateWithString = {
...mockQualityGate,
expectedOutput: "All tests passed",
isRegex: false,
};
const result = service.toOrchestratorFormat([gateWithString]);
expect(typeof result[0]?.expectedOutput).toBe("string");
});
});
});

View 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;
});
}
}