Summary
@dexpace/core ships the assembly half of a code-generation target but not the
execution half. buildRequest turns an OperationDescriptor into a Request, and
Runtime, Paginator, typedSseStream, decodeSuccessResponse, and the auth steps each
do their job — but there is no seam that binds them, so a generated client has nothing to
delegate to and would have to hand-roll the wiring in every service SDK.
The Python SDK closed this with dexpace.sdk.core.codegen (see
docs/codegen-target.md),
whose contract is: the generator emits data and delegation, never logic. An operation
table plus a facade whose every method binds arguments into an input object and calls a
shared executor. All behaviour — request assembly, pipeline, retry, auth resolution, error
mapping, pagination, SSE — stays in core.
This issue proposes the Node equivalent, and proposes proving it with a compiling witness
before committing to the API.
What's missing
-
No executor tier. Nothing exports an object with execute / executeRequest /
paginate / events plus ownership-aware close (an owned transport is closed, a
borrowed runtime is not). Runtime already implements Transport, so this layer is
thin — but it does not exist.
-
No declarative status-to-error map. toHttpError produces exactly one class,
HttpStatusError. Every service SDK would grow its own hand-written
if (status === ...) throw ... chain — precisely what the Python StatusErrorMap
exists to prevent. Python additionally enforces at construction that a mapped class
extends the response-error base and is disjoint from the transport-error branch, so a
service error can never masquerade as a transport failure. Node needs the equivalent
guarantee stated and enforced.
-
OperationDescriptor merges the static and per-call halves. It carries
pathParams, query, headers, and body inline, so it cannot be a module-level
constant reused across calls the way a generated operation table needs. Python splits
this into a frozen Operation (static) and an OperationInput (per-call). Resolving
this is a compatibility question on an already-@public interface, so it wants a
decision, not a patch.
-
The operation auth tier has no source. docs/deferred-items.md records it as
unscoped because "nothing in this roadmap ships a per-operation layer (no
codegen/client surface)". perCall (via RequestOptions.auth) and client are both
satisfiable today. A generated client is that missing surface, and it needs the
strict resolution rule Python states: presence selects the tier, not satisfiability
— a present-but-unsatisfiable tier fails loudly and never falls through to a broader
one, so a caller who asked for a specific credential never gets a silent downgrade to
the client default.
-
FakeTransport and countingResponse are unreachable. They live in
packages/core/src/testing/ and are not re-exported from the package entry point.
Anyone testing against the public surface either deep-imports src/ — loading core
twice and breaking every cross-boundary instanceof — or rewrites a fake transport.
Worth deciding whether the testing helpers are public API.
Proposed approach
Build a throwaway examples/petstore/ spike first, outside packages/ and tests/ so no
CI script, bunfig glob, partition gate, or api-extractor report is touched. It mirrors the
Python witness: the same frozen OpenAPI document, a deterministic generator, a
projection-only facade, a regen-diff check, and an end-to-end canary over an in-memory
transport covering unary calls, a merge-patch body with three-valued fields, cursor
pagination, SSE with a [DONE] sentinel, all three auth-tier outcomes, and both close
semantics.
The spike's output is a findings list: for each gap above, what had to be hand-written and
whether it belongs in packages/core/src/codegen/. Only then does the core API get
designed — with a working consumer already in hand rather than guessed at.
Non-goals
- Shipping a code generator. The generator here is a fixture that renders one frozen
document; a real one is a separate project.
- Generating models from
components/schemas. Models stay hand-written, exactly as in the
Python example — the generator reads operationId, method, path template, and the
x-dexpace vendor extension, nothing else.
- Making the example a published package or a CI-visible test.
References
- Python contract:
python-sdk/docs/codegen-target.md
- Python witness:
python-sdk/examples/petstore/
packages/core/src/seams/operation.ts — the half that exists
docs/deferred-items.md — the operation auth tier, recorded as unscoped
Summary
@dexpace/coreships the assembly half of a code-generation target but not theexecution half.
buildRequestturns anOperationDescriptorinto aRequest, andRuntime,Paginator,typedSseStream,decodeSuccessResponse, and the auth steps eachdo their job — but there is no seam that binds them, so a generated client has nothing to
delegate to and would have to hand-roll the wiring in every service SDK.
The Python SDK closed this with
dexpace.sdk.core.codegen(seedocs/codegen-target.md),whose contract is: the generator emits data and delegation, never logic. An operation
table plus a facade whose every method binds arguments into an input object and calls a
shared executor. All behaviour — request assembly, pipeline, retry, auth resolution, error
mapping, pagination, SSE — stays in core.
This issue proposes the Node equivalent, and proposes proving it with a compiling witness
before committing to the API.
What's missing
No executor tier. Nothing exports an object with
execute/executeRequest/paginate/eventsplus ownership-aware close (an owned transport is closed, aborrowed runtime is not).
Runtimealready implementsTransport, so this layer isthin — but it does not exist.
No declarative status-to-error map.
toHttpErrorproduces exactly one class,HttpStatusError. Every service SDK would grow its own hand-writtenif (status === ...) throw ...chain — precisely what the PythonStatusErrorMapexists to prevent. Python additionally enforces at construction that a mapped class
extends the response-error base and is disjoint from the transport-error branch, so a
service error can never masquerade as a transport failure. Node needs the equivalent
guarantee stated and enforced.
OperationDescriptormerges the static and per-call halves. It carriespathParams,query,headers, andbodyinline, so it cannot be a module-levelconstant reused across calls the way a generated operation table needs. Python splits
this into a frozen
Operation(static) and anOperationInput(per-call). Resolvingthis is a compatibility question on an already-
@publicinterface, so it wants adecision, not a patch.
The
operationauth tier has no source.docs/deferred-items.mdrecords it asunscoped because "nothing in this roadmap ships a per-operation layer (no
codegen/client surface)".
perCall(viaRequestOptions.auth) andclientare bothsatisfiable today. A generated client is that missing surface, and it needs the
strict resolution rule Python states: presence selects the tier, not satisfiability
— a present-but-unsatisfiable tier fails loudly and never falls through to a broader
one, so a caller who asked for a specific credential never gets a silent downgrade to
the client default.
FakeTransportandcountingResponseare unreachable. They live inpackages/core/src/testing/and are not re-exported from the package entry point.Anyone testing against the public surface either deep-imports
src/— loadingcoretwice and breaking every cross-boundary
instanceof— or rewrites a fake transport.Worth deciding whether the testing helpers are public API.
Proposed approach
Build a throwaway
examples/petstore/spike first, outsidepackages/andtests/so noCI script, bunfig glob, partition gate, or api-extractor report is touched. It mirrors the
Python witness: the same frozen OpenAPI document, a deterministic generator, a
projection-only facade, a regen-diff check, and an end-to-end canary over an in-memory
transport covering unary calls, a merge-patch body with three-valued fields, cursor
pagination, SSE with a
[DONE]sentinel, all three auth-tier outcomes, and both closesemantics.
The spike's output is a findings list: for each gap above, what had to be hand-written and
whether it belongs in
packages/core/src/codegen/. Only then does the core API getdesigned — with a working consumer already in hand rather than guessed at.
Non-goals
document; a real one is a separate project.
components/schemas. Models stay hand-written, exactly as in thePython example — the generator reads
operationId, method, path template, and thex-dexpacevendor extension, nothing else.References
python-sdk/docs/codegen-target.mdpython-sdk/examples/petstore/packages/core/src/seams/operation.ts— the half that existsdocs/deferred-items.md— the operation auth tier, recorded as unscoped