From 9005c41968ffe294d2230fd983fc67956bd9663b Mon Sep 17 00:00:00 2001 From: bburda Date: Tue, 1 Sep 2026 17:15:38 +0200 Subject: [PATCH] fix(scripts): follow the tightened script request bodies The gateway now describes both script request bodies instead of leaving them open. The upload body declares a required `file` part with optional `metadata`; the execution body is ScriptExecutionRequest, with `execution_type` required. Two casts in api-dispatch.ts existed only because those bodies used to be untyped, and they stop compiling against the 0.7.0 client. The execution cast is gone. StartScriptExecutionRequest already has the same shape as the generated ScriptExecutionRequest, so the body passes through and a future divergence between the two becomes a type error here. The upload keeps a cast, because FormData is a DOM interface and can never be assignable to a generated object type, but it now names the declared body instead of Record, so a change to the parts is caught at compile time rather than as a 400 at runtime. No request path changed between 0.6.0 and 0.7.0, so nothing else is affected. The client pin moves to ^0.7.0 and the package version follows it. --- package-lock.json | 12 ++++++------ package.json | 4 ++-- src/lib/api-dispatch.ts | 25 +++++++++++++++---------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 779d3af..2a83984 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ros2_medkit_web_ui", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ros2_medkit_web_ui", - "version": "0.6.0", + "version": "0.7.0", "license": "Apache-2.0", "dependencies": { "@codemirror/lang-python": "^6.2.1", @@ -18,7 +18,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.2.8", - "@selfpatch/ros2-medkit-client-ts": "^0.6.0", + "@selfpatch/ros2-medkit-client-ts": "^0.7.0", "@tailwindcss/vite": "^4.1.14", "@uiw/react-codemirror": "^4.25.11", "class-variance-authority": "^0.7.1", @@ -3425,9 +3425,9 @@ ] }, "node_modules/@selfpatch/ros2-medkit-client-ts": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@selfpatch/ros2-medkit-client-ts/-/ros2-medkit-client-ts-0.6.0.tgz", - "integrity": "sha512-eeF3aFm4SJz+8zYBIukSZgG+9nQja2iJqIxfuKIjhBS6y04scpVSdCMMvYJJJBjoo+1KbgxWd017WwNgj7LHew==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@selfpatch/ros2-medkit-client-ts/-/ros2-medkit-client-ts-0.7.0.tgz", + "integrity": "sha512-tf7rxO6Haw/sgMQZEDCQn/7QcoEGHJmhK8xVZ2PMW4iux8ZEHIfJcFsnlje6nYPvtOghxzKnMzr1D0V2Wj70hQ==", "license": "Apache-2.0", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 4bede9e..d60a6cc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ros2_medkit_web_ui", "private": true, - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "Simple web UI for browsing SOVD entity trees via discovery endpoints", "repository": { @@ -42,7 +42,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.2.8", - "@selfpatch/ros2-medkit-client-ts": "^0.6.0", + "@selfpatch/ros2-medkit-client-ts": "^0.7.0", "@tailwindcss/vite": "^4.1.14", "@uiw/react-codemirror": "^4.25.11", "class-variance-authority": "^0.7.1", diff --git a/src/lib/api-dispatch.ts b/src/lib/api-dispatch.ts index f38183e..29c1fb7 100644 --- a/src/lib/api-dispatch.ts +++ b/src/lib/api-dispatch.ts @@ -21,7 +21,7 @@ * typed path based on the entity type string. */ -import type { MedkitClient } from '@selfpatch/ros2-medkit-client-ts'; +import type { MedkitClient, paths } from '@selfpatch/ros2-medkit-client-ts'; import type { SovdResourceEntityType, LifecycleAction, ScriptEntityType, StartScriptExecutionRequest } from './types'; import type { LogsQueryParams, LogsConfiguration } from './log-types'; @@ -730,13 +730,19 @@ export function getEntityScript( } } +/** The upload body both script paths declare: a required `file` part plus optional `metadata`. */ +type ScriptUploadBody = NonNullable< + paths['/apps/{app_id}/scripts']['post']['requestBody'] +>['content']['multipart/form-data']; + /** * Multipart upload. * - * The spec declares the body as `{type: object, additionalProperties: true}`, so - * the generated type is `{ [key: string]: unknown }` and FormData (a DOM interface) - * is not assignable to it. bodySerializer returns the FormData unchanged so fetch - * sets Content-Type with the multipart boundary itself - the gateway rejects the + * FormData is a DOM interface, so it is never assignable to the generated body + * object however the spec describes it - hence the cast, which names the real + * body type so a change to the declared parts is a type error here rather than + * a runtime 400. bodySerializer returns the FormData unchanged so fetch sets + * Content-Type with the multipart boundary itself - the gateway rejects the * request without it. */ export function uploadEntityScript( @@ -745,7 +751,7 @@ export function uploadEntityScript( entityId: string, form: FormData ) { - const body = form as unknown as Record; + const body = form as unknown as ScriptUploadBody; const bodySerializer = (value: unknown) => value as FormData; switch (entityType) { case 'apps': @@ -784,9 +790,8 @@ export function deleteEntityScript( /** * Start an execution. * - * The spec declares this request body as a bare `type: object`, so the generated - * type is `Record` and any real body fails the type check. The cast - * keeps the runtime payload correct; removing it requires a spec fix in the gateway. + * StartScriptExecutionRequest mirrors the generated ScriptExecutionRequest, so the + * body passes straight through and a divergence between the two shows up here. */ export function startScriptExecution( client: MedkitClient, @@ -795,7 +800,7 @@ export function startScriptExecution( scriptId: string, request: StartScriptExecutionRequest ) { - const body = request as unknown as Record; + const body = request; switch (entityType) { case 'apps': return client.POST('/apps/{app_id}/scripts/{script_id}/executions', {