Skip to content

feat(core): named/shared schemas + flatten response allOf#343

Draft
Joehoel wants to merge 5 commits into
alchemy-run:mainfrom
Joehoel:feat/openapi-named-schemas
Draft

feat(core): named/shared schemas + flatten response allOf#343
Joehoel wants to merge 5 commits into
alchemy-run:mainfrom
Joehoel:feat/openapi-named-schemas

Conversation

@Joehoel

@Joehoel Joehoel commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Emit each OpenAPI definition once as a named/shared schema instead of inlining it at every $ref, then flatten allOf in response schemas so create/get operations return the full provisioned resource.

Stacked on #341 (the Azure LRO PR) — the first three commits are that PR and drop out once it merges.

Why

The generator inlined every $ref at each use site. For services that concatenate many operations into one file (Azure: ~50 ops in storage.ts), inlining a deep resource schema per operation made tsgo blow up — so response schemas had to be left as { id, name, type } stubs, which in turn meant the LRO poller resolved the full resource only for the output schema to strip it back.

What changed

  • Named/shared schemas. Each definition is emitted once as a const (in a shared _schemas.ts; inlined once into the service file for Azure's concatenated layout) and referenced via Schema.suspend(() => XSchema). Cyclic back-edges degrade to Schema.Unknown (same as the existing inline seenRefs guard) — which also breaks the type-level recursion, so no hand-written Schema.suspend annotations are needed. Each schema is type-checked once.
  • Response allOf flattening. With inlining gone, generateOutputSchema now flattens the top-level allOf inheritance chain, so outputs carry the resource's own + inherited properties:
- StorageAccountsCreateOutput = { id, name, type, systemData }
+ StorageAccountsCreateOutput = { id, name, type, sku, kind,
+   properties: { provisioningState, … }, … }   // via shared StorageAccountPropertiesSchema

Regenerated Azure (27M, smaller than the prior 32M thanks to dedup) and Neon. Other OpenAPI packages keep their committed output until next regenerated.

Validation

  • tsgo clean locally on Neon (the per-operation path) — proves the named-schema + suspend mechanism compiles.
  • Azure: oxlint 0 errors, oxfmt --check clean, and the live storage smoke test now asserts provisioningState: "Succeeded" end-to-end.
  • Azure's full tsgo is large; it runs in CI here (the deduped output is smaller than the baseline that already passed CI).

Joehoel added 5 commits June 12, 2026 12:09
Core gains a `longRunning` Http trait and an optional `pollLongRunning`
client hook: when an operation carries the trait and the server returns a
201/202 ack, core delegates to the hook and decodes its resolved body
through the output schema.

Azure implements the hook as an Effect-native ARM poller (Effect.repeat +
Schedule honoring per-response Retry-After; Data.taggedEnum strategy +
provisioning-state classification via Match). Supports azure-async-operation,
location, and original-uri final-state resolution. Terminal Failed/Canceled
surfaces as AzureLongRunningOperationFailed (uncategorized, so it is not
auto-retried).

Tested account-free with a scripted fake HttpClient (incl. Retry-After via
TestClock).
The shared Swagger emitter now detects `x-ms-long-running-operation` (+
`-options.final-state-via`) on each operation and bakes
`longRunning: { finalStateVia }` into the generated `T.Http` trait — the
same place `apiVersion` is injected. Regenerated all Azure services: 3783
long-running operations across 182 service files now poll to completion.
The ARM `final-state-via: location` for a resource create points to an
operationResults URL that returns only a stub (`{id, name, type}`); the
provisioned resource lives at the original request URI. Mirror
@azure/core-lro's `findResourceLocation`: PUT/PATCH (and any `original-uri`
op) resolve via the original URI; POST/DELETE use the terminal poll body.

Caught by a live smoke test (real storage-account create on the non-profit
account, free Standard_LRS, torn down) — now included.
The OpenAPI generator inlined every `$ref` at each use site. For services that
concatenate many operations into one file (Azure), inlining deep resource
schemas per operation made `tsgo` blow up — which is why response schemas were
left as `{id, name, type}` stubs.

Now each definition is emitted once as a named const (in a shared `_schemas.ts`,
inlined into the service file for Azure) and referenced via
`Schema.suspend(() => XSchema)`; cyclic back-edges degrade to `Schema.Unknown`
(same as the inline `seenRefs` guard), which also breaks the type-level
recursion so no hand-written annotations are needed. Each schema is type-checked
once, so the deep shapes are now affordable.

With that in place, `generateOutputSchema` flattens the top-level response
`allOf` chain, so create/get operations return the full provisioned resource
(`properties.provisioningState`, `sku`, `kind`, …) instead of a stub — mirroring
PR alchemy-run#324's request-body `allOf` fix, for responses.

Regenerated Azure (smaller: 27M vs the prior 32M, deduped) and Neon. The live
Azure storage smoke test now asserts `provisioningState: "Succeeded"`. Verified
`tsgo` locally on Neon (the per-operation path); Azure's full type-check runs in
CI.
Apply the named/shared-schema generator to the other OpenAPI-based SDKs so they
match the new generator output (azure + neon were done in the previous commit).
Each definition is emitted once in a `_schemas.ts` and referenced via
`Schema.suspend(() => XSchema)`.

Also fixes `kubernetes/scripts/generate.ts` (its own concatenating assembler,
like azure): exclude `_schemas.ts` from the operation-file scan and import the
shared schemas into each service file instead of inlining them.

tsgo verified locally: axiom, coinbase, fly-io, kubernetes, mongodb-atlas,
planetscale, prisma-postgres, supabase, turso, typesense, workos. posthog and
stripe (large, per-operation layout) are left to CI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant