refactor(environment): rename urls payload to data

- replace hardcoded URL keys with a generic key-value data map

- align backend DTOs, MCP schemas, and service import/export mapping

- update environment UI forms to edit JSON data instead of fixed fields
This commit is contained in:
2026-04-10 21:22:46 +03:00
parent b45f95d8cf
commit fd515ee459
15 changed files with 141 additions and 143 deletions
@@ -7,7 +7,7 @@ import { TraceLogger } from "../common/trace-logger";
import { parse } from "acorn";
import type { Page, BrowserContext } from "playwright";
import { dumpDom } from "./dom-helpers";
import type { EnvironmentUrls } from "../environment/environment.entity";
import type { EnvironmentData } from "../environment/environment.entity";
export interface ExecResult {
result: unknown;
@@ -49,7 +49,7 @@ export class CodeExecutorService {
log?: ScriptLogger,
getStepOutput?: (order: number) => Promise<unknown>,
credentials?: Record<string, unknown>,
environment?: EnvironmentUrls | null,
environment?: EnvironmentData | null,
snippets?: Record<string, string> | null,
result?: unknown,
): Promise<ExecResult> {
@@ -61,7 +61,7 @@ export class CodeExecutorService {
.join(" ");
const credMap: Record<string, unknown> = credentials ?? {};
const envUrls: EnvironmentUrls = environment ?? {};
const envData: EnvironmentData = environment ?? {};
const snippetMap: Record<string, string> = snippets ?? {};
// pageHelpers is referenced by runSnippet, so we declare it as a var first.
@@ -91,14 +91,14 @@ export class CodeExecutorService {
}
return credMap[alias];
},
/** All URLs defined for the current environment (may be empty if no environment is set). */
env: { ...envUrls },
/** Returns the URL for the given key, or throws if it is not defined. */
/** All values defined for the current environment (may be empty if no environment is set). */
env: { ...envData },
/** Returns the value for the given key, or throws if it is not defined. */
getEnvUrl: (key: string): string => {
const value = envUrls[key];
const value = envData[key];
if (value == null) {
throw new Error(
`Environment URL "${key}" is not defined for this environment`,
`Environment value "${key}" is not defined for this environment`,
);
}
return value;
@@ -1,6 +1,6 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsObject, IsString } from "class-validator";
import { EnvironmentUrls } from "../environment.entity";
import { EnvironmentData } from "../environment.entity";
export class CreateEnvironmentDto {
@ApiProperty({ example: "liquio-diia-stg" })
@@ -9,13 +9,13 @@ export class CreateEnvironmentDto {
name: string;
@ApiProperty({
description: "Map of URL identifiers to URL strings",
description: "Generic key-value map for environment metadata",
example: {
id_url: "https://id-liquio-diia-stg.kitsoft.ua/",
cabinet_url: "https://cabinet-liquio-diia-stg.kitsoft.ua/",
admin_url: "https://admin-liquio-diia-stg.kitsoft.ua/",
id: "https://id-liquio-diia-stg.kitsoft.ua/",
cabinet: "https://cabinet-liquio-diia-stg.kitsoft.ua/",
feature_flag: "enabled",
},
})
@IsObject()
urls: EnvironmentUrls;
data: EnvironmentData;
}
@@ -7,7 +7,7 @@ import {
IsString,
IsUUID,
} from "class-validator";
import { EnvironmentUrls } from "../environment.entity";
import { EnvironmentData } from "../environment.entity";
export class EnvironmentExportDto {
@ApiPropertyOptional()
@@ -27,5 +27,5 @@ export class EnvironmentExportDto {
@ApiProperty()
@IsObject()
urls: EnvironmentUrls;
data: EnvironmentData;
}
+2 -5
View File
@@ -6,10 +6,7 @@ import {
UpdateDateColumn,
} from "typeorm";
export interface EnvironmentUrls {
id_url?: string;
cabinet_url?: string;
admin_url?: string;
export interface EnvironmentData {
[key: string]: string | undefined;
}
@@ -22,7 +19,7 @@ export class EnvironmentEntity {
name: string;
@Column("simple-json")
urls: EnvironmentUrls;
data: EnvironmentData;
@CreateDateColumn()
createdAt: Date;
@@ -71,7 +71,7 @@ export class EnvironmentService {
kind: "environment",
id: env.id,
name: env.name,
urls: env.urls,
data: env.data,
};
}
@@ -79,18 +79,18 @@ export class EnvironmentService {
if (dto.id) {
const existing = await this.repo.findOneBy({ id: dto.id });
if (existing) {
Object.assign(existing, { name: dto.name, urls: dto.urls });
Object.assign(existing, { name: dto.name, data: dto.data });
return this.repo.save(existing);
}
return this.repo.save(
this.repo.create({ id: dto.id, name: dto.name, urls: dto.urls }),
this.repo.create({ id: dto.id, name: dto.name, data: dto.data }),
);
}
const byName = await this.repo.findOneBy({ name: dto.name });
if (byName) {
Object.assign(byName, { urls: dto.urls });
Object.assign(byName, { data: dto.data });
return this.repo.save(byName);
}
return this.repo.save(this.repo.create({ name: dto.name, urls: dto.urls }));
return this.repo.save(this.repo.create({ name: dto.name, data: dto.data }));
}
}
+11 -13
View File
@@ -6,7 +6,7 @@ import type { Request, Response } from "express";
import { SessionService } from "../session/session.service";
import { SessionContextService } from "../session/session-context.service";
import { EnvironmentService } from "../environment/environment.service";
import type { EnvironmentUrls } from "../environment/environment.entity";
import type { EnvironmentData } from "../environment/environment.entity";
import { BrowserService } from "../browser/browser.service";
import { CodeExecutorService } from "../code-executor/code-executor.service";
import { ScenarioService } from "../scenario/scenario.service";
@@ -174,23 +174,21 @@ export class McpService {
server.registerTool(
"create_environment",
{
description: "Create a new named environment with a set of URLs",
description: "Create a new named environment with generic data",
inputSchema: {
name: z
.string()
.describe("Unique environment name, e.g. liquio-diia-stg"),
urls: z
data: z
.record(z.string(), z.string())
.describe(
"Map of URL keys to URL strings (id_url, cabinet_url, admin_url, …)",
),
.describe("Map of string keys to string values"),
},
},
async ({ name, urls }) => {
async ({ name, data }) => {
try {
const env = await this.environmentService.create({
name,
urls: urls as EnvironmentUrls,
data: data as EnvironmentData,
});
return {
content: [{ type: "text" as const, text: JSON.stringify(env) }],
@@ -207,21 +205,21 @@ export class McpService {
server.registerTool(
"update_environment",
{
description: "Update an existing environment (name and/or urls)",
description: "Update an existing environment (name and/or data)",
inputSchema: {
id: z.string().uuid().describe("Environment ID to update"),
name: z.string().optional().describe("New name"),
urls: z
data: z
.record(z.string(), z.string())
.optional()
.describe("New URLs map"),
.describe("New data map"),
},
},
async ({ id, name, urls }) => {
async ({ id, name, data }) => {
try {
const env = await this.environmentService.update(id, {
name,
urls: urls as EnvironmentUrls | undefined,
data: data as EnvironmentData | undefined,
});
return {
content: [{ type: "text" as const, text: JSON.stringify(env) }],
+16 -16
View File
@@ -19,44 +19,44 @@ describe("EnvironmentController", () => {
it("creates an environment and returns 201", async () => {
const res = await request(app.getHttpServer())
.post("/environments")
.send({ name: "env-a", urls: { id_url: "https://id.example.com" } })
.send({ name: "env-a", data: { id: "https://id.example.com" } })
.expect(201);
expect(res.body.id).toBeDefined();
expect(res.body.name).toBe("env-a");
expect(res.body.urls.id_url).toBe("https://id.example.com");
expect(res.body.data.id).toBe("https://id.example.com");
});
it("returns 400 when name is missing", async () => {
await request(app.getHttpServer())
.post("/environments")
.send({ urls: { id_url: "https://id.example.com" } })
.send({ data: { id: "https://id.example.com" } })
.expect(400);
});
it("returns 400 when urls is missing", async () => {
it("returns 400 when data is missing", async () => {
await request(app.getHttpServer())
.post("/environments")
.send({ name: "env-no-urls" })
.send({ name: "env-no-data" })
.expect(400);
});
it("returns 400 when urls is not an object", async () => {
it("returns 400 when data is not an object", async () => {
await request(app.getHttpServer())
.post("/environments")
.send({ name: "env-bad-urls", urls: "not-an-object" })
.send({ name: "env-bad-data", data: "not-an-object" })
.expect(400);
});
it("returns 409 when name already exists", async () => {
await request(app.getHttpServer())
.post("/environments")
.send({ name: "env-duplicate", urls: {} })
.send({ name: "env-duplicate", data: {} })
.expect(201);
await request(app.getHttpServer())
.post("/environments")
.send({ name: "env-duplicate", urls: {} })
.send({ name: "env-duplicate", data: {} })
.expect(409);
});
});
@@ -78,11 +78,11 @@ describe("EnvironmentController", () => {
// seed two extra environments
await request(app.getHttpServer())
.post("/environments")
.send({ name: "env-page-1", urls: {} })
.send({ name: "env-page-1", data: {} })
.expect(201);
await request(app.getHttpServer())
.post("/environments")
.send({ name: "env-page-2", urls: {} })
.send({ name: "env-page-2", data: {} })
.expect(201);
const res = await request(app.getHttpServer())
@@ -109,10 +109,10 @@ describe("EnvironmentController", () => {
it("orders by name ASC", async () => {
await request(app.getHttpServer())
.post("/environments")
.send({ name: "zzz-env", urls: {} });
.send({ name: "zzz-env", data: {} });
await request(app.getHttpServer())
.post("/environments")
.send({ name: "aaa-env", urls: {} });
.send({ name: "aaa-env", data: {} });
const res = await request(app.getHttpServer())
.get("/environments?orderBy=name&orderDir=ASC")
@@ -148,7 +148,7 @@ describe("EnvironmentController", () => {
.post("/environments")
.send({
name: "env-get-one",
urls: { cabinet_url: "https://cabinet.example.com" },
data: { cabinet: "https://cabinet.example.com" },
})
.expect(201);
@@ -174,7 +174,7 @@ describe("EnvironmentController", () => {
it("updates name and returns 200", async () => {
const created = await request(app.getHttpServer())
.post("/environments")
.send({ name: "env-patch-me", urls: {} })
.send({ name: "env-patch-me", data: {} })
.expect(201);
const res = await request(app.getHttpServer())
@@ -199,7 +199,7 @@ describe("EnvironmentController", () => {
it("deletes and returns 204", async () => {
const created = await request(app.getHttpServer())
.post("/environments")
.send({ name: "env-delete-me", urls: {} })
.send({ name: "env-delete-me", data: {} })
.expect(201);
await request(app.getHttpServer())
+1 -1
View File
@@ -118,7 +118,7 @@ describe("McpController", () => {
it("creates an environment via MCP", async () => {
const { status, rpc } = await mcpCall("create_environment", {
name: "mcp-test-env",
urls: { id_url: "https://id.example.com" },
data: { id: "https://id.example.com" },
});
expect(status).toBe(200);
const result = rpc.result as { content: { text: string }[] };