feat(export-import): add yaml export/import with id upsert for credentials, snippets, and scenarios
- all entities export a kind field (credential/snippet/scenario) for safe type checking on import - import upserts by id: overwrites if id exists, creates with explicit id otherwise - scenario export now includes id and step ids; import deletes old steps before recreating - add GET /:id/export and POST /import endpoints to credential and snippet controllers - add UuidBadge ui component: shortens uuid to first+last 4 hex chars, tooltip, clipboard copy with animated ClipboardCheck icon pop - apply UuidBadge across all entity id display sites (detail pages, card footers, table columns) - add export/import buttons to CredentialsPage, SnippetsPage, CredentialDetailPage, SnippetDetailPage
This commit is contained in:
@@ -63,7 +63,7 @@ export class ScenarioService {
|
||||
return { data, total, page, limit };
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<ScenarioEntity> {
|
||||
async findOne(id: string): Promise<ScenarioEntity> {
|
||||
const scenario = await this.scenarioRepo.findOne({
|
||||
where: { id },
|
||||
relations: ["steps", "scenarioCredentials", "scenarioCredentials.credential"],
|
||||
@@ -73,13 +73,13 @@ export class ScenarioService {
|
||||
return scenario;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateScenarioDto): Promise<ScenarioEntity> {
|
||||
async update(id: string, dto: UpdateScenarioDto): Promise<ScenarioEntity> {
|
||||
const scenario = await this.findOne(id);
|
||||
Object.assign(scenario, dto);
|
||||
return this.scenarioRepo.save(scenario);
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<void> {
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findOne(id);
|
||||
await this.scenarioRepo.delete(id);
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export class ScenarioService {
|
||||
// ── Steps ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async createStep(
|
||||
scenarioId: number,
|
||||
scenarioId: string,
|
||||
dto: CreateScenarioStepDto,
|
||||
): Promise<ScenarioStepEntity> {
|
||||
await this.findOne(scenarioId);
|
||||
@@ -102,8 +102,8 @@ export class ScenarioService {
|
||||
}
|
||||
|
||||
async findStep(
|
||||
scenarioId: number,
|
||||
stepId: number,
|
||||
scenarioId: string,
|
||||
stepId: string,
|
||||
): Promise<ScenarioStepEntity> {
|
||||
const step = await this.stepRepo.findOneBy({ id: stepId, scenarioId });
|
||||
if (!step)
|
||||
@@ -114,8 +114,8 @@ export class ScenarioService {
|
||||
}
|
||||
|
||||
async updateStep(
|
||||
scenarioId: number,
|
||||
stepId: number,
|
||||
scenarioId: string,
|
||||
stepId: string,
|
||||
dto: UpdateScenarioStepDto,
|
||||
): Promise<ScenarioStepEntity> {
|
||||
const step = await this.findStep(scenarioId, stepId);
|
||||
@@ -123,7 +123,7 @@ export class ScenarioService {
|
||||
return this.stepRepo.save(step);
|
||||
}
|
||||
|
||||
async removeStep(scenarioId: number, stepId: number): Promise<void> {
|
||||
async removeStep(scenarioId: string, stepId: string): Promise<void> {
|
||||
await this.findStep(scenarioId, stepId);
|
||||
await this.stepRepo.delete(stepId);
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export class ScenarioService {
|
||||
// ── Scenario Credentials ──────────────────────────────────────────────────
|
||||
|
||||
async findScenarioCredentials(
|
||||
scenarioId: number,
|
||||
scenarioId: string,
|
||||
): Promise<ScenarioCredentialEntity[]> {
|
||||
await this.findOne(scenarioId); // 404 guard
|
||||
return this.scenarioCredRepo.find({
|
||||
@@ -142,7 +142,7 @@ export class ScenarioService {
|
||||
}
|
||||
|
||||
async addScenarioCredential(
|
||||
scenarioId: number,
|
||||
scenarioId: string,
|
||||
dto: AddScenarioCredentialDto,
|
||||
): Promise<ScenarioCredentialEntity> {
|
||||
await this.findOne(scenarioId); // 404 guard
|
||||
@@ -168,8 +168,8 @@ export class ScenarioService {
|
||||
}
|
||||
|
||||
async removeScenarioCredential(
|
||||
scenarioId: number,
|
||||
scCredId: number,
|
||||
scenarioId: string,
|
||||
scCredId: string,
|
||||
): Promise<void> {
|
||||
await this.findOne(scenarioId); // 404 guard
|
||||
const sc = await this.scenarioCredRepo.findOneBy({
|
||||
@@ -187,7 +187,7 @@ 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: number): 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) {
|
||||
@@ -207,7 +207,7 @@ export class ScenarioService {
|
||||
// ── Runs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async findRuns(
|
||||
scenarioId: number,
|
||||
scenarioId: string,
|
||||
query: RunsQueryDto,
|
||||
): Promise<PaginatedResult<ScenarioRunEntity>> {
|
||||
await this.findOne(scenarioId); // 404 guard
|
||||
@@ -245,8 +245,8 @@ export class ScenarioService {
|
||||
}
|
||||
|
||||
async findRun(
|
||||
scenarioId: number,
|
||||
runId: number,
|
||||
scenarioId: string,
|
||||
runId: string,
|
||||
): Promise<ScenarioRunEntity & { logs: ScenarioRunLogEntity[] }> {
|
||||
await this.findOne(scenarioId); // 404 guard
|
||||
const run = await this.runRepo.findOne({
|
||||
@@ -266,8 +266,8 @@ export class ScenarioService {
|
||||
}
|
||||
|
||||
async waitForRun(
|
||||
scenarioId: number,
|
||||
runId: number,
|
||||
scenarioId: string,
|
||||
runId: string,
|
||||
timeoutMs = 300_000,
|
||||
): Promise<ScenarioRunEntity & { logs: ScenarioRunLogEntity[] }> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
@@ -285,7 +285,7 @@ export class ScenarioService {
|
||||
return this.findRun(scenarioId, runId);
|
||||
}
|
||||
|
||||
async createRun(scenarioId: number): Promise<ScenarioRunEntity> {
|
||||
async createRun(scenarioId: string): Promise<ScenarioRunEntity> {
|
||||
const scenario = await this.findOne(scenarioId);
|
||||
|
||||
const run = await this.runRepo.save(
|
||||
@@ -313,14 +313,16 @@ export class ScenarioService {
|
||||
|
||||
// ── Export / Import ───────────────────────────────────────────────────────
|
||||
|
||||
async exportScenario(id: number): Promise<ScenarioExportDto> {
|
||||
async exportScenario(id: string): Promise<ScenarioExportDto> {
|
||||
const scenario = await this.findOne(id);
|
||||
return {
|
||||
kind: "scenario",
|
||||
id: scenario.id,
|
||||
name: scenario.name,
|
||||
steps: scenario.steps.map((s) => ({
|
||||
order: s.order,
|
||||
type: s.type,
|
||||
sessionName: s.sessionName,
|
||||
title: s.title,
|
||||
execCode: s.execCode,
|
||||
validateCode: s.validateCode,
|
||||
})),
|
||||
@@ -328,16 +330,32 @@ export class ScenarioService {
|
||||
}
|
||||
|
||||
async importScenario(dto: ScenarioExportDto): Promise<ScenarioEntity> {
|
||||
const scenario = await this.scenarioRepo.save(
|
||||
this.scenarioRepo.create({ name: dto.name }),
|
||||
);
|
||||
// Upsert: if id provided and entity exists, replace steps; else create new
|
||||
let scenario: ScenarioEntity;
|
||||
if (dto.id) {
|
||||
const existing = await this.scenarioRepo.findOneBy({ id: dto.id });
|
||||
if (existing) {
|
||||
existing.name = dto.name;
|
||||
scenario = await this.scenarioRepo.save(existing);
|
||||
// Delete old steps and recreate
|
||||
await this.stepRepo.delete({ scenarioId: scenario.id });
|
||||
} else {
|
||||
scenario = await this.scenarioRepo.save(
|
||||
this.scenarioRepo.create({ id: dto.id, name: dto.name }),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
scenario = await this.scenarioRepo.save(
|
||||
this.scenarioRepo.create({ name: dto.name }),
|
||||
);
|
||||
}
|
||||
if (dto.steps.length > 0) {
|
||||
const steps = dto.steps.map((s) =>
|
||||
this.stepRepo.create({
|
||||
scenarioId: scenario.id,
|
||||
order: s.order,
|
||||
type: s.type,
|
||||
sessionName: s.sessionName,
|
||||
title: s.title ?? null,
|
||||
execCode: s.execCode ?? null,
|
||||
validateCode: s.validateCode ?? null,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user