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:
@@ -5,7 +5,7 @@ import {
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
ParseUUIDPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
@@ -14,6 +14,7 @@ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { CredentialService } from "./credential.service";
|
||||
import { CreateCredentialDto } from "./dto/create-credential.dto";
|
||||
import { UpdateCredentialDto } from "./dto/update-credential.dto";
|
||||
import { CredentialExportDto } from "./dto/credential-export.dto";
|
||||
import { PaginationQueryDto } from "../common/dto/pagination.dto";
|
||||
import { CredentialOrderBy } from "./credential.service";
|
||||
|
||||
@@ -29,6 +30,13 @@ export class CredentialController {
|
||||
return this.credentialService.create(dto);
|
||||
}
|
||||
|
||||
@Post("import")
|
||||
@ApiOperation({ summary: "Import a credential (upsert by id)" })
|
||||
@ApiResponse({ status: 201, description: "Credential imported" })
|
||||
async importCredential(@Body() dto: CredentialExportDto) {
|
||||
return this.credentialService.importCredential(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: "List all credentials (paginated)" })
|
||||
@ApiResponse({ status: 200, description: "Paginated credentials" })
|
||||
@@ -36,11 +44,20 @@ export class CredentialController {
|
||||
return this.credentialService.findAll(query);
|
||||
}
|
||||
|
||||
@Get(":id/export")
|
||||
@ApiOperation({ summary: "Export a credential as a plain object" })
|
||||
@ApiResponse({ status: 200, description: "Credential export payload" })
|
||||
@ApiResponse({ status: 404, description: "Credential not found" })
|
||||
async exportCredential(@Param("id", ParseUUIDPipe) id: string) {
|
||||
const credential = await this.credentialService.findOne(id);
|
||||
return this.credentialService.exportCredential(credential);
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@ApiOperation({ summary: "Get credential by ID" })
|
||||
@ApiResponse({ status: 200, description: "Credential record" })
|
||||
@ApiResponse({ status: 404, description: "Credential not found" })
|
||||
findOne(@Param("id", ParseIntPipe) id: number) {
|
||||
findOne(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.credentialService.findOne(id);
|
||||
}
|
||||
|
||||
@@ -49,7 +66,7 @@ export class CredentialController {
|
||||
@ApiResponse({ status: 200, description: "Credential updated" })
|
||||
@ApiResponse({ status: 404, description: "Credential not found" })
|
||||
update(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateCredentialDto,
|
||||
) {
|
||||
return this.credentialService.update(id, dto);
|
||||
@@ -60,7 +77,7 @@ export class CredentialController {
|
||||
@ApiOperation({ summary: "Delete a credential" })
|
||||
@ApiResponse({ status: 204, description: "Credential deleted" })
|
||||
@ApiResponse({ status: 404, description: "Credential not found" })
|
||||
remove(@Param("id", ParseIntPipe) id: number) {
|
||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.credentialService.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
|
||||
@Entity("credentials")
|
||||
export class CredentialEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Repository } from "typeorm";
|
||||
import { CredentialEntity } from "./credential.entity";
|
||||
import { CreateCredentialDto } from "./dto/create-credential.dto";
|
||||
import { UpdateCredentialDto } from "./dto/update-credential.dto";
|
||||
import { CredentialExportDto } from "./dto/credential-export.dto";
|
||||
import {
|
||||
PaginationQueryDto,
|
||||
PaginatedResult,
|
||||
@@ -37,20 +38,40 @@ export class CredentialService {
|
||||
return { data, total, page, limit };
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<CredentialEntity> {
|
||||
async findOne(id: string): 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> {
|
||||
async update(id: string, 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> {
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findOne(id);
|
||||
await this.repo.delete(id);
|
||||
}
|
||||
|
||||
exportCredential(credential: CredentialEntity): CredentialExportDto {
|
||||
return { kind: "credential", id: credential.id, name: credential.name, data: credential.data };
|
||||
}
|
||||
|
||||
async importCredential(dto: CredentialExportDto): Promise<CredentialEntity> {
|
||||
if (dto.id) {
|
||||
const existing = await this.repo.findOneBy({ id: dto.id });
|
||||
if (existing) {
|
||||
Object.assign(existing, { name: dto.name, data: dto.data ?? null });
|
||||
return this.repo.save(existing);
|
||||
}
|
||||
return this.repo.save(
|
||||
this.repo.create({ id: dto.id, name: dto.name, data: dto.data ?? null }),
|
||||
);
|
||||
}
|
||||
return this.repo.save(
|
||||
this.repo.create({ name: dto.name, data: dto.data ?? null }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
|
||||
|
||||
export class CredentialExportDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsIn(["credential"])
|
||||
kind?: "credential";
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
id?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
data?: string | null;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
ParseUUIDPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
@@ -41,7 +41,7 @@ export class EnvironmentController {
|
||||
@ApiOperation({ summary: "Get environment by ID" })
|
||||
@ApiResponse({ status: 200, description: "Environment record" })
|
||||
@ApiResponse({ status: 404, description: "Environment not found" })
|
||||
findOne(@Param("id", ParseIntPipe) id: number) {
|
||||
findOne(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.environmentService.findOne(id);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export class EnvironmentController {
|
||||
@ApiResponse({ status: 200, description: "Environment updated" })
|
||||
@ApiResponse({ status: 404, description: "Environment not found" })
|
||||
update(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateEnvironmentDto,
|
||||
) {
|
||||
return this.environmentService.update(id, dto);
|
||||
@@ -61,7 +61,7 @@ export class EnvironmentController {
|
||||
@ApiOperation({ summary: "Delete an environment" })
|
||||
@ApiResponse({ status: 204, description: "Environment deleted" })
|
||||
@ApiResponse({ status: 404, description: "Environment not found" })
|
||||
remove(@Param("id", ParseIntPipe) id: number) {
|
||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.environmentService.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ export interface EnvironmentUrls {
|
||||
|
||||
@Entity("environments")
|
||||
export class EnvironmentEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
name: string;
|
||||
|
||||
@@ -45,14 +45,14 @@ export class EnvironmentService {
|
||||
return { data, total, page, limit };
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<EnvironmentEntity> {
|
||||
async findOne(id: string): Promise<EnvironmentEntity> {
|
||||
const env = await this.repo.findOneBy({ id });
|
||||
if (!env) throw new NotFoundException(`Environment ${id} not found`);
|
||||
return env;
|
||||
}
|
||||
|
||||
async update(
|
||||
id: number,
|
||||
id: string,
|
||||
dto: UpdateEnvironmentDto,
|
||||
): Promise<EnvironmentEntity> {
|
||||
const env = await this.findOne(id);
|
||||
@@ -60,7 +60,7 @@ export class EnvironmentService {
|
||||
return this.repo.save(env);
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<void> {
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findOne(id);
|
||||
await this.repo.delete(id);
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ export class McpService {
|
||||
{
|
||||
description: "Delete a session by numeric ID (closes it first if open)",
|
||||
inputSchema: {
|
||||
id: z.number().int().describe("Session ID to delete"),
|
||||
id: z.string().uuid().describe("Session ID to delete"),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
@@ -200,7 +200,7 @@ export class McpService {
|
||||
{
|
||||
description: "Get an environment record by ID",
|
||||
inputSchema: {
|
||||
id: z.number().int().describe("Environment ID"),
|
||||
id: z.string().uuid().describe("Environment ID"),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
@@ -258,7 +258,7 @@ export class McpService {
|
||||
{
|
||||
description: "Update an existing environment (name and/or urls)",
|
||||
inputSchema: {
|
||||
id: z.number().int().describe("Environment ID to update"),
|
||||
id: z.string().uuid().describe("Environment ID to update"),
|
||||
name: z.string().optional().describe("New name"),
|
||||
urls: z
|
||||
.record(z.string(), z.string())
|
||||
@@ -289,7 +289,7 @@ export class McpService {
|
||||
{
|
||||
description: "Delete an environment by ID",
|
||||
inputSchema: {
|
||||
id: z.number().int().describe("Environment ID to delete"),
|
||||
id: z.string().uuid().describe("Environment ID to delete"),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
@@ -443,7 +443,7 @@ export class McpService {
|
||||
{
|
||||
description: "Get a scenario with its steps by ID",
|
||||
inputSchema: {
|
||||
id: z.number().int().describe("Scenario ID"),
|
||||
id: z.string().uuid().describe("Scenario ID"),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
@@ -493,7 +493,7 @@ export class McpService {
|
||||
{
|
||||
description: "Update a scenario name",
|
||||
inputSchema: {
|
||||
id: z.number().int().describe("Scenario ID"),
|
||||
id: z.string().uuid().describe("Scenario ID"),
|
||||
name: z.string().optional().describe("New name"),
|
||||
},
|
||||
},
|
||||
@@ -519,7 +519,7 @@ export class McpService {
|
||||
{
|
||||
description: "Delete a scenario by ID",
|
||||
inputSchema: {
|
||||
id: z.number().int().describe("Scenario ID"),
|
||||
id: z.string().uuid().describe("Scenario ID"),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
@@ -544,7 +544,7 @@ export class McpService {
|
||||
{
|
||||
description: "Add a step to a scenario",
|
||||
inputSchema: {
|
||||
scenarioId: z.number().int().describe("Parent scenario ID"),
|
||||
scenarioId: z.string().uuid().describe("Parent scenario ID"),
|
||||
order: z
|
||||
.number()
|
||||
.int()
|
||||
@@ -582,8 +582,8 @@ export class McpService {
|
||||
{
|
||||
description: "Get a single step of a scenario",
|
||||
inputSchema: {
|
||||
scenarioId: z.number().int().describe("Scenario ID"),
|
||||
stepId: z.number().int().describe("Step ID"),
|
||||
scenarioId: z.string().uuid().describe("Scenario ID"),
|
||||
stepId: z.string().uuid().describe("Step ID"),
|
||||
},
|
||||
},
|
||||
async ({ scenarioId, stepId }) => {
|
||||
@@ -606,8 +606,8 @@ export class McpService {
|
||||
{
|
||||
description: "Update a step within a scenario",
|
||||
inputSchema: {
|
||||
scenarioId: z.number().int().describe("Scenario ID"),
|
||||
stepId: z.number().int().describe("Step ID"),
|
||||
scenarioId: z.string().uuid().describe("Scenario ID"),
|
||||
stepId: z.string().uuid().describe("Step ID"),
|
||||
order: z
|
||||
.number()
|
||||
.int()
|
||||
@@ -647,8 +647,8 @@ export class McpService {
|
||||
{
|
||||
description: "Delete a step from a scenario",
|
||||
inputSchema: {
|
||||
scenarioId: z.number().int().describe("Scenario ID"),
|
||||
stepId: z.number().int().describe("Step ID"),
|
||||
scenarioId: z.string().uuid().describe("Scenario ID"),
|
||||
stepId: z.string().uuid().describe("Step ID"),
|
||||
},
|
||||
},
|
||||
async ({ scenarioId, stepId }) => {
|
||||
@@ -677,7 +677,7 @@ export class McpService {
|
||||
description:
|
||||
"List runs for a scenario (paginated, optionally filtered by status)",
|
||||
inputSchema: {
|
||||
scenarioId: z.number().int().describe("Scenario ID"),
|
||||
scenarioId: z.string().uuid().describe("Scenario ID"),
|
||||
status: z
|
||||
.enum(["pending", "in_progress", "pass", "fail"])
|
||||
.optional()
|
||||
@@ -720,7 +720,7 @@ export class McpService {
|
||||
{
|
||||
description: "Trigger an immediate run of a scenario by ID",
|
||||
inputSchema: {
|
||||
id: z.number().int().describe("Scenario ID to run"),
|
||||
id: z.string().uuid().describe("Scenario ID to run"),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
@@ -744,8 +744,8 @@ export class McpService {
|
||||
description:
|
||||
"Get a specific scenario run with all step runs and their outputs",
|
||||
inputSchema: {
|
||||
scenarioId: z.number().int().describe("Scenario ID"),
|
||||
runId: z.number().int().describe("Run ID"),
|
||||
scenarioId: z.string().uuid().describe("Scenario ID"),
|
||||
runId: z.string().uuid().describe("Run ID"),
|
||||
},
|
||||
},
|
||||
async ({ scenarioId, runId }) => {
|
||||
@@ -769,8 +769,8 @@ export class McpService {
|
||||
description:
|
||||
"Block until a scenario run reaches pass or fail (max 5 min), then return the full run with step outputs",
|
||||
inputSchema: {
|
||||
scenarioId: z.number().int().describe("Scenario ID"),
|
||||
runId: z.number().int().describe("Run ID"),
|
||||
scenarioId: z.string().uuid().describe("Scenario ID"),
|
||||
runId: z.string().uuid().describe("Run ID"),
|
||||
},
|
||||
},
|
||||
async ({ scenarioId, runId }) => {
|
||||
@@ -794,7 +794,7 @@ export class McpService {
|
||||
description:
|
||||
"Export a scenario as a portable JSON payload (name + steps)",
|
||||
inputSchema: {
|
||||
id: z.number().int().describe("Scenario ID to export"),
|
||||
id: z.string().uuid().describe("Scenario ID to export"),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
@@ -826,7 +826,7 @@ export class McpService {
|
||||
z.object({
|
||||
order: z.number().int().min(0).describe("Execution order"),
|
||||
type: z.enum(["login", "exec", "sign"]).describe("Step type"),
|
||||
sessionName: z.string().describe("Session name"),
|
||||
title: z.string().nullable().optional().describe("Step title"),
|
||||
execCode: z
|
||||
.string()
|
||||
.nullable()
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsInt, IsNotEmpty, IsPositive, IsString } from "class-validator";
|
||||
import { IsNotEmpty, IsString, IsUUID } from "class-validator";
|
||||
|
||||
export class AddScenarioCredentialDto {
|
||||
@ApiProperty({ example: 1 })
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
credentialId: number;
|
||||
|
||||
@ApiProperty({ example: "uuid-here" })
|
||||
@IsUUID()
|
||||
credentialId: string
|
||||
@ApiProperty({ example: "api_key" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
@@ -22,10 +23,11 @@ export class ScenarioStepExportDto {
|
||||
@IsIn(["login", "exec", "sign"])
|
||||
type: StepType;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
sessionName: string | null;
|
||||
title: string | null;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@@ -41,6 +43,22 @@ export class ScenarioStepExportDto {
|
||||
}
|
||||
|
||||
export class ScenarioExportDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsIn(["scenario"])
|
||||
kind?: "scenario";
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
id?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -13,14 +13,14 @@ import { CredentialEntity } from "../credential/credential.entity";
|
||||
@Entity("scenario_credentials")
|
||||
@Unique(["scenarioId", "alias"])
|
||||
export class ScenarioCredentialEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
scenarioId: number;
|
||||
@Column("text")
|
||||
scenarioId: string;
|
||||
|
||||
@Column()
|
||||
credentialId: number;
|
||||
@Column("text")
|
||||
credentialId: string;
|
||||
|
||||
@Column()
|
||||
alias: string;
|
||||
|
||||
@@ -13,18 +13,18 @@ export type LogLevel = "log" | "warn" | "error";
|
||||
|
||||
@Entity("scenario_run_logs")
|
||||
export class ScenarioRunLogEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
runId: number;
|
||||
@Column("text")
|
||||
runId: string;
|
||||
|
||||
@ManyToOne(() => ScenarioRunEntity, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "runId" })
|
||||
run: ScenarioRunEntity;
|
||||
|
||||
@Column({ nullable: true })
|
||||
stepRunId: number | null;
|
||||
@Column({ type: "text", nullable: true })
|
||||
stepRunId: string | null;
|
||||
|
||||
@ManyToOne(() => ScenarioRunStepEntity, {
|
||||
onDelete: "SET NULL",
|
||||
|
||||
@@ -20,11 +20,11 @@ export type RunStepStatus =
|
||||
|
||||
@Entity("scenario_run_steps")
|
||||
export class ScenarioRunStepEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
runId: number;
|
||||
@Column("text")
|
||||
runId: string;
|
||||
|
||||
@ManyToOne(() => ScenarioRunEntity, (run) => run.stepRuns, {
|
||||
onDelete: "CASCADE",
|
||||
@@ -32,8 +32,8 @@ export class ScenarioRunStepEntity {
|
||||
@JoinColumn({ name: "runId" })
|
||||
run: ScenarioRunEntity;
|
||||
|
||||
@Column()
|
||||
scenarioStepId: number;
|
||||
@Column("text")
|
||||
scenarioStepId: string;
|
||||
|
||||
@ManyToOne(() => ScenarioStepEntity, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "scenarioStepId" })
|
||||
|
||||
@@ -15,11 +15,11 @@ export type RunStatus = "pending" | "in_progress" | "pass" | "fail";
|
||||
|
||||
@Entity("scenario_runs")
|
||||
export class ScenarioRunEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
scenarioId: number;
|
||||
@Column("text")
|
||||
scenarioId: string;
|
||||
|
||||
@ManyToOne(() => ScenarioEntity, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "scenarioId" })
|
||||
|
||||
@@ -33,14 +33,14 @@ interface BrowserHandle {
|
||||
@Injectable()
|
||||
export class ScenarioSchedulerService {
|
||||
private readonly logger = new TraceLogger(ScenarioSchedulerService.name);
|
||||
private readonly activeRuns = new Set<number>();
|
||||
private readonly runBrowsers = new Map<number, BrowserHandle>();
|
||||
private readonly activeRuns = new Set<string>();
|
||||
private readonly runBrowsers = new Map<string, BrowserHandle>();
|
||||
// Cache credential maps per run (built once when a run starts)
|
||||
private readonly runCredentials = new Map<number, Record<string, unknown>>();
|
||||
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<number, EnvironmentUrls>();
|
||||
private readonly runEnvironments = new Map<string, EnvironmentUrls>();
|
||||
// Cache snippet code map per run (built once when a run starts)
|
||||
private readonly runSnippets = new Map<number, Record<string, string>>();
|
||||
private readonly runSnippets = new Map<string, Record<string, string>>();
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ScenarioRunEntity)
|
||||
@@ -57,8 +57,8 @@ export class ScenarioSchedulerService {
|
||||
) {}
|
||||
|
||||
private persistLog(
|
||||
runId: number,
|
||||
stepRunId: number | null,
|
||||
runId: string,
|
||||
stepRunId: string | null,
|
||||
level: "log" | "warn" | "error",
|
||||
message: string,
|
||||
): void {
|
||||
@@ -67,7 +67,7 @@ export class ScenarioSchedulerService {
|
||||
);
|
||||
}
|
||||
|
||||
private stepLogger(stepRunId: number, runId: number): ScriptLogger {
|
||||
private stepLogger(stepRunId: string, runId: string): ScriptLogger {
|
||||
return (level, msg) => {
|
||||
this.logger[level](`StepRun #${stepRunId} script: ${msg}`);
|
||||
this.persistLog(runId, stepRunId, level, msg);
|
||||
@@ -98,7 +98,7 @@ export class ScenarioSchedulerService {
|
||||
* no login step or the environment cannot be found.
|
||||
*/
|
||||
private async resolveRunEnvironment(
|
||||
scenarioId: number,
|
||||
scenarioId: string,
|
||||
): Promise<EnvironmentUrls | null> {
|
||||
try {
|
||||
const scenario = await this.scenarioService.findOne(scenarioId);
|
||||
@@ -147,7 +147,7 @@ export class ScenarioSchedulerService {
|
||||
}
|
||||
}
|
||||
|
||||
private async processRunToCompletion(runId: number): Promise<void> {
|
||||
private async processRunToCompletion(runId: string): Promise<void> {
|
||||
try {
|
||||
let stepRun = await this.runStepRepo.findOne({
|
||||
where: { runId, status: "pending" },
|
||||
@@ -207,7 +207,7 @@ export class ScenarioSchedulerService {
|
||||
|
||||
// ── Shared browser per run ─────────────────────────────────────────────────
|
||||
|
||||
private async getOrCreateBrowserHandle(runId: number): Promise<BrowserHandle> {
|
||||
private async getOrCreateBrowserHandle(runId: string): Promise<BrowserHandle> {
|
||||
const existing = this.runBrowsers.get(runId);
|
||||
if (existing) return existing;
|
||||
|
||||
@@ -224,7 +224,7 @@ export class ScenarioSchedulerService {
|
||||
return handle;
|
||||
}
|
||||
|
||||
private async closeBrowserHandle(runId: number): Promise<void> {
|
||||
private async closeBrowserHandle(runId: string): Promise<void> {
|
||||
const handle = this.runBrowsers.get(runId);
|
||||
if (!handle) return;
|
||||
this.runBrowsers.delete(runId);
|
||||
|
||||
@@ -13,11 +13,11 @@ export type StepType = "login" | "exec" | "sign";
|
||||
|
||||
@Entity("scenario_steps")
|
||||
export class ScenarioStepEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
scenarioId: number;
|
||||
@Column("text")
|
||||
scenarioId: string;
|
||||
|
||||
@ManyToOne(() => ScenarioEntity, (scenario) => scenario.steps, {
|
||||
onDelete: "CASCADE",
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
ParseUUIDPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
@@ -61,7 +61,7 @@ export class ScenarioController {
|
||||
@ApiOperation({ summary: "Get a scenario with its steps" })
|
||||
@ApiResponse({ status: 200 })
|
||||
@ApiResponse({ status: 404, description: "Scenario not found" })
|
||||
findOne(@Param("id", ParseIntPipe) id: number) {
|
||||
findOne(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.scenarioService.findOne(id);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export class ScenarioController {
|
||||
@ApiResponse({ status: 200 })
|
||||
@ApiResponse({ status: 404, description: "Scenario not found" })
|
||||
update(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateScenarioDto,
|
||||
) {
|
||||
return this.scenarioService.update(id, dto);
|
||||
@@ -81,7 +81,7 @@ export class ScenarioController {
|
||||
@ApiOperation({ summary: "Delete a scenario and all its steps" })
|
||||
@ApiResponse({ status: 204 })
|
||||
@ApiResponse({ status: 404, description: "Scenario not found" })
|
||||
remove(@Param("id", ParseIntPipe) id: number) {
|
||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.scenarioService.remove(id);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export class ScenarioController {
|
||||
@ApiOperation({ summary: "Export a scenario as a portable JSON payload" })
|
||||
@ApiResponse({ status: 200 })
|
||||
@ApiResponse({ status: 404, description: "Scenario not found" })
|
||||
exportScenario(@Param("id", ParseIntPipe) id: number) {
|
||||
exportScenario(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.scenarioService.exportScenario(id);
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export class ScenarioController {
|
||||
@ApiResponse({ status: 201, description: "Step created" })
|
||||
@ApiResponse({ status: 404, description: "Scenario not found" })
|
||||
createStep(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() dto: CreateScenarioStepDto,
|
||||
) {
|
||||
return this.scenarioService.createStep(id, dto);
|
||||
@@ -111,8 +111,8 @@ export class ScenarioController {
|
||||
@ApiResponse({ status: 200 })
|
||||
@ApiResponse({ status: 404, description: "Scenario or step not found" })
|
||||
findStep(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("stepId", ParseIntPipe) stepId: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Param("stepId", ParseUUIDPipe) stepId: string,
|
||||
) {
|
||||
return this.scenarioService.findStep(id, stepId);
|
||||
}
|
||||
@@ -122,8 +122,8 @@ export class ScenarioController {
|
||||
@ApiResponse({ status: 200 })
|
||||
@ApiResponse({ status: 404, description: "Scenario or step not found" })
|
||||
updateStep(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("stepId", ParseIntPipe) stepId: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Param("stepId", ParseUUIDPipe) stepId: string,
|
||||
@Body() dto: UpdateScenarioStepDto,
|
||||
) {
|
||||
return this.scenarioService.updateStep(id, stepId, dto);
|
||||
@@ -135,8 +135,8 @@ export class ScenarioController {
|
||||
@ApiResponse({ status: 204 })
|
||||
@ApiResponse({ status: 404, description: "Scenario or step not found" })
|
||||
removeStep(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("stepId", ParseIntPipe) stepId: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Param("stepId", ParseUUIDPipe) stepId: string,
|
||||
) {
|
||||
return this.scenarioService.removeStep(id, stepId);
|
||||
}
|
||||
@@ -147,7 +147,7 @@ export class ScenarioController {
|
||||
@ApiOperation({ summary: "List credentials assigned to a scenario" })
|
||||
@ApiResponse({ status: 200 })
|
||||
@ApiResponse({ status: 404, description: "Scenario not found" })
|
||||
findScenarioCredentials(@Param("id", ParseIntPipe) id: number) {
|
||||
findScenarioCredentials(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.scenarioService.findScenarioCredentials(id);
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ export class ScenarioController {
|
||||
@ApiResponse({ status: 404, description: "Scenario or credential not found" })
|
||||
@ApiResponse({ status: 409, description: "Alias already used in this scenario" })
|
||||
addScenarioCredential(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() dto: AddScenarioCredentialDto,
|
||||
) {
|
||||
return this.scenarioService.addScenarioCredential(id, dto);
|
||||
@@ -169,8 +169,8 @@ export class ScenarioController {
|
||||
@ApiResponse({ status: 204 })
|
||||
@ApiResponse({ status: 404, description: "Scenario or credential assignment not found" })
|
||||
removeScenarioCredential(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("scCredId", ParseIntPipe) scCredId: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Param("scCredId", ParseUUIDPipe) scCredId: string,
|
||||
) {
|
||||
return this.scenarioService.removeScenarioCredential(id, scCredId);
|
||||
}
|
||||
@@ -184,7 +184,7 @@ export class ScenarioController {
|
||||
@ApiResponse({ status: 200 })
|
||||
@ApiResponse({ status: 404, description: "Scenario not found" })
|
||||
findRuns(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Query() query: RunsQueryDto,
|
||||
) {
|
||||
return this.scenarioService.findRuns(id, query);
|
||||
@@ -194,7 +194,7 @@ export class ScenarioController {
|
||||
@ApiOperation({ summary: "Create a new run for a scenario" })
|
||||
@ApiResponse({ status: 201, description: "Run created with step runs" })
|
||||
@ApiResponse({ status: 404, description: "Scenario not found" })
|
||||
createRun(@Param("id", ParseIntPipe) id: number) {
|
||||
createRun(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.scenarioService.createRun(id);
|
||||
}
|
||||
|
||||
@@ -203,8 +203,8 @@ export class ScenarioController {
|
||||
@ApiResponse({ status: 200 })
|
||||
@ApiResponse({ status: 404, description: "Scenario or run not found" })
|
||||
findRun(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("runId", ParseIntPipe) runId: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Param("runId", ParseUUIDPipe) runId: string,
|
||||
) {
|
||||
return this.scenarioService.findRun(id, runId);
|
||||
}
|
||||
@@ -218,8 +218,8 @@ export class ScenarioController {
|
||||
@ApiResponse({ status: 200 })
|
||||
@ApiResponse({ status: 404, description: "Scenario or run not found" })
|
||||
waitForRun(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("runId", ParseIntPipe) runId: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Param("runId", ParseUUIDPipe) runId: string,
|
||||
) {
|
||||
return this.scenarioService.waitForRun(id, runId);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import { ScenarioCredentialEntity } from "./scenario-credential.entity";
|
||||
|
||||
@Entity("scenarios")
|
||||
export class ScenarioEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -110,7 +110,7 @@ export class SessionContextService implements OnModuleDestroy {
|
||||
* Close the context (if open) and delete the session record from DB.
|
||||
* Throws 404 if the session ID does not exist.
|
||||
*/
|
||||
async delete(id: number): Promise<void> {
|
||||
async delete(id: string): Promise<void> {
|
||||
const session = await this.sessionService.findById(id);
|
||||
if (!session) {
|
||||
throw new NotFoundException(`Session ${id} not found`);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
HttpCode,
|
||||
NotFoundException,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
ParseUUIDPipe,
|
||||
Query,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
@@ -38,7 +38,7 @@ export class SessionController {
|
||||
@ApiOperation({ summary: "Get a session by ID" })
|
||||
@ApiResponse({ status: 200, description: "Session found" })
|
||||
@ApiResponse({ status: 404, description: "Session not found" })
|
||||
async findOne(@Param("id", ParseIntPipe) id: number) {
|
||||
async findOne(@Param("id", ParseUUIDPipe) id: string) {
|
||||
const session = await this.sessionService.findById(id);
|
||||
if (!session) throw new NotFoundException(`Session ${id} not found`);
|
||||
const {
|
||||
@@ -58,7 +58,7 @@ export class SessionController {
|
||||
@ApiOperation({ summary: "Delete a session by ID (closes it first if open)" })
|
||||
@ApiResponse({ status: 204, description: "Session deleted" })
|
||||
@ApiResponse({ status: 404, description: "Session not found" })
|
||||
async remove(@Param("id", ParseIntPipe) id: number): Promise<void> {
|
||||
async remove(@Param("id", ParseUUIDPipe) id: string): Promise<void> {
|
||||
await this.sessionContextService.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ export type SessionStatus = "open" | "closed";
|
||||
|
||||
@Entity("sessions")
|
||||
export class SessionEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
sessionName: string;
|
||||
|
||||
@@ -72,7 +72,7 @@ export class SessionService implements OnApplicationBootstrap {
|
||||
return this.repo.findOneBy({ sessionName });
|
||||
}
|
||||
|
||||
findById(id: number): Promise<SessionEntity | null> {
|
||||
findById(id: string): Promise<SessionEntity | null> {
|
||||
return this.repo.findOneBy({ id });
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export class SessionService implements OnApplicationBootstrap {
|
||||
return { data, total, page, limit };
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<void> {
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.repo.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
|
||||
|
||||
export class SnippetExportDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsIn(["snippet"])
|
||||
kind?: "snippet";
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
id?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
code: string;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
ParseUUIDPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
@@ -14,6 +14,7 @@ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { SnippetService, SnippetOrderBy } from "./snippet.service";
|
||||
import { CreateSnippetDto } from "./dto/create-snippet.dto";
|
||||
import { UpdateSnippetDto } from "./dto/update-snippet.dto";
|
||||
import { SnippetExportDto } from "./dto/snippet-export.dto";
|
||||
import { PaginationQueryDto } from "../common/dto/pagination.dto";
|
||||
|
||||
@ApiTags("snippets")
|
||||
@@ -29,6 +30,13 @@ export class SnippetController {
|
||||
return this.snippetService.create(dto);
|
||||
}
|
||||
|
||||
@Post("import")
|
||||
@ApiOperation({ summary: "Import a snippet (upsert by id)" })
|
||||
@ApiResponse({ status: 201, description: "Snippet imported" })
|
||||
async importSnippet(@Body() dto: SnippetExportDto) {
|
||||
return this.snippetService.importSnippet(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: "List all snippets (paginated)" })
|
||||
@ApiResponse({ status: 200, description: "Paginated snippets" })
|
||||
@@ -36,11 +44,20 @@ export class SnippetController {
|
||||
return this.snippetService.findAll(query);
|
||||
}
|
||||
|
||||
@Get(":id/export")
|
||||
@ApiOperation({ summary: "Export a snippet as a plain object" })
|
||||
@ApiResponse({ status: 200, description: "Snippet export payload" })
|
||||
@ApiResponse({ status: 404, description: "Snippet not found" })
|
||||
async exportSnippet(@Param("id", ParseUUIDPipe) id: string) {
|
||||
const snippet = await this.snippetService.findOne(id);
|
||||
return this.snippetService.exportSnippet(snippet);
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@ApiOperation({ summary: "Get snippet by ID" })
|
||||
@ApiResponse({ status: 200, description: "Snippet record" })
|
||||
@ApiResponse({ status: 404, description: "Snippet not found" })
|
||||
findOne(@Param("id", ParseIntPipe) id: number) {
|
||||
findOne(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.snippetService.findOne(id);
|
||||
}
|
||||
|
||||
@@ -50,7 +67,7 @@ export class SnippetController {
|
||||
@ApiResponse({ status: 404, description: "Snippet not found" })
|
||||
@ApiResponse({ status: 409, description: "Name already taken" })
|
||||
update(
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateSnippetDto,
|
||||
) {
|
||||
return this.snippetService.update(id, dto);
|
||||
@@ -61,7 +78,7 @@ export class SnippetController {
|
||||
@ApiOperation({ summary: "Delete a snippet" })
|
||||
@ApiResponse({ status: 204, description: "Snippet deleted" })
|
||||
@ApiResponse({ status: 404, description: "Snippet not found" })
|
||||
remove(@Param("id", ParseIntPipe) id: number) {
|
||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.snippetService.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
|
||||
@Entity("snippets")
|
||||
export class SnippetEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
/** Unique identifier used to call the snippet: helpers.runSnippet('mySnippet', ...) */
|
||||
@Column({ unique: true })
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Repository } from "typeorm";
|
||||
import { SnippetEntity } from "./snippet.entity";
|
||||
import { CreateSnippetDto } from "./dto/create-snippet.dto";
|
||||
import { UpdateSnippetDto } from "./dto/update-snippet.dto";
|
||||
import { SnippetExportDto } from "./dto/snippet-export.dto";
|
||||
import {
|
||||
PaginationQueryDto,
|
||||
PaginatedResult,
|
||||
@@ -47,13 +48,13 @@ export class SnippetService {
|
||||
return { data, total, page, limit };
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<SnippetEntity> {
|
||||
async findOne(id: string): Promise<SnippetEntity> {
|
||||
const snippet = await this.repo.findOneBy({ id });
|
||||
if (!snippet) throw new NotFoundException(`Snippet ${id} not found`);
|
||||
return snippet;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateSnippetDto): Promise<SnippetEntity> {
|
||||
async update(id: string, dto: UpdateSnippetDto): Promise<SnippetEntity> {
|
||||
const snippet = await this.findOne(id);
|
||||
if (dto.name && dto.name !== snippet.name) {
|
||||
const conflict = await this.repo.findOneBy({ name: dto.name });
|
||||
@@ -63,7 +64,7 @@ export class SnippetService {
|
||||
return this.repo.save(snippet);
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<void> {
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findOne(id);
|
||||
await this.repo.delete(id);
|
||||
}
|
||||
@@ -73,4 +74,43 @@ export class SnippetService {
|
||||
const { data } = await this.findAll({ limit: 1000 });
|
||||
return Object.fromEntries(data.map((s) => [s.name, s.code]));
|
||||
}
|
||||
|
||||
exportSnippet(snippet: SnippetEntity): SnippetExportDto {
|
||||
return {
|
||||
kind: "snippet",
|
||||
id: snippet.id,
|
||||
name: snippet.name,
|
||||
description: snippet.description,
|
||||
code: snippet.code,
|
||||
};
|
||||
}
|
||||
|
||||
async importSnippet(dto: SnippetExportDto): Promise<SnippetEntity> {
|
||||
if (dto.id) {
|
||||
const existing = await this.repo.findOneBy({ id: dto.id });
|
||||
if (existing) {
|
||||
Object.assign(existing, {
|
||||
name: dto.name,
|
||||
description: dto.description ?? null,
|
||||
code: dto.code,
|
||||
});
|
||||
return this.repo.save(existing);
|
||||
}
|
||||
return this.repo.save(
|
||||
this.repo.create({
|
||||
id: dto.id,
|
||||
name: dto.name,
|
||||
description: dto.description ?? null,
|
||||
code: dto.code,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return this.repo.save(
|
||||
this.repo.create({
|
||||
name: dto.name,
|
||||
description: dto.description ?? null,
|
||||
code: dto.code,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user