271 lines
11 KiB
TypeScript
271 lines
11 KiB
TypeScript
import { HttpClient } from "@angular/common/http";
|
|
import { inject } from "@angular/core";
|
|
import { environment } from "@environment";
|
|
import {
|
|
BlockType,
|
|
FlowContainer,
|
|
FlowData,
|
|
FlowSubflowValidationResult
|
|
} from "@models/flow";
|
|
import { map, Observable } from "rxjs";
|
|
import { ContainersCallServiceBase } from "./container-call.base";
|
|
|
|
export class ContainersCallService extends ContainersCallServiceBase {
|
|
private readonly http = inject(HttpClient);
|
|
private containerTypesCache: BlockType[] | null = null;
|
|
|
|
override retrieveAllContainerTypes(): Observable<BlockType[]> {
|
|
return this.http
|
|
.get<unknown[]>(`${environment.apiUrl}/containers/types`)
|
|
.pipe(
|
|
map((raw) => (Array.isArray(raw) ? raw.map((value) => this.containerTypeFromApi(value)) : [])),
|
|
map((types) => {
|
|
this.containerTypesCache = types;
|
|
return types;
|
|
})
|
|
);
|
|
}
|
|
|
|
override createEmptyContainer(containerType: string): Observable<FlowContainer> {
|
|
return this.http
|
|
.get<unknown>(`${environment.apiUrl}/containers/types/${encodeURIComponent(containerType)}/example`)
|
|
.pipe(map((raw) => this.flowContainerFromApi(raw, containerType)));
|
|
}
|
|
|
|
override createContainer(containerId: string, configuration: any): Observable<FlowContainer> {
|
|
const containerType = String(configuration?.typeName ?? configuration?.type ?? "GenericContainer");
|
|
const payload = this.buildContainerConfigurationPayload(
|
|
containerType,
|
|
this.toRecord(configuration?.specificConfiguration ?? configuration)
|
|
);
|
|
|
|
return this.http
|
|
.post<unknown>(`${environment.apiUrl}/containers`, payload)
|
|
.pipe(
|
|
map((raw) => {
|
|
if (!raw || typeof raw !== "object") {
|
|
throw new Error(`Invalid createContainer response for ${containerType}`);
|
|
}
|
|
return raw;
|
|
}),
|
|
map((raw) =>
|
|
this.flowContainerFromApi(
|
|
{
|
|
...(this.toRecord(raw)),
|
|
id: this.toRecord(raw)["id"] ?? containerId
|
|
},
|
|
containerType
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
override validateContainerSubflow(subFlow: FlowData, validationUrl?: string | null): Observable<FlowSubflowValidationResult> {
|
|
const resolvedValidationUrl = this.toApiPath(validationUrl) ?? `${environment.apiUrl}/containers/validate-subflow`;
|
|
return this.http
|
|
.post<unknown>(
|
|
resolvedValidationUrl,
|
|
{ subFlow }
|
|
)
|
|
.pipe(map((raw) => this.subflowValidationFromApi(raw)));
|
|
}
|
|
|
|
private containerTypeFromApi(raw: unknown): BlockType {
|
|
const value = this.toRecord(raw);
|
|
return {
|
|
type: String(value["type"] ?? value["containerType"] ?? value["name"] ?? "GenericContainer"),
|
|
family: 'container',
|
|
description: String(value["description"] ?? ""),
|
|
userInteractive: Boolean(value["userInteractive"] ?? value["interactive"] ?? false),
|
|
hasExampleBlock: Boolean(value["hasExampleBlock"] ?? false),
|
|
exampleBlockEndpoint: this.toApiPath(value["exampleBlockEndpoint"]),
|
|
configurationType: this.toNullableString(value["configurationType"]),
|
|
configurationClass: this.toNullableString(value["configurationClass"]),
|
|
schema: this.toSchema(value["schema"] ?? value["configurationSchema"] ?? null)
|
|
};
|
|
}
|
|
|
|
private flowContainerFromApi(raw: unknown, fallbackTypeName = "GenericContainer"): FlowContainer {
|
|
const root = this.toRecord(raw);
|
|
const value = this.toRecord(root["container"] ?? root["node"] ?? root["data"] ?? root);
|
|
const specificConfigurationRaw = value["specificConfiguration"] ?? value["configuration"] ?? value["containerConfiguration"] ?? {};
|
|
const specificConfiguration = this.toRecord(specificConfigurationRaw);
|
|
const typeName = String(value["typeName"] ?? value["containerType"] ?? specificConfiguration["typeName"] ?? fallbackTypeName);
|
|
|
|
return {
|
|
id: String(value["id"] ?? crypto.randomUUID()),
|
|
name: String(value["name"] ?? specificConfiguration["name"] ?? typeName),
|
|
position: this.toPosition(value["position"]),
|
|
inputs: this.toPorts(value["inputs"]),
|
|
outputs: this.toPorts(value["outputs"]),
|
|
specificConfiguration,
|
|
typeName,
|
|
nodeFamily: 'container'
|
|
};
|
|
}
|
|
|
|
private subflowValidationFromApi(raw: unknown): FlowSubflowValidationResult {
|
|
const value = this.toRecord(raw);
|
|
const rawErrors = Array.isArray(value['errors']) ? value['errors'] : [];
|
|
|
|
return {
|
|
valid: Boolean(value['valid'] ?? false),
|
|
errors: rawErrors
|
|
.map((item) => this.toRecord(item))
|
|
.map((item) => ({
|
|
entity: this.toNullableString(item['entity']) ?? undefined,
|
|
id: this.toNullableString(item['id']) ?? undefined,
|
|
field: this.toNullableString(item['field']) ?? undefined,
|
|
message: String(item['message'] ?? 'Invalid subflow')
|
|
})),
|
|
openInputs: this.toOpenInputs(value['openInputs']),
|
|
openOutputs: this.toOpenOutputs(value['openOutputs'])
|
|
};
|
|
}
|
|
|
|
private toOpenInputs(raw: unknown) {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw
|
|
.map((item) => this.toRecord(item))
|
|
.map((item) => {
|
|
const io = this.toRecord(item['io']);
|
|
const port = this.toPorts([Object.keys(io).length ? io : item])[0];
|
|
if (!port) return null;
|
|
return {
|
|
...port,
|
|
targetBlockId: this.toNullableString(item['targetBlockId'] ?? item['blockId'] ?? item['nodeId']) ?? undefined,
|
|
targetInputName: this.toNullableString(item['targetInputName'] ?? item['inputName'] ?? io['name']) ?? undefined,
|
|
blockId: this.toNullableString(item['blockId'] ?? item['nodeId']) ?? undefined,
|
|
inputName: this.toNullableString(item['inputName'] ?? io['name']) ?? undefined
|
|
};
|
|
})
|
|
.filter((item): item is NonNullable<typeof item> => !!item);
|
|
}
|
|
|
|
private toOpenOutputs(raw: unknown) {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw
|
|
.map((item) => this.toRecord(item))
|
|
.map((item) => {
|
|
const io = this.toRecord(item['io']);
|
|
const port = this.toPorts([Object.keys(io).length ? io : item])[0];
|
|
if (!port) return null;
|
|
return {
|
|
...port,
|
|
sourceBlockId: this.toNullableString(item['sourceBlockId'] ?? item['blockId'] ?? item['nodeId']) ?? undefined,
|
|
sourceOutputName: this.toNullableString(item['sourceOutputName'] ?? item['outputName'] ?? io['name']) ?? undefined,
|
|
blockId: this.toNullableString(item['blockId'] ?? item['nodeId']) ?? undefined,
|
|
outputName: this.toNullableString(item['outputName'] ?? io['name']) ?? undefined
|
|
};
|
|
})
|
|
.filter((item): item is NonNullable<typeof item> => !!item);
|
|
}
|
|
|
|
private toPorts(raw: unknown) {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw
|
|
.map((port) => this.toRecord(port))
|
|
.filter((port) => typeof port["name"] === "string" && (port["name"] as string).length > 0)
|
|
.map((port) => {
|
|
const type = String(port["type"] ?? "TEXT");
|
|
const multiple = Boolean(port["multiple"] ?? false);
|
|
return {
|
|
...port,
|
|
name: String(port["name"]),
|
|
type,
|
|
multiple,
|
|
valueKinds: this.toValueKinds(port["valueKinds"], { type, multiple })
|
|
};
|
|
});
|
|
}
|
|
|
|
private toValueKinds(raw: unknown, fallback: { type: string; multiple: boolean }) {
|
|
if (!Array.isArray(raw)) {
|
|
return [{ type: fallback.type, multiple: fallback.multiple }];
|
|
}
|
|
|
|
const kinds = raw
|
|
.map((item) => this.toRecord(item))
|
|
.filter((item) => typeof item["type"] === "string")
|
|
.map((item) => ({
|
|
type: String(item["type"] ?? fallback.type),
|
|
multiple: Boolean(item["multiple"] ?? false)
|
|
}));
|
|
|
|
return kinds.length ? kinds : [{ type: fallback.type, multiple: fallback.multiple }];
|
|
}
|
|
|
|
private toPosition(raw: unknown): { x: number; y: number } | undefined {
|
|
const value = this.toRecord(raw);
|
|
const x = value["x"];
|
|
const y = value["y"];
|
|
if (typeof x !== "number" || typeof y !== "number") return undefined;
|
|
return { x, y };
|
|
}
|
|
|
|
private toSchema(raw: unknown): Record<string, unknown> | null {
|
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
return raw as Record<string, unknown>;
|
|
}
|
|
|
|
private toRecord(value: unknown): Record<string, unknown> {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
private toNullableString(value: unknown): string | null {
|
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
}
|
|
|
|
private toApiPath(value: unknown): string | null {
|
|
if (typeof value !== "string" || value.length === 0) return null;
|
|
if (/^https?:\/\//.test(value)) return value;
|
|
return `${environment.apiUrl}${value.startsWith("/") ? value : `/${value}`}`;
|
|
}
|
|
|
|
private buildContainerConfigurationPayload(containerType: string, configuration: Record<string, unknown>) {
|
|
const { typeName: _ignoreTypeName, ...sanitized } = configuration;
|
|
const configurationType = this.resolveConfigurationType(containerType, sanitized);
|
|
return {
|
|
...sanitized,
|
|
type: configurationType,
|
|
name: typeof sanitized["name"] === "string" && sanitized["name"].length > 0
|
|
? sanitized["name"]
|
|
: containerType
|
|
};
|
|
}
|
|
|
|
private resolveConfigurationType(containerType: string, configuration: Record<string, unknown>) {
|
|
const explicitType = this.toNullableString(configuration["type"]);
|
|
if (explicitType) return explicitType;
|
|
|
|
const descriptor = this.containerTypesCache
|
|
?.find((candidate) => candidate.type === containerType);
|
|
const schemaType = this.resolveConfigurationTypeFromSchema(descriptor?.schema);
|
|
if (schemaType) return schemaType;
|
|
|
|
return descriptor?.configurationType ?? "GenericContainerConfiguration";
|
|
}
|
|
|
|
private resolveConfigurationTypeFromSchema(schema: Record<string, unknown> | null | undefined) {
|
|
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return null;
|
|
|
|
const properties = schema["properties"];
|
|
if (!properties || typeof properties !== "object" || Array.isArray(properties)) return null;
|
|
|
|
const typeProperty = (properties as Record<string, unknown>)["type"];
|
|
if (!typeProperty || typeof typeProperty !== "object" || Array.isArray(typeProperty)) return null;
|
|
|
|
const typeSchema = typeProperty as Record<string, unknown>;
|
|
const defaultValue = this.toNullableString(typeSchema["default"]);
|
|
if (defaultValue) return defaultValue;
|
|
|
|
const enumValues = Array.isArray(typeSchema["enum"])
|
|
? typeSchema["enum"].filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
: [];
|
|
if (enumValues.length === 1) return enumValues[0];
|
|
|
|
return null;
|
|
}
|
|
}
|