From 3dbb1401311abb38f90f546daa178bf639eb5d25 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Wed, 20 May 2026 15:54:33 +0200 Subject: [PATCH] fix: ensure LoopContainer required fields are set via schema-driven normalization --- .../containers/containers-call.spec.ts | 60 +++++++++++++++++++ .../services/containers/containers-call.ts | 40 +++++++++++-- src/app/utilities/rete-editor.ts | 1 + 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/src/app/services/containers/containers-call.spec.ts b/src/app/services/containers/containers-call.spec.ts index 9ea094b..7a3fa7b 100644 --- a/src/app/services/containers/containers-call.spec.ts +++ b/src/app/services/containers/containers-call.spec.ts @@ -143,4 +143,64 @@ describe('ContainersCallService', () => { 'Invalid container catalog response: expected reduced catalog format with a descriptors array' ); }); + + it('fills missing required boolean fields using descriptor schema defaults', async () => { + const typesRequest = firstValueFrom(service.retrieveAllContainerTypes()); + + httpMock.expectOne(`${environment.apiUrl}/containers/types/catalog`).flush({ + descriptors: [ + { + type: 'LoopContainer', + description: 'Loop container node', + userInteractive: false, + hasExampleContainer: true, + exampleContainerEndpoint: '/containers/types/LoopContainer/example', + schema: { + type: 'object', + properties: { + name: { type: 'string' }, + maxIterations: { type: 'integer' }, + useLlm: { type: 'boolean' } + }, + required: ['name', 'maxIterations', 'useLlm'] + } + } + ] + }); + + await typesRequest; + + const request = firstValueFrom(service.createContainer('container-1', { + typeName: 'LoopContainer', + specificConfiguration: { + name: 'Loop', + maxIterations: 3 + } + })); + + const createRequest = httpMock.expectOne(`${environment.apiUrl}/containers`); + expect(createRequest.request.method).toBe('POST'); + expect(createRequest.request.body.useLlm).toBe(false); + expect(createRequest.request.body.useLLM).toBeUndefined(); + + createRequest.flush({ + id: 'container-1', + name: 'Loop', + typeName: 'LoopContainer', + specificConfiguration: { + type: 'LoopContainerConfiguration', + name: 'Loop', + maxIterations: 3, + useLlm: false + }, + inputs: [], + outputs: [] + }); + + await expect(request).resolves.toEqual(expect.objectContaining({ + id: 'container-1', + typeName: 'LoopContainer', + nodeFamily: 'container' + })); + }); }); diff --git a/src/app/services/containers/containers-call.ts b/src/app/services/containers/containers-call.ts index 962e32a..2027558 100644 --- a/src/app/services/containers/containers-call.ts +++ b/src/app/services/containers/containers-call.ts @@ -267,16 +267,48 @@ export class ContainersCallService extends ContainersCallServiceBase { private buildContainerConfigurationPayload(containerType: string, configuration: Record) { const { typeName: _ignoreTypeName, ...sanitized } = configuration; - const configurationType = this.resolveConfigurationType(containerType, sanitized); + const normalized = this.normalizeConfigurationWithSchema(containerType, sanitized); + const configurationType = this.resolveConfigurationType(containerType, normalized); return { - ...sanitized, + ...normalized, type: configurationType, - name: typeof sanitized["name"] === "string" && sanitized["name"].length > 0 - ? sanitized["name"] + name: typeof normalized["name"] === "string" && normalized["name"].length > 0 + ? normalized["name"] : containerType }; } + private normalizeConfigurationWithSchema(containerType: string, configuration: Record) { + const normalized = { ...configuration }; + const descriptor = this.containerTypesCache + ?.find((candidate) => candidate.type === containerType); + const schema = descriptor?.schema; + if (!schema || typeof schema !== "object" || Array.isArray(schema)) { + return normalized; + } + + const required = Array.isArray(schema["required"]) + ? schema["required"].filter((value): value is string => typeof value === "string" && value.length > 0) + : []; + if (!required.length) return normalized; + + const properties = this.toRecord(schema["properties"]); + for (const key of required) { + if (normalized[key] !== undefined) continue; + const propertySchema = this.toRecord(properties[key]); + const defaultValue = propertySchema["default"]; + if (typeof defaultValue === "boolean" || typeof defaultValue === "number" || typeof defaultValue === "string") { + normalized[key] = defaultValue; + continue; + } + if (propertySchema["type"] === "boolean") { + normalized[key] = false; + } + } + + return normalized; + } + private resolveConfigurationType(containerType: string, configuration: Record) { const explicitType = this.toNullableString(configuration["type"]); if (explicitType) return explicitType; diff --git a/src/app/utilities/rete-editor.ts b/src/app/utilities/rete-editor.ts index 36da7e9..8b22064 100644 --- a/src/app/utilities/rete-editor.ts +++ b/src/app/utilities/rete-editor.ts @@ -289,6 +289,7 @@ export async function addBlockToEditor( const currentConfiguration = cloneValue(currentLiveNode.data.specificConfiguration ?? {}) as Record; const nextConfiguration: Record = { + ...currentConfiguration, name: String(currentConfiguration['name'] ?? currentLiveNode.data['name'] ?? 'Container'), subFlow: candidateSubFlow };