fix(sdk): preserve SSE event payload types

This commit is contained in:
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 2026-06-26 20:37:53 +00:00
commit 8a8b572e9f
4 changed files with 43 additions and 22 deletions

View file

@ -34,6 +34,8 @@ type OpenApiSchema = {
additionalProperties?: OpenApiSchema | boolean additionalProperties?: OpenApiSchema | boolean
allOf?: OpenApiSchema[] allOf?: OpenApiSchema[]
anyOf?: OpenApiSchema[] anyOf?: OpenApiSchema[]
contentMediaType?: string
contentSchema?: OpenApiSchema
description?: string description?: string
enum?: Array<string | boolean> enum?: Array<string | boolean>
items?: OpenApiSchema items?: OpenApiSchema
@ -88,6 +90,7 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
// payload and inside an annotated union arm. Resolve these by inlining the // payload and inside an annotated union arm. Resolve these by inlining the
// actual schema from any parent union that references them. // actual schema from any parent union that references them.
fixSelfReferencingComponents(spec) fixSelfReferencingComponents(spec)
collapseJsonSseComponents(spec)
// Effect's Schema.optional emits `anyOf: [T, {type:"null"}]` in OpenAPI, // Effect's Schema.optional emits `anyOf: [T, {type:"null"}]` in OpenAPI,
// but the legacy SDK expected plain `T` for optional fields. Strip null // but the legacy SDK expected plain `T` for optional fields. Strip null
@ -229,6 +232,26 @@ function collapseDuplicateComponents(spec: OpenApiSpec) {
} }
} }
function collapseJsonSseComponents(spec: OpenApiSpec) {
const schemas = spec.components?.schemas
if (!schemas) return
for (const [name, schema] of Object.entries(schemas)) {
const ref = schema.contentMediaType === "application/json" ? schema.contentSchema?.$ref : undefined
const target = ref?.replace("#/components/schemas/", "")
if (schema.type !== "string" || !target || !schemas[target]) continue
// Effect represents JSON SSE payloads as encoded string schemas, but the
// generated SDK's SSE client already decodes them. Keep the payload union
// under its stable, unsuffixed component name instead of exposing `string`.
const canonical = name.replace(/\d+$/, "")
if (target.replace(/\d+$/, "") !== canonical) continue
schemas[canonical] = schemas[target]
rewriteRefs(spec, name, canonical)
rewriteRefs(spec, target, canonical)
if (name !== canonical) delete schemas[name]
if (target !== canonical) delete schemas[target]
}
}
function normalizeComponentNames(spec: OpenApiSpec) { function normalizeComponentNames(spec: OpenApiSpec) {
const schemas = spec.components?.schemas const schemas = spec.components?.schemas
if (!schemas) return if (!schemas) return

View file

@ -99,6 +99,22 @@ describe("PublicApi OpenAPI v2 errors", () => {
}) })
}) })
test("exposes decoded SSE payload schemas", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
expect(spec.paths["/api/event"]?.get?.responses?.["200"]?.content?.["text/event-stream"]?.schema?.$ref).toBe(
"#/components/schemas/V2Event",
)
expect(spec.components.schemas.V2Event?.anyOf?.length).toBeGreaterThan(0)
expect(spec.components.schemas.V2Event1).toBeUndefined()
expect(
spec.paths["/api/session/{sessionID}/event"]?.get?.responses?.["200"]?.content?.["text/event-stream"]?.schema
?.properties?.data?.$ref,
).toBe("#/components/schemas/SessionDurableEvent")
expect(spec.components.schemas.SessionDurableEvent1).toBeUndefined()
})
test("preserves /api auth responses", () => { test("preserves /api auth responses", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec

View file

@ -2766,8 +2766,6 @@ export type SessionHistory = {
hasMore: boolean hasMore: boolean
} }
export type SessionDurableEvent1 = string
export type SessionMessagesResponse = { export type SessionMessagesResponse = {
data: Array<SessionMessage> data: Array<SessionMessage>
cursor: { cursor: {
@ -2847,7 +2845,7 @@ export type QuestionRejected2 = {
} }
} }
export type V2Event1 = export type V2Event =
| ModelsDevRefreshed | ModelsDevRefreshed
| IntegrationUpdated | IntegrationUpdated
| IntegrationConnectionUpdated | IntegrationConnectionUpdated
@ -2937,8 +2935,6 @@ export type V2Event1 =
| ServerConnected | ServerConnected
| GlobalDisposed | GlobalDisposed
export type V2Event = string
export type ForbiddenError = { export type ForbiddenError = {
_tag: "ForbiddenError" _tag: "ForbiddenError"
message: string message: string
@ -11897,7 +11893,7 @@ export type V2SessionEventsResponses = {
200: { 200: {
id: string id: string
event: string event: string
data: SessionDurableEvent1 data: SessionDurableEvent
} }
} }

View file

@ -11260,7 +11260,7 @@
"type": "string" "type": "string"
}, },
"data": { "data": {
"$ref": "#/components/schemas/SessionDurableEvent1" "$ref": "#/components/schemas/SessionDurableEvent"
} }
}, },
"required": ["id", "event", "data"], "required": ["id", "event", "data"],
@ -23732,13 +23732,6 @@
"required": ["data", "hasMore"], "required": ["data", "hasMore"],
"additionalProperties": false "additionalProperties": false
}, },
"SessionDurableEvent1": {
"type": "string",
"contentSchema": {
"$ref": "#/components/schemas/SessionDurableEvent"
},
"contentMediaType": "application/json"
},
"SessionMessagesResponse": { "SessionMessagesResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -23975,7 +23968,7 @@
"required": ["id", "type", "data"], "required": ["id", "type", "data"],
"additionalProperties": false "additionalProperties": false
}, },
"V2Event1": { "V2Event": {
"anyOf": [ "anyOf": [
{ {
"$ref": "#/components/schemas/Models-devRefreshed" "$ref": "#/components/schemas/Models-devRefreshed"
@ -24243,13 +24236,6 @@
} }
] ]
}, },
"V2Event": {
"type": "string",
"contentSchema": {
"$ref": "#/components/schemas/V2Event1"
},
"contentMediaType": "application/json"
},
"ForbiddenError": { "ForbiddenError": {
"type": "object", "type": "object",
"properties": { "properties": {