refactor(scenario): remove step type and simplify scheduler to exec-only
- remove StepType, type column, and type field from step entity and DTOs - remove executeLoginStep, executeSignStep, executeExecStep from scheduler - inline exec logic directly in executeStepRun; all steps run execCode - remove AuthService, EnvironmentService, runEnvironments from scheduler - remove type selector from CreateStepPage and EditStepPage - remove type badge column from ScenarioDetailPage - fix useEffect dependency arrays in RunDetailPage (FINAL, id, runId, polling) - fix duplicate /snippets proxy key in vite.config.ts - remove unused escapeHtml export from hljs.ts
This commit is contained in:
@@ -65,7 +65,7 @@ export class CodeExecutorService {
|
||||
const snippetMap: Record<string, string> = snippets ?? {};
|
||||
|
||||
// pageHelpers is referenced by runSnippet, so we declare it as a var first.
|
||||
// eslint-disable-next-line prefer-const
|
||||
|
||||
let pageHelpers: Record<string, unknown>;
|
||||
|
||||
const fakeConsole = {
|
||||
@@ -109,7 +109,10 @@ export class CodeExecutorService {
|
||||
*
|
||||
* Example: await helpers.runSnippet('clickLoginButton', '#submit')
|
||||
*/
|
||||
runSnippet: async (name: string, ...args: unknown[]): Promise<unknown> => {
|
||||
runSnippet: async (
|
||||
name: string,
|
||||
...args: unknown[]
|
||||
): Promise<unknown> => {
|
||||
const snippetCode = snippetMap[name];
|
||||
if (snippetCode == null) {
|
||||
throw new Error(`Snippet "${name}" not found`);
|
||||
@@ -136,7 +139,13 @@ export class CodeExecutorService {
|
||||
`return (async (page, context, helpers) => { ${code} })(page, context, helpers)`,
|
||||
);
|
||||
this.logger.debug("Executing user code");
|
||||
const execResult = await fn(page, context, pageHelpers, fakeConsole, result);
|
||||
const execResult = await fn(
|
||||
page,
|
||||
context,
|
||||
pageHelpers,
|
||||
fakeConsole,
|
||||
result,
|
||||
);
|
||||
return { result: execResult };
|
||||
} catch (err) {
|
||||
throw new InternalServerErrorException(
|
||||
|
||||
@@ -10,7 +10,12 @@ import {
|
||||
PaginatedResult,
|
||||
} from "../common/dto/pagination.dto";
|
||||
|
||||
export type CredentialOrderBy = "id" | "name" | "lastUsedAt" | "createdAt" | "updatedAt";
|
||||
export type CredentialOrderBy =
|
||||
| "id"
|
||||
| "name"
|
||||
| "lastUsedAt"
|
||||
| "createdAt"
|
||||
| "updatedAt";
|
||||
|
||||
@Injectable()
|
||||
export class CredentialService {
|
||||
@@ -44,7 +49,10 @@ export class CredentialService {
|
||||
return credential;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateCredentialDto): Promise<CredentialEntity> {
|
||||
async update(
|
||||
id: string,
|
||||
dto: UpdateCredentialDto,
|
||||
): Promise<CredentialEntity> {
|
||||
const credential = await this.findOne(id);
|
||||
Object.assign(credential, dto);
|
||||
return this.repo.save(credential);
|
||||
@@ -56,7 +64,12 @@ export class CredentialService {
|
||||
}
|
||||
|
||||
exportCredential(credential: CredentialEntity): CredentialExportDto {
|
||||
return { kind: "credential", id: credential.id, name: credential.name, data: credential.data };
|
||||
return {
|
||||
kind: "credential",
|
||||
id: credential.id,
|
||||
name: credential.name,
|
||||
data: credential.data,
|
||||
};
|
||||
}
|
||||
|
||||
async importCredential(dto: CredentialExportDto): Promise<CredentialEntity> {
|
||||
@@ -67,7 +80,11 @@ export class CredentialService {
|
||||
return this.repo.save(existing);
|
||||
}
|
||||
return this.repo.save(
|
||||
this.repo.create({ id: dto.id, name: dto.name, data: dto.data ?? null }),
|
||||
this.repo.create({
|
||||
id: dto.id,
|
||||
name: dto.name,
|
||||
data: dto.data ?? null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return this.repo.save(
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
|
||||
import {
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from "class-validator";
|
||||
|
||||
export class CredentialExportDto {
|
||||
@ApiPropertyOptional()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IsNotEmpty, IsString, IsUUID } from "class-validator";
|
||||
export class AddScenarioCredentialDto {
|
||||
@ApiProperty({ example: "uuid-here" })
|
||||
@IsUUID()
|
||||
credentialId: string
|
||||
credentialId: string;
|
||||
@ApiProperty({ example: "api_key" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import {
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
} from "class-validator";
|
||||
import { StepType } from "../scenario-step.entity";
|
||||
import { IsInt, IsNotEmpty, IsOptional, IsString, Min } from "class-validator";
|
||||
|
||||
export class CreateScenarioStepDto {
|
||||
@ApiPropertyOptional({ example: "Check login page title" })
|
||||
@@ -20,12 +12,9 @@ export class CreateScenarioStepDto {
|
||||
@Min(0)
|
||||
order: number;
|
||||
|
||||
@ApiProperty({ enum: ["login", "exec", "sign"], example: "exec" })
|
||||
@IsIn(["login", "exec", "sign"])
|
||||
type: StepType;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: "Session name (deprecated — browser is created automatically per run).",
|
||||
description:
|
||||
"Session name (deprecated — browser is created automatically per run).",
|
||||
example: "my-session",
|
||||
})
|
||||
@IsOptional()
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { StepType } from "../scenario-step.entity";
|
||||
|
||||
export class ScenarioStepExportDto {
|
||||
@ApiProperty()
|
||||
@@ -19,10 +17,6 @@ export class ScenarioStepExportDto {
|
||||
@Min(0)
|
||||
order: number;
|
||||
|
||||
@ApiProperty({ enum: ["login", "exec", "sign"] })
|
||||
@IsIn(["login", "exec", "sign"])
|
||||
type: StepType;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import {
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
} from "class-validator";
|
||||
import { StepType } from "../scenario-step.entity";
|
||||
import { IsInt, IsNotEmpty, IsOptional, IsString, Min } from "class-validator";
|
||||
|
||||
export class UpdateScenarioStepDto {
|
||||
@ApiPropertyOptional({ example: "Check login page title" })
|
||||
@@ -21,11 +13,6 @@ export class UpdateScenarioStepDto {
|
||||
@Min(0)
|
||||
order?: number;
|
||||
|
||||
@ApiPropertyOptional({ enum: ["login", "exec", "sign"] })
|
||||
@IsOptional()
|
||||
@IsIn(["login", "exec", "sign"])
|
||||
type?: StepType;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -12,11 +12,8 @@ import { ScenarioRunStepEntity } from "./scenario-run-step.entity";
|
||||
import { ScenarioRunLogEntity } from "./scenario-run-log.entity";
|
||||
import { ScenarioStepEntity } from "./scenario-step.entity";
|
||||
import type { ScriptLogger } from "../code-executor/code-executor.service";
|
||||
import { AuthService } from "../auth/auth.service";
|
||||
import { CodeExecutorService } from "../code-executor/code-executor.service";
|
||||
import { ScenarioService } from "./scenario.service";
|
||||
import { EnvironmentService } from "../environment/environment.service";
|
||||
import type { EnvironmentUrls } from "../environment/environment.entity";
|
||||
import { SnippetService } from "../snippet/snippet.service";
|
||||
|
||||
interface ValidateResult {
|
||||
@@ -37,8 +34,6 @@ export class ScenarioSchedulerService {
|
||||
private readonly runBrowsers = new Map<string, BrowserHandle>();
|
||||
// Cache credential maps per run (built once when a run starts)
|
||||
private readonly runCredentials = new Map<string, Record<string, unknown>>();
|
||||
// Cache environment URLs per run (resolved from the first login step)
|
||||
private readonly runEnvironments = new Map<string, EnvironmentUrls>();
|
||||
// Cache snippet code map per run (built once when a run starts)
|
||||
private readonly runSnippets = new Map<string, Record<string, string>>();
|
||||
|
||||
@@ -49,10 +44,8 @@ export class ScenarioSchedulerService {
|
||||
private readonly runStepRepo: Repository<ScenarioRunStepEntity>,
|
||||
@InjectRepository(ScenarioRunLogEntity)
|
||||
private readonly runLogRepo: Repository<ScenarioRunLogEntity>,
|
||||
private readonly authService: AuthService,
|
||||
private readonly codeExecutor: CodeExecutorService,
|
||||
private readonly scenarioService: ScenarioService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly snippetService: SnippetService,
|
||||
) {}
|
||||
|
||||
@@ -92,30 +85,6 @@ export class ScenarioSchedulerService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first login step for a scenario and returns the environment URLs
|
||||
* for the environment named in that step's execCode. Returns null if there is
|
||||
* no login step or the environment cannot be found.
|
||||
*/
|
||||
private async resolveRunEnvironment(
|
||||
scenarioId: string,
|
||||
): Promise<EnvironmentUrls | null> {
|
||||
try {
|
||||
const scenario = await this.scenarioService.findOne(scenarioId);
|
||||
const loginStep = scenario.steps.find((s) => s.type === "login");
|
||||
if (!loginStep?.execCode) return null;
|
||||
const params = JSON.parse(loginStep.execCode) as {
|
||||
environmentName?: string;
|
||||
};
|
||||
if (!params.environmentName) return null;
|
||||
const { data } = await this.environmentService.findAll({});
|
||||
const env = data.find((e) => e.name === params.environmentName);
|
||||
return env?.urls ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Job: pick up pending runs and process each to completion ─────────────
|
||||
|
||||
@Interval(1000)
|
||||
@@ -130,15 +99,12 @@ export class ScenarioSchedulerService {
|
||||
// Pre-load credential map for the scenario
|
||||
const credMap = await this.scenarioService
|
||||
.buildCredentialMap(run.scenarioId)
|
||||
.catch(() => ({} as Record<string, unknown>));
|
||||
.catch(() => ({}) as Record<string, unknown>);
|
||||
this.runCredentials.set(run.id, credMap);
|
||||
// Resolve environment from the first login step (best-effort)
|
||||
const envUrls = await this.resolveRunEnvironment(run.scenarioId);
|
||||
if (envUrls) this.runEnvironments.set(run.id, envUrls);
|
||||
// Pre-load snippet map
|
||||
const snippetMap = await this.snippetService
|
||||
.buildSnippetMap()
|
||||
.catch(() => ({} as Record<string, string>));
|
||||
.catch(() => ({}) as Record<string, string>);
|
||||
this.runSnippets.set(run.id, snippetMap);
|
||||
const traceId = crypto.randomUUID();
|
||||
void traceStorage.run({ traceId }, () =>
|
||||
@@ -176,7 +142,6 @@ export class ScenarioSchedulerService {
|
||||
} finally {
|
||||
this.activeRuns.delete(runId);
|
||||
this.runCredentials.delete(runId);
|
||||
this.runEnvironments.delete(runId);
|
||||
this.runSnippets.delete(runId);
|
||||
}
|
||||
}
|
||||
@@ -191,13 +156,45 @@ export class ScenarioSchedulerService {
|
||||
);
|
||||
|
||||
try {
|
||||
if (step.type === "login") {
|
||||
await this.executeLoginStep(stepRun, step);
|
||||
} else if (step.type === "sign") {
|
||||
await this.executeSignStep(stepRun, step);
|
||||
} else {
|
||||
await this.executeExecStep(stepRun, step);
|
||||
if (!step.execCode) throw new Error("step has no execCode");
|
||||
this.codeExecutor.validate(step.execCode);
|
||||
const { page, context } = await this.getOrCreateBrowserHandle(
|
||||
stepRun.runId,
|
||||
);
|
||||
const getStepOutput = this.makeGetStepOutput(stepRun);
|
||||
const creds = this.runCredentials.get(stepRun.runId);
|
||||
const snips = this.runSnippets.get(stepRun.runId);
|
||||
const { result: execOutput } = await this.codeExecutor.execute(
|
||||
page,
|
||||
context,
|
||||
step.execCode,
|
||||
this.stepLogger(stepRun.id, stepRun.runId),
|
||||
getStepOutput,
|
||||
creds,
|
||||
undefined,
|
||||
snips,
|
||||
);
|
||||
|
||||
if (step.validateCode) {
|
||||
this.codeExecutor.validate(step.validateCode);
|
||||
const { result } = await this.codeExecutor.execute(
|
||||
page,
|
||||
context,
|
||||
step.validateCode,
|
||||
this.stepLogger(stepRun.id, stepRun.runId),
|
||||
getStepOutput,
|
||||
creds,
|
||||
undefined,
|
||||
snips,
|
||||
execOutput,
|
||||
);
|
||||
const vr = this.parseValidateResult(result);
|
||||
if (!vr.success) throw new Error(vr.description ?? "Validation failed");
|
||||
await this.passStepRun(stepRun, vr.description ?? null, execOutput);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.passStepRun(stepRun, null, execOutput);
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message ?? String(err);
|
||||
this.logger.error(`StepRun #${stepRun.id} threw: ${msg}`);
|
||||
@@ -207,7 +204,9 @@ export class ScenarioSchedulerService {
|
||||
|
||||
// ── Shared browser per run ─────────────────────────────────────────────────
|
||||
|
||||
private async getOrCreateBrowserHandle(runId: string): Promise<BrowserHandle> {
|
||||
private async getOrCreateBrowserHandle(
|
||||
runId: string,
|
||||
): Promise<BrowserHandle> {
|
||||
const existing = this.runBrowsers.get(runId);
|
||||
if (existing) return existing;
|
||||
|
||||
@@ -238,154 +237,6 @@ export class ScenarioSchedulerService {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Login step ─────────────────────────────────────────────────────────────
|
||||
|
||||
private async executeLoginStep(
|
||||
stepRun: ScenarioRunStepEntity,
|
||||
step: ScenarioStepEntity,
|
||||
): Promise<void> {
|
||||
// execCode must be a JSON object: { "keyId": "...", "environmentName": "..." }
|
||||
let params: { keyId: string; environmentName: string };
|
||||
try {
|
||||
params = JSON.parse(step.execCode ?? "{}");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"login step execCode must be valid JSON with keyId and environmentName",
|
||||
);
|
||||
}
|
||||
if (!params.keyId || !params.environmentName) {
|
||||
throw new Error(
|
||||
"login step execCode must include keyId and environmentName",
|
||||
);
|
||||
}
|
||||
|
||||
const loginResult = await this.authService.login(
|
||||
params.keyId,
|
||||
params.environmentName,
|
||||
step.sessionName ?? undefined,
|
||||
);
|
||||
this.logger.log(
|
||||
`StepRun #${stepRun.id}: login OK, session=${loginResult.sessionName}`,
|
||||
);
|
||||
|
||||
if (step.validateCode) {
|
||||
const { page, context } = await this.getOrCreateBrowserHandle(
|
||||
stepRun.runId,
|
||||
);
|
||||
this.codeExecutor.validate(step.validateCode);
|
||||
const { result } = await this.codeExecutor.execute(
|
||||
page,
|
||||
context,
|
||||
step.validateCode,
|
||||
this.stepLogger(stepRun.id, stepRun.runId),
|
||||
this.makeGetStepOutput(stepRun),
|
||||
this.runCredentials.get(stepRun.runId),
|
||||
this.runEnvironments.get(stepRun.runId),
|
||||
this.runSnippets.get(stepRun.runId),
|
||||
null,
|
||||
);
|
||||
const vr = this.parseValidateResult(result);
|
||||
if (!vr.success) throw new Error(vr.description ?? "Validation failed");
|
||||
await this.passStepRun(stepRun, vr.description ?? null);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.passStepRun(stepRun, null);
|
||||
}
|
||||
|
||||
// ── Exec step ──────────────────────────────────────────────────────────────
|
||||
|
||||
private async executeExecStep(
|
||||
stepRun: ScenarioRunStepEntity,
|
||||
step: ScenarioStepEntity,
|
||||
): Promise<void> {
|
||||
if (!step.execCode) throw new Error("exec step has no execCode");
|
||||
|
||||
this.codeExecutor.validate(step.execCode);
|
||||
const { page, context } = await this.getOrCreateBrowserHandle(
|
||||
stepRun.runId,
|
||||
);
|
||||
const getStepOutput = this.makeGetStepOutput(stepRun);
|
||||
const creds = this.runCredentials.get(stepRun.runId);
|
||||
const env = this.runEnvironments.get(stepRun.runId);
|
||||
const snips = this.runSnippets.get(stepRun.runId);
|
||||
const { result: execOutput } = await this.codeExecutor.execute(
|
||||
page,
|
||||
context,
|
||||
step.execCode,
|
||||
this.stepLogger(stepRun.id, stepRun.runId),
|
||||
getStepOutput,
|
||||
creds,
|
||||
env,
|
||||
snips,
|
||||
);
|
||||
this.logger.log(`StepRun #${stepRun.id}: exec OK`);
|
||||
|
||||
if (step.validateCode) {
|
||||
this.codeExecutor.validate(step.validateCode);
|
||||
const { result } = await this.codeExecutor.execute(
|
||||
page,
|
||||
context,
|
||||
step.validateCode,
|
||||
this.stepLogger(stepRun.id, stepRun.runId),
|
||||
getStepOutput,
|
||||
creds,
|
||||
env,
|
||||
snips,
|
||||
execOutput,
|
||||
);
|
||||
const vr = this.parseValidateResult(result);
|
||||
if (!vr.success) throw new Error(vr.description ?? "Validation failed");
|
||||
await this.passStepRun(stepRun, vr.description ?? null, execOutput);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.passStepRun(stepRun, null, execOutput);
|
||||
}
|
||||
|
||||
// ── Sign step ──────────────────────────────────────────────────────────────
|
||||
|
||||
private async executeSignStep(
|
||||
stepRun: ScenarioRunStepEntity,
|
||||
step: ScenarioStepEntity,
|
||||
): Promise<void> {
|
||||
// execCode must be a JSON object: { "keyId": "..." }
|
||||
let params: { keyId: string };
|
||||
try {
|
||||
params = JSON.parse(step.execCode ?? "{}");
|
||||
} catch {
|
||||
throw new Error("sign step execCode must be valid JSON with keyId");
|
||||
}
|
||||
if (!params.keyId) throw new Error("sign step execCode must include keyId");
|
||||
|
||||
const { page, context } = await this.getOrCreateBrowserHandle(
|
||||
stepRun.runId,
|
||||
);
|
||||
await this.authService.signWithKey(params.keyId, page);
|
||||
this.logger.log(`StepRun #${stepRun.id}: sign OK`);
|
||||
|
||||
if (step.validateCode) {
|
||||
this.codeExecutor.validate(step.validateCode);
|
||||
const { result } = await this.codeExecutor.execute(
|
||||
page,
|
||||
context,
|
||||
step.validateCode,
|
||||
this.stepLogger(stepRun.id, stepRun.runId),
|
||||
this.makeGetStepOutput(stepRun),
|
||||
this.runCredentials.get(stepRun.runId),
|
||||
this.runEnvironments.get(stepRun.runId),
|
||||
this.runSnippets.get(stepRun.runId),
|
||||
null,
|
||||
);
|
||||
const vr = this.parseValidateResult(result);
|
||||
if (!vr.success) throw new Error(vr.description ?? "Validation failed");
|
||||
await this.passStepRun(stepRun, vr.description ?? null);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.passStepRun(stepRun, null);
|
||||
}
|
||||
|
||||
// ── Validation helper ──────────────────────────────────────────────────────
|
||||
|
||||
private parseValidateResult(raw: unknown): ValidateResult {
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
} from "typeorm";
|
||||
import { ScenarioEntity } from "./scenario.entity";
|
||||
|
||||
export type StepType = "login" | "exec" | "sign";
|
||||
|
||||
@Entity("scenario_steps")
|
||||
export class ScenarioStepEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
@@ -28,9 +26,6 @@ export class ScenarioStepEntity {
|
||||
@Column({ default: 0 })
|
||||
order: number;
|
||||
|
||||
@Column({ type: "text" })
|
||||
type: StepType;
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
title: string | null;
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@ export class ScenarioController {
|
||||
}
|
||||
|
||||
@Get("runs")
|
||||
@ApiOperation({ summary: "List runs across all scenarios (paginated, filterable by status)" })
|
||||
@ApiOperation({
|
||||
summary: "List runs across all scenarios (paginated, filterable by status)",
|
||||
})
|
||||
@ApiResponse({ status: 200 })
|
||||
findAllRuns(@Query() query: RunsQueryDto) {
|
||||
return this.scenarioService.findAllRuns(query);
|
||||
@@ -155,7 +157,10 @@ export class ScenarioController {
|
||||
@ApiOperation({ summary: "Add a credential to a scenario with an alias" })
|
||||
@ApiResponse({ status: 201, description: "Credential added" })
|
||||
@ApiResponse({ status: 404, description: "Scenario or credential not found" })
|
||||
@ApiResponse({ status: 409, description: "Alias already used in this scenario" })
|
||||
@ApiResponse({
|
||||
status: 409,
|
||||
description: "Alias already used in this scenario",
|
||||
})
|
||||
addScenarioCredential(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() dto: AddScenarioCredentialDto,
|
||||
@@ -167,7 +172,10 @@ export class ScenarioController {
|
||||
@HttpCode(204)
|
||||
@ApiOperation({ summary: "Remove a credential from a scenario" })
|
||||
@ApiResponse({ status: 204 })
|
||||
@ApiResponse({ status: 404, description: "Scenario or credential assignment not found" })
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: "Scenario or credential assignment not found",
|
||||
})
|
||||
removeScenarioCredential(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Param("scCredId", ParseUUIDPipe) scCredId: string,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { ConflictException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Like, Repository } from "typeorm";
|
||||
import { ScenarioEntity } from "./scenario.entity";
|
||||
@@ -66,7 +70,11 @@ export class ScenarioService {
|
||||
async findOne(id: string): Promise<ScenarioEntity> {
|
||||
const scenario = await this.scenarioRepo.findOne({
|
||||
where: { id },
|
||||
relations: ["steps", "scenarioCredentials", "scenarioCredentials.credential"],
|
||||
relations: [
|
||||
"steps",
|
||||
"scenarioCredentials",
|
||||
"scenarioCredentials.credential",
|
||||
],
|
||||
order: { steps: { order: "ASC" } },
|
||||
});
|
||||
if (!scenario) throw new NotFoundException(`Scenario ${id} not found`);
|
||||
@@ -187,7 +195,9 @@ export class ScenarioService {
|
||||
* Builds a map of alias → parsed credential data for use in code execution.
|
||||
* Returns null values for credentials with no data.
|
||||
*/
|
||||
async buildCredentialMap(scenarioId: string): Promise<Record<string, unknown>> {
|
||||
async buildCredentialMap(
|
||||
scenarioId: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const scs = await this.findScenarioCredentials(scenarioId);
|
||||
const map: Record<string, unknown> = {};
|
||||
for (const sc of scs) {
|
||||
@@ -227,7 +237,9 @@ export class ScenarioService {
|
||||
|
||||
async findAllRuns(
|
||||
query: RunsQueryDto,
|
||||
): Promise<PaginatedResult<ScenarioRunEntity & { scenario: ScenarioEntity }>> {
|
||||
): Promise<
|
||||
PaginatedResult<ScenarioRunEntity & { scenario: ScenarioEntity }>
|
||||
> {
|
||||
const page = query.page ?? 1;
|
||||
const limit = query.limit ?? 20;
|
||||
const where: Record<string, unknown> = {};
|
||||
|
||||
@@ -7,12 +7,16 @@ export class CreateSnippetDto {
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({ example: "Clicks the login button and waits for navigation" })
|
||||
@ApiPropertyOptional({
|
||||
example: "Clicks the login button and waits for navigation",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ example: "await page.click('#login-btn');\nawait page.waitForNavigation();" })
|
||||
@ApiProperty({
|
||||
example: "await page.click('#login-btn');\nawait page.waitForNavigation();",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
|
||||
import {
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from "class-validator";
|
||||
|
||||
export class SnippetExportDto {
|
||||
@ApiPropertyOptional()
|
||||
|
||||
@@ -58,7 +58,8 @@ export class SnippetService {
|
||||
const snippet = await this.findOne(id);
|
||||
if (dto.name && dto.name !== snippet.name) {
|
||||
const conflict = await this.repo.findOneBy({ name: dto.name });
|
||||
if (conflict) throw new ConflictException(`Snippet "${dto.name}" already exists`);
|
||||
if (conflict)
|
||||
throw new ConflictException(`Snippet "${dto.name}" already exists`);
|
||||
}
|
||||
Object.assign(snippet, dto);
|
||||
return this.repo.save(snippet);
|
||||
|
||||
Reference in New Issue
Block a user