refactor: strip optional null from all component schemas, not just request bodies
Extends stripOptionalNull to run over every component schema, closing ~200 null-vs-undefined mismatches between Hono and HttpApi SDK output. Removes LegacyBodyRefParameters since components are now pre-cleaned. Adds scripts/diff-sdk-types.sh for comparing Hono vs HttpApi SDK types.
This commit is contained in:
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
# Compare SDK types generated from Hono vs HttpApi specs.
|
||||
# Sorts types alphabetically so only meaningful body differences show.
|
||||
#
|
||||
# Usage: ./scripts/diff-sdk-types.sh # full diff
|
||||
# ./scripts/diff-sdk-types.sh --stat # summary only
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SDK="$(cd "$DIR/../sdk/js" && pwd)"
|
||||
|
||||
normalize() {
|
||||
python3 -c "
|
||||
import re, sys
|
||||
content = open(sys.argv[1]).read()
|
||||
blocks = re.split(r'(?=^export (?:type|function|const) )', content, flags=re.MULTILINE)
|
||||
header, body = blocks[0], blocks[1:]
|
||||
body.sort(key=lambda b: m.group(1) if (m := re.match(r'export \w+ (\w+)', b)) else '')
|
||||
sys.stdout.write(header + ''.join(body))
|
||||
" "$1"
|
||||
}
|
||||
|
||||
echo "Generating Hono SDK..." >&2
|
||||
(cd "$SDK" && bun run script/build.ts >/dev/null 2>&1)
|
||||
normalize "$SDK/src/v2/gen/types.gen.ts" > /tmp/sdk-types-hono.ts
|
||||
git -C "$SDK" checkout -- src/ 2>/dev/null
|
||||
|
||||
echo "Generating HttpApi SDK..." >&2
|
||||
(cd "$SDK" && OPENCODE_SDK_OPENAPI=httpapi bun run script/build.ts >/dev/null 2>&1)
|
||||
normalize "$SDK/src/v2/gen/types.gen.ts" > /tmp/sdk-types-httpapi.ts
|
||||
git -C "$SDK" checkout -- src/ 2>/dev/null
|
||||
|
||||
echo "" >&2
|
||||
if [[ "${1:-}" == "--stat" ]]; then
|
||||
honly=$(diff /tmp/sdk-types-hono.ts /tmp/sdk-types-httpapi.ts | grep -c '^< export type' || true)
|
||||
aonly=$(diff /tmp/sdk-types-hono.ts /tmp/sdk-types-httpapi.ts | grep -c '^> export type' || true)
|
||||
total=$(diff /tmp/sdk-types-hono.ts /tmp/sdk-types-httpapi.ts | wc -l | tr -d ' ')
|
||||
echo "Hono-only: $honly types HttpApi-only: $aonly types Diff lines: $total"
|
||||
echo ""
|
||||
if [[ $honly -gt 0 ]]; then
|
||||
echo "=== Hono-only types ==="
|
||||
diff /tmp/sdk-types-hono.ts /tmp/sdk-types-httpapi.ts | grep '^< export type' | sed 's/< export type / /' | sed 's/ .*//'
|
||||
echo ""
|
||||
fi
|
||||
if [[ $aonly -gt 0 ]]; then
|
||||
echo "=== HttpApi-only types ==="
|
||||
diff /tmp/sdk-types-hono.ts /tmp/sdk-types-httpapi.ts | grep '^> export type' | sed 's/> export type / /' | sed 's/ .*//'
|
||||
fi
|
||||
else
|
||||
diff /tmp/sdk-types-hono.ts /tmp/sdk-types-httpapi.ts || true
|
||||
fi
|
||||
@@ -58,10 +58,6 @@ const InstanceQueryParameters = [
|
||||
},
|
||||
] satisfies OpenApiParameter[]
|
||||
|
||||
// These refs already match the legacy SDK's expected body shape. Expanding all
|
||||
// other refs lets us strip Effect's `null` from optional fields in one place.
|
||||
const LegacyBodyRefParameters = new Set(["Auth", "Config", "Part", "WorktreeRemoveInput", "WorktreeResetInput"])
|
||||
|
||||
// Query schemas describe decoded Effect values, but the generated SDK needs the
|
||||
// public call shape. These keep SDK callers passing numbers/booleans while the
|
||||
// server still decodes string query params at runtime.
|
||||
@@ -82,6 +78,13 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
|
||||
// actual schema from any parent union that references them.
|
||||
fixSelfReferencingComponents(spec)
|
||||
|
||||
// Effect's Schema.optional emits `anyOf: [T, {type:"null"}]` in OpenAPI,
|
||||
// but the legacy SDK expected plain `T` for optional fields. Strip null
|
||||
// from all component schemas so both request and response types match.
|
||||
for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
|
||||
spec.components!.schemas![name] = stripOptionalNull(structuredClone(schema))
|
||||
}
|
||||
|
||||
for (const [path, item] of Object.entries(spec.paths ?? {})) {
|
||||
const isInstanceRoute = !path.startsWith("/global/") && !path.startsWith("/auth/")
|
||||
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
|
||||
@@ -91,23 +94,12 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
|
||||
// Hono's generated OpenAPI never marked request bodies as required. Keep
|
||||
// that SDK surface stable during the HttpApi migration.
|
||||
delete operation.requestBody.required
|
||||
// Effect's Schema.optional emits `anyOf: [T, {type:"null"}]` in OpenAPI,
|
||||
// but the legacy SDK expected plain `T` for optional fields. Expand
|
||||
// non-legacy $refs and strip the null arms so the SDK surface is stable.
|
||||
for (const media of Object.values(operation.requestBody.content ?? {})) {
|
||||
const ref = media.schema?.$ref?.replace("#/components/schemas/", "")
|
||||
if (ref && LegacyBodyRefParameters.has(ref)) continue
|
||||
if (ref && spec.components?.schemas?.[ref]) {
|
||||
media.schema = stripOptionalNull(structuredClone(spec.components.schemas[ref]))
|
||||
continue
|
||||
}
|
||||
if (media.schema) media.schema = stripOptionalNull(media.schema)
|
||||
}
|
||||
if (path === "/experimental/workspace" && method === "post") {
|
||||
// Workspace creation fields `branch` and `extra` are Schema.NullOr —
|
||||
// genuinely nullable, not just optional. Re-add the null that the
|
||||
// global strip above removed.
|
||||
const properties = operation.requestBody.content?.["application/json"]?.schema?.properties
|
||||
// component-level strip above removed.
|
||||
const ref = operation.requestBody.content?.["application/json"]?.schema?.$ref?.replace("#/components/schemas/", "")
|
||||
const properties = ref ? spec.components?.schemas?.[ref]?.properties : operation.requestBody.content?.["application/json"]?.schema?.properties
|
||||
if (properties?.branch) properties.branch = { anyOf: [properties.branch, { type: "null" }] }
|
||||
if (properties?.extra) properties.extra = { anyOf: [properties.extra, { type: "null" }] }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user