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:
2026-04-09 23:37:21 +03:00
parent 1f3a604940
commit 1efbbb38a3
34 changed files with 1362 additions and 14 deletions
@@ -0,0 +1,56 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { CredentialEntity } from "./credential.entity";
import { CreateCredentialDto } from "./dto/create-credential.dto";
import { UpdateCredentialDto } from "./dto/update-credential.dto";
import {
PaginationQueryDto,
PaginatedResult,
} from "../common/dto/pagination.dto";
export type CredentialOrderBy = "id" | "name" | "lastUsedAt" | "createdAt" | "updatedAt";
@Injectable()
export class CredentialService {
constructor(
@InjectRepository(CredentialEntity)
private readonly repo: Repository<CredentialEntity>,
) {}
async create(dto: CreateCredentialDto): Promise<CredentialEntity> {
return this.repo.save(this.repo.create({ ...dto, data: dto.data ?? null }));
}
async findAll(
query: PaginationQueryDto<CredentialOrderBy> = {},
): Promise<PaginatedResult<CredentialEntity>> {
const page = query.page ?? 1;
const limit = query.limit ?? 20;
const orderBy = query.orderBy ?? "id";
const orderDir = query.orderDir ?? "ASC";
const [data, total] = await this.repo.findAndCount({
order: { [orderBy]: orderDir },
skip: (page - 1) * limit,
take: limit,
});
return { data, total, page, limit };
}
async findOne(id: number): Promise<CredentialEntity> {
const credential = await this.repo.findOneBy({ id });
if (!credential) throw new NotFoundException(`Credential ${id} not found`);
return credential;
}
async update(id: number, dto: UpdateCredentialDto): Promise<CredentialEntity> {
const credential = await this.findOne(id);
Object.assign(credential, dto);
return this.repo.save(credential);
}
async remove(id: number): Promise<void> {
await this.findOne(id);
await this.repo.delete(id);
}
}