feat(scenarios): add step title, scenario credentials, and environment context in executor
- add nullable title column to scenario steps; exposed in create/edit forms and step table - add scenario-credential join (many-to-many with CredentialEntity) with CRUD endpoints - expose environment URLs in code executor helpers via helpers.env and helpers.getEnvUrl() - resolve and cache environment per run from the login step's environmentName in scheduler - add section spacing and step table title column to ScenarioDetailPage
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { ConflictException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { ScenarioEntity } from "./scenario.entity";
|
||||
@@ -6,10 +6,13 @@ import { ScenarioStepEntity } from "./scenario-step.entity";
|
||||
import { ScenarioRunEntity } from "./scenario-run.entity";
|
||||
import { ScenarioRunStepEntity } from "./scenario-run-step.entity";
|
||||
import { ScenarioRunLogEntity } from "./scenario-run-log.entity";
|
||||
import { ScenarioCredentialEntity } from "./scenario-credential.entity";
|
||||
import { CredentialEntity } from "../credential/credential.entity";
|
||||
import { CreateScenarioDto } from "./dto/create-scenario.dto";
|
||||
import { UpdateScenarioDto } from "./dto/update-scenario.dto";
|
||||
import { CreateScenarioStepDto } from "./dto/create-scenario-step.dto";
|
||||
import { UpdateScenarioStepDto } from "./dto/update-scenario-step.dto";
|
||||
import { AddScenarioCredentialDto } from "./dto/add-scenario-credential.dto";
|
||||
import {
|
||||
PaginationQueryDto,
|
||||
PaginatedResult,
|
||||
@@ -33,6 +36,10 @@ export class ScenarioService {
|
||||
private readonly runStepRepo: Repository<ScenarioRunStepEntity>,
|
||||
@InjectRepository(ScenarioRunLogEntity)
|
||||
private readonly runLogRepo: Repository<ScenarioRunLogEntity>,
|
||||
@InjectRepository(ScenarioCredentialEntity)
|
||||
private readonly scenarioCredRepo: Repository<ScenarioCredentialEntity>,
|
||||
@InjectRepository(CredentialEntity)
|
||||
private readonly credentialRepo: Repository<CredentialEntity>,
|
||||
) {}
|
||||
|
||||
// ── Scenarios ─────────────────────────────────────────────────────────────
|
||||
@@ -59,7 +66,7 @@ export class ScenarioService {
|
||||
async findOne(id: number): Promise<ScenarioEntity> {
|
||||
const scenario = await this.scenarioRepo.findOne({
|
||||
where: { id },
|
||||
relations: ["steps"],
|
||||
relations: ["steps", "scenarioCredentials", "scenarioCredentials.credential"],
|
||||
order: { steps: { order: "ASC" } },
|
||||
});
|
||||
if (!scenario) throw new NotFoundException(`Scenario ${id} not found`);
|
||||
@@ -121,6 +128,84 @@ export class ScenarioService {
|
||||
await this.stepRepo.delete(stepId);
|
||||
}
|
||||
|
||||
// ── Scenario Credentials ──────────────────────────────────────────────────
|
||||
|
||||
async findScenarioCredentials(
|
||||
scenarioId: number,
|
||||
): Promise<ScenarioCredentialEntity[]> {
|
||||
await this.findOne(scenarioId); // 404 guard
|
||||
return this.scenarioCredRepo.find({
|
||||
where: { scenarioId },
|
||||
relations: ["credential"],
|
||||
order: { alias: "ASC" },
|
||||
});
|
||||
}
|
||||
|
||||
async addScenarioCredential(
|
||||
scenarioId: number,
|
||||
dto: AddScenarioCredentialDto,
|
||||
): Promise<ScenarioCredentialEntity> {
|
||||
await this.findOne(scenarioId); // 404 guard
|
||||
const credential = await this.credentialRepo.findOneBy({
|
||||
id: dto.credentialId,
|
||||
});
|
||||
if (!credential)
|
||||
throw new NotFoundException(`Credential ${dto.credentialId} not found`);
|
||||
const existing = await this.scenarioCredRepo.findOneBy({
|
||||
scenarioId,
|
||||
alias: dto.alias,
|
||||
});
|
||||
if (existing)
|
||||
throw new ConflictException(
|
||||
`Alias "${dto.alias}" already used in this scenario`,
|
||||
);
|
||||
const sc = this.scenarioCredRepo.create({
|
||||
scenarioId,
|
||||
credentialId: dto.credentialId,
|
||||
alias: dto.alias,
|
||||
});
|
||||
return this.scenarioCredRepo.save(sc);
|
||||
}
|
||||
|
||||
async removeScenarioCredential(
|
||||
scenarioId: number,
|
||||
scCredId: number,
|
||||
): Promise<void> {
|
||||
await this.findOne(scenarioId); // 404 guard
|
||||
const sc = await this.scenarioCredRepo.findOneBy({
|
||||
id: scCredId,
|
||||
scenarioId,
|
||||
});
|
||||
if (!sc)
|
||||
throw new NotFoundException(
|
||||
`Scenario credential ${scCredId} not found in scenario ${scenarioId}`,
|
||||
);
|
||||
await this.scenarioCredRepo.delete(scCredId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a map of alias → parsed credential data for use in code execution.
|
||||
* Returns null values for credentials with no data.
|
||||
*/
|
||||
async buildCredentialMap(scenarioId: number): Promise<Record<string, unknown>> {
|
||||
const scs = await this.findScenarioCredentials(scenarioId);
|
||||
const map: Record<string, unknown> = {};
|
||||
for (const sc of scs) {
|
||||
let parsed: unknown = null;
|
||||
if (sc.credential.data) {
|
||||
try {
|
||||
parsed = JSON.parse(sc.credential.data);
|
||||
} catch {
|
||||
parsed = sc.credential.data;
|
||||
}
|
||||
}
|
||||
map[sc.alias] = parsed;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// ── Runs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async findRuns(
|
||||
scenarioId: number,
|
||||
query: RunsQueryDto,
|
||||
|
||||
Reference in New Issue
Block a user