@lovrozagar/honey 0.6.5 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +514 -192
- package/bin/honey.js +25 -0
- package/dist/accepts.d.ts +7 -1
- package/dist/accepts.d.ts.map +1 -1
- package/dist/accepts.js +126 -0
- package/dist/accepts.js.map +1 -0
- package/dist/body-kind.d.ts +26 -0
- package/dist/body-kind.d.ts.map +1 -0
- package/dist/body-kind.js +70 -0
- package/dist/body-kind.js.map +1 -0
- package/dist/body-limit.d.ts +5 -1
- package/dist/body-limit.d.ts.map +1 -1
- package/dist/body-limit.js +126 -0
- package/dist/body-limit.js.map +1 -0
- package/dist/build/index.d.ts +19 -3
- package/dist/build/index.d.ts.map +1 -1
- package/dist/build/index.js +173 -0
- package/dist/build/index.js.map +1 -0
- package/dist/cli-args.d.ts +33 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +102 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli-go/auth.go +26 -0
- package/dist/cli-go/config.go +192 -0
- package/dist/cli-go/errors.go +75 -0
- package/dist/cli-go/multipart.go +79 -0
- package/dist/cli-go/output.go +163 -0
- package/dist/cli-go/root.go +30 -0
- package/dist/cli-go/stream.go +88 -0
- package/dist/cli-go/version.go +5 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +209 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/defaults.js +48 -0
- package/dist/client/defaults.js.map +1 -0
- package/dist/client/error.d.ts +35 -0
- package/dist/client/error.d.ts.map +1 -1
- package/dist/client/error.js +71 -0
- package/dist/client/error.js.map +1 -0
- package/dist/client/http.d.ts +67 -8
- package/dist/client/http.d.ts.map +1 -1
- package/dist/client/http.js +571 -0
- package/dist/client/http.js.map +1 -0
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +95 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/path.d.ts +26 -0
- package/dist/client/path.d.ts.map +1 -0
- package/dist/client/path.js +163 -0
- package/dist/client/path.js.map +1 -0
- package/dist/client/redirect-policy.d.ts +12 -0
- package/dist/client/redirect-policy.d.ts.map +1 -0
- package/dist/client/redirect-policy.js +20 -0
- package/dist/client/redirect-policy.js.map +1 -0
- package/dist/client/sdk.d.ts +36 -0
- package/dist/client/sdk.d.ts.map +1 -1
- package/dist/client/sdk.js +292 -0
- package/dist/client/sdk.js.map +1 -0
- package/dist/client/sse.d.ts +13 -0
- package/dist/client/sse.d.ts.map +1 -1
- package/dist/client/sse.js +129 -0
- package/dist/client/sse.js.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/ws.d.ts +9 -2
- package/dist/client/ws.d.ts.map +1 -1
- package/dist/client/ws.js +112 -0
- package/dist/client/ws.js.map +1 -0
- package/dist/client-go/errors.go +236 -0
- package/dist/client-go/export.go +44 -0
- package/dist/client-go/invalidation.go +371 -0
- package/dist/client-go/nullable.go +62 -0
- package/dist/client-go/realtime.go +354 -0
- package/dist/client-go/result.go +20 -0
- package/dist/client-go/runtime.go +975 -0
- package/dist/client-go/sse.go +159 -0
- package/dist/client-go/transport.go +495 -0
- package/dist/client-go/ws.go +128 -0
- package/dist/client-info.d.ts +36 -0
- package/dist/client-info.d.ts.map +1 -0
- package/dist/client-info.js +53 -0
- package/dist/client-info.js.map +1 -0
- package/dist/client-mcp/README.md +10 -0
- package/dist/client-mcp/server.ts +221 -0
- package/dist/client-mcp/types.ts +19 -0
- package/dist/client-python/_errors.py +115 -0
- package/dist/client-python/_invalidation.py +494 -0
- package/dist/client-python/_realtime.py +179 -0
- package/dist/client-python/_runtime.py +860 -0
- package/dist/client-python/_sse.py +155 -0
- package/dist/client-python/_transport.py +366 -0
- package/dist/client-python/_ws.py +159 -0
- package/dist/client-python/py.typed +0 -0
- package/dist/client-rust/errors.rs +220 -0
- package/dist/client-rust/invalidation.rs +412 -0
- package/dist/client-rust/invalidation_sync.rs +93 -0
- package/dist/client-rust/nullable.rs +17 -0
- package/dist/client-rust/realtime.rs +702 -0
- package/dist/client-rust/result.rs +22 -0
- package/dist/client-rust/runtime.rs +980 -0
- package/dist/client-rust/runtime_sync.rs +484 -0
- package/dist/client-rust/sse.rs +188 -0
- package/dist/client-rust/ws.rs +167 -0
- package/dist/codegen-go-cli.d.ts.map +1 -1
- package/dist/codegen-go-cli.js +964 -0
- package/dist/codegen-go-cli.js.map +1 -0
- package/dist/codegen-go.d.ts +15 -3
- package/dist/codegen-go.d.ts.map +1 -1
- package/dist/codegen-go.js +872 -0
- package/dist/codegen-go.js.map +1 -0
- package/dist/codegen-hash.js +9 -0
- package/dist/codegen-hash.js.map +1 -0
- package/dist/codegen-ir.d.ts +69 -2
- package/dist/codegen-ir.d.ts.map +1 -1
- package/dist/codegen-ir.js +661 -0
- package/dist/codegen-ir.js.map +1 -0
- package/dist/codegen-lang.d.ts +65 -0
- package/dist/codegen-lang.d.ts.map +1 -0
- package/dist/codegen-lang.js +526 -0
- package/dist/codegen-lang.js.map +1 -0
- package/dist/codegen-load.d.ts +31 -0
- package/dist/codegen-load.d.ts.map +1 -0
- package/dist/codegen-load.js +138 -0
- package/dist/codegen-load.js.map +1 -0
- package/dist/codegen-loaders.js +55 -0
- package/dist/codegen-loaders.js.map +1 -0
- package/dist/codegen-mcp.d.ts.map +1 -1
- package/dist/codegen-mcp.js +368 -0
- package/dist/codegen-mcp.js.map +1 -0
- package/dist/codegen-python.d.ts +13 -1
- package/dist/codegen-python.d.ts.map +1 -1
- package/dist/codegen-python.js +828 -0
- package/dist/codegen-python.js.map +1 -0
- package/dist/codegen-route-tree-intern.d.ts +15 -5
- package/dist/codegen-route-tree-intern.d.ts.map +1 -1
- package/dist/codegen-route-tree-intern.js +221 -0
- package/dist/codegen-route-tree-intern.js.map +1 -0
- package/dist/codegen-rust.d.ts.map +1 -1
- package/dist/codegen-rust.js +1293 -0
- package/dist/codegen-rust.js.map +1 -0
- package/dist/codegen-sanitize.d.ts.map +1 -1
- package/dist/codegen-sanitize.js +56 -0
- package/dist/codegen-sanitize.js.map +1 -0
- package/dist/codegen-schema-naming.d.ts +5 -0
- package/dist/codegen-schema-naming.d.ts.map +1 -1
- package/dist/codegen-schema-naming.js +150 -0
- package/dist/codegen-schema-naming.js.map +1 -0
- package/dist/codegen-sdk-model.d.ts +71 -0
- package/dist/codegen-sdk-model.d.ts.map +1 -0
- package/dist/codegen-sdk-model.js +179 -0
- package/dist/codegen-sdk-model.js.map +1 -0
- package/dist/codegen.d.ts +41 -23
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +4610 -0
- package/dist/codegen.js.map +1 -0
- package/dist/context.d.ts +36 -6
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +242 -0
- package/dist/context.js.map +1 -0
- package/dist/cookie-sign.d.ts +16 -2
- package/dist/cookie-sign.d.ts.map +1 -1
- package/dist/cookie-sign.js +104 -0
- package/dist/cookie-sign.js.map +1 -0
- package/dist/cookie.d.ts +12 -0
- package/dist/cookie.d.ts.map +1 -1
- package/dist/cookie.js +138 -0
- package/dist/cookie.js.map +1 -0
- package/dist/cors.d.ts +1 -0
- package/dist/cors.d.ts.map +1 -1
- package/dist/cors.js +87 -0
- package/dist/cors.js.map +1 -0
- package/dist/crypto.d.ts +6 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +38 -0
- package/dist/crypto.js.map +1 -0
- package/dist/csrf.d.ts +19 -0
- package/dist/csrf.d.ts.map +1 -1
- package/dist/csrf.js +79 -0
- package/dist/csrf.js.map +1 -0
- package/dist/curl-logger.d.ts +14 -3
- package/dist/curl-logger.d.ts.map +1 -1
- package/dist/curl-logger.js +199 -0
- package/dist/curl-logger.js.map +1 -0
- package/dist/detect-runtime.js +13 -0
- package/dist/detect-runtime.js.map +1 -0
- package/dist/dict.d.ts +11 -0
- package/dist/dict.d.ts.map +1 -0
- package/dist/dict.js +13 -0
- package/dist/dict.js.map +1 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +44 -0
- package/dist/error.js.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/errors.js.map +1 -0
- package/dist/etag.d.ts +2 -0
- package/dist/etag.d.ts.map +1 -1
- package/dist/etag.js +82 -0
- package/dist/etag.js.map +1 -0
- package/dist/feature-detect.d.ts +25 -0
- package/dist/feature-detect.d.ts.map +1 -0
- package/dist/feature-detect.js +144 -0
- package/dist/feature-detect.js.map +1 -0
- package/dist/feature-load.js +36 -0
- package/dist/feature-load.js.map +1 -0
- package/dist/feature-slots.d.ts +44 -0
- package/dist/feature-slots.d.ts.map +1 -0
- package/dist/feature-slots.js +48 -0
- package/dist/feature-slots.js.map +1 -0
- package/dist/fetch-request.d.ts +10 -0
- package/dist/fetch-request.d.ts.map +1 -0
- package/dist/fetch-request.js +13 -0
- package/dist/fetch-request.js.map +1 -0
- package/dist/gen-process.d.ts +15 -0
- package/dist/gen-process.d.ts.map +1 -0
- package/dist/gen-process.js +43 -0
- package/dist/gen-process.js.map +1 -0
- package/dist/gen-write.d.ts +19 -0
- package/dist/gen-write.d.ts.map +1 -0
- package/dist/gen-write.js +120 -0
- package/dist/gen-write.js.map +1 -0
- package/dist/glob.d.ts +21 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +115 -0
- package/dist/glob.js.map +1 -0
- package/dist/go-type-emitter.d.ts +18 -4
- package/dist/go-type-emitter.d.ts.map +1 -1
- package/dist/go-type-emitter.js +594 -0
- package/dist/go-type-emitter.js.map +1 -0
- package/dist/honey-response.d.ts.map +1 -1
- package/dist/honey-response.js +338 -0
- package/dist/honey-response.js.map +1 -0
- package/dist/i18n-slot.d.ts +8 -0
- package/dist/i18n-slot.d.ts.map +1 -1
- package/dist/i18n-slot.js +14 -0
- package/dist/i18n-slot.js.map +1 -0
- package/dist/i18n.d.ts +3 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +250 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +410 -149
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2324 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +3 -5
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +140 -0
- package/dist/init.js.map +1 -0
- package/dist/input.js +4 -0
- package/dist/input.js.map +1 -0
- package/dist/invalidate-check.d.ts +11 -0
- package/dist/invalidate-check.d.ts.map +1 -1
- package/dist/invalidate-check.js +163 -0
- package/dist/invalidate-check.js.map +1 -0
- package/dist/invoke-user.d.ts +35 -0
- package/dist/invoke-user.d.ts.map +1 -0
- package/dist/invoke-user.js +73 -0
- package/dist/invoke-user.js.map +1 -0
- package/dist/ip-restrict.d.ts +13 -1
- package/dist/ip-restrict.d.ts.map +1 -1
- package/dist/ip-restrict.js +60 -0
- package/dist/ip-restrict.js.map +1 -0
- package/dist/ip.d.ts +39 -0
- package/dist/ip.d.ts.map +1 -0
- package/dist/ip.js +209 -0
- package/dist/ip.js.map +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +113 -0
- package/dist/logger.js.map +1 -0
- package/dist/media-type.d.ts +30 -0
- package/dist/media-type.d.ts.map +1 -0
- package/dist/media-type.js +97 -0
- package/dist/media-type.js.map +1 -0
- package/dist/meta-spec-merge.d.ts +32 -0
- package/dist/meta-spec-merge.d.ts.map +1 -0
- package/dist/meta-spec-merge.js +68 -0
- package/dist/meta-spec-merge.js.map +1 -0
- package/dist/meta-spec.d.ts +16 -3
- package/dist/meta-spec.d.ts.map +1 -1
- package/dist/meta-spec.js +535 -0
- package/dist/meta-spec.js.map +1 -0
- package/dist/middleware.d.ts +23 -6
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +253 -0
- package/dist/middleware.js.map +1 -0
- package/dist/node-request.d.ts +57 -12
- package/dist/node-request.d.ts.map +1 -1
- package/dist/node-request.js +407 -0
- package/dist/node-request.js.map +1 -0
- package/dist/node.d.ts +20 -3
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +396 -0
- package/dist/node.js.map +1 -0
- package/dist/openapi/collect.d.ts +6 -3
- package/dist/openapi/collect.d.ts.map +1 -1
- package/dist/openapi/collect.js +30 -0
- package/dist/openapi/collect.js.map +1 -0
- package/dist/openapi/docs-page.d.ts +42 -0
- package/dist/openapi/docs-page.d.ts.map +1 -0
- package/dist/openapi/docs-page.js +80 -0
- package/dist/openapi/docs-page.js.map +1 -0
- package/dist/openapi/document.d.ts +4 -0
- package/dist/openapi/document.d.ts.map +1 -1
- package/dist/openapi/document.js +785 -0
- package/dist/openapi/document.js.map +1 -0
- package/dist/openapi/json-schema-slot.d.ts +7 -0
- package/dist/openapi/json-schema-slot.d.ts.map +1 -1
- package/dist/openapi/json-schema-slot.js +38 -0
- package/dist/openapi/json-schema-slot.js.map +1 -0
- package/dist/openapi/register.d.ts.map +1 -1
- package/dist/openapi/register.js +20 -0
- package/dist/openapi/register.js.map +1 -0
- package/dist/openapi/scalar.d.ts +3 -1
- package/dist/openapi/scalar.d.ts.map +1 -1
- package/dist/openapi/scalar.js +21 -0
- package/dist/openapi/scalar.js.map +1 -0
- package/dist/openapi/serve.d.ts +9 -0
- package/dist/openapi/serve.d.ts.map +1 -0
- package/dist/openapi/serve.js +36 -0
- package/dist/openapi/serve.js.map +1 -0
- package/dist/openapi/spec-factory.d.ts +30 -1
- package/dist/openapi/spec-factory.d.ts.map +1 -1
- package/dist/openapi/spec-factory.js +52 -0
- package/dist/openapi/spec-factory.js.map +1 -0
- package/dist/openapi/spec.d.ts +14 -5
- package/dist/openapi/spec.d.ts.map +1 -1
- package/dist/openapi/spec.js +50 -0
- package/dist/openapi/spec.js.map +1 -0
- package/dist/openapi/swagger.d.ts +3 -1
- package/dist/openapi/swagger.d.ts.map +1 -1
- package/dist/openapi/swagger.js +22 -0
- package/dist/openapi/swagger.js.map +1 -0
- package/dist/pattern.d.ts +71 -0
- package/dist/pattern.d.ts.map +1 -0
- package/dist/pattern.js +239 -0
- package/dist/pattern.js.map +1 -0
- package/dist/peer.d.ts +17 -0
- package/dist/peer.d.ts.map +1 -0
- package/dist/peer.js +50 -0
- package/dist/peer.js.map +1 -0
- package/dist/plugin.d.ts +67 -16
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +649 -0
- package/dist/plugin.js.map +1 -0
- package/dist/powered-by.d.ts.map +1 -1
- package/dist/powered-by.js +11 -0
- package/dist/powered-by.js.map +1 -0
- package/dist/pretty-json.d.ts.map +1 -1
- package/dist/pretty-json.js +47 -0
- package/dist/pretty-json.js.map +1 -0
- package/dist/producer-stream.d.ts +47 -0
- package/dist/producer-stream.d.ts.map +1 -0
- package/dist/producer-stream.js +267 -0
- package/dist/producer-stream.js.map +1 -0
- package/dist/proxy.d.ts +47 -8
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +274 -0
- package/dist/proxy.js.map +1 -0
- package/dist/python-type-emitter.d.ts +14 -1
- package/dist/python-type-emitter.d.ts.map +1 -1
- package/dist/python-type-emitter.js +157 -0
- package/dist/python-type-emitter.js.map +1 -0
- package/dist/realtime/bus.d.ts +17 -1
- package/dist/realtime/bus.d.ts.map +1 -1
- package/dist/realtime/bus.js +112 -0
- package/dist/realtime/bus.js.map +1 -0
- package/dist/realtime/register.d.ts +3 -0
- package/dist/realtime/register.d.ts.map +1 -0
- package/dist/realtime/register.js +10 -0
- package/dist/realtime/register.js.map +1 -0
- package/dist/realtime/route.d.ts +65 -26
- package/dist/realtime/route.d.ts.map +1 -1
- package/dist/realtime/route.js +72 -0
- package/dist/realtime/route.js.map +1 -0
- package/dist/realtime/server.d.ts +43 -0
- package/dist/realtime/server.d.ts.map +1 -0
- package/dist/realtime/server.js +255 -0
- package/dist/realtime/server.js.map +1 -0
- package/dist/request-id.d.ts +7 -0
- package/dist/request-id.d.ts.map +1 -1
- package/dist/request-id.js +16 -0
- package/dist/request-id.js.map +1 -0
- package/dist/request-limits.d.ts +3 -0
- package/dist/request-limits.d.ts.map +1 -0
- package/dist/request-limits.js +3 -0
- package/dist/request-limits.js.map +1 -0
- package/dist/request-path.d.ts +33 -0
- package/dist/request-path.d.ts.map +1 -0
- package/dist/request-path.js +178 -0
- package/dist/request-path.js.map +1 -0
- package/dist/request-to-curl.d.ts +7 -1
- package/dist/request-to-curl.d.ts.map +1 -1
- package/dist/request-to-curl.js +48 -0
- package/dist/request-to-curl.js.map +1 -0
- package/dist/response.d.ts +25 -2
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +313 -0
- package/dist/response.js.map +1 -0
- package/dist/rust-type-emitter.d.ts +14 -2
- package/dist/rust-type-emitter.d.ts.map +1 -1
- package/dist/rust-type-emitter.js +463 -0
- package/dist/rust-type-emitter.js.map +1 -0
- package/dist/secure-headers.d.ts.map +1 -1
- package/dist/secure-headers.js +53 -0
- package/dist/secure-headers.js.map +1 -0
- package/dist/serve-register.d.ts +4 -0
- package/dist/serve-register.d.ts.map +1 -1
- package/dist/serve-register.js +9 -0
- package/dist/serve-register.js.map +1 -0
- package/dist/serve-slot.d.ts +3 -0
- package/dist/serve-slot.d.ts.map +1 -1
- package/dist/serve-slot.js +52 -0
- package/dist/serve-slot.js.map +1 -0
- package/dist/serve.d.ts +18 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +157 -0
- package/dist/serve.js.map +1 -0
- package/dist/server-timing.d.ts.map +1 -1
- package/dist/server-timing.js +51 -0
- package/dist/server-timing.js.map +1 -0
- package/dist/static.d.ts +9 -0
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +102 -0
- package/dist/static.js.map +1 -0
- package/dist/telemetry/otel.d.ts +2 -0
- package/dist/telemetry/otel.d.ts.map +1 -1
- package/dist/telemetry/otel.js +135 -0
- package/dist/telemetry/otel.js.map +1 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +124 -0
- package/dist/testing.js.map +1 -0
- package/dist/timeout.d.ts +6 -0
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +36 -0
- package/dist/timeout.js.map +1 -0
- package/dist/tree.d.ts +133 -36
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +437 -0
- package/dist/tree.js.map +1 -0
- package/dist/trust.d.ts +21 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +100 -0
- package/dist/trust.js.map +1 -0
- package/dist/ts-type-emitter.d.ts.map +1 -1
- package/dist/ts-type-emitter.js +150 -0
- package/dist/ts-type-emitter.js.map +1 -0
- package/dist/type-emitter.d.ts +16 -2
- package/dist/type-emitter.d.ts.map +1 -1
- package/dist/type-emitter.js +729 -0
- package/dist/type-emitter.js.map +1 -0
- package/dist/type-extractor.d.ts +8 -4
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +776 -0
- package/dist/type-extractor.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +96 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +14 -2
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +335 -0
- package/dist/validation.js.map +1 -0
- package/dist/with-headers.d.ts +15 -0
- package/dist/with-headers.d.ts.map +1 -0
- package/dist/with-headers.js +45 -0
- package/dist/with-headers.js.map +1 -0
- package/dist/ws/bun.d.ts +11 -5
- package/dist/ws/bun.d.ts.map +1 -1
- package/dist/ws/bun.js +82 -0
- package/dist/ws/bun.js.map +1 -0
- package/dist/ws/cloudflare.d.ts +25 -3
- package/dist/ws/cloudflare.d.ts.map +1 -1
- package/dist/ws/cloudflare.js +117 -0
- package/dist/ws/cloudflare.js.map +1 -0
- package/dist/ws/deno.d.ts +4 -4
- package/dist/ws/deno.d.ts.map +1 -1
- package/dist/ws/deno.js +133 -0
- package/dist/ws/deno.js.map +1 -0
- package/dist/ws/node.d.ts +20 -3
- package/dist/ws/node.d.ts.map +1 -1
- package/dist/ws/node.js +173 -0
- package/dist/ws/node.js.map +1 -0
- package/dist/ws/session.d.ts +11 -0
- package/dist/ws/session.d.ts.map +1 -0
- package/dist/ws/session.js +60 -0
- package/dist/ws/session.js.map +1 -0
- package/dist/ws/shared.d.ts +65 -0
- package/dist/ws/shared.d.ts.map +1 -0
- package/dist/ws/shared.js +103 -0
- package/dist/ws/shared.js.map +1 -0
- package/dist/ws-origin.d.ts +20 -0
- package/dist/ws-origin.d.ts.map +1 -0
- package/dist/ws-origin.js +41 -0
- package/dist/ws-origin.js.map +1 -0
- package/dist/yaml.d.ts +15 -1
- package/dist/yaml.d.ts.map +1 -1
- package/dist/yaml.js +101 -0
- package/dist/yaml.js.map +1 -0
- package/docs/invalidate-spec.md +13 -1
- package/docs/meta-spec.md +36 -12
- package/docs/packaging.md +43 -24
- package/docs/regression-matrix/README.md +32 -0
- package/docs/regression-matrix/review.md +44 -0
- package/docs/regression-matrix/ws1-3.md +97 -0
- package/docs/regression-matrix/ws4-8.md +107 -0
- package/docs/regression-matrix/ws9-12.md +266 -0
- package/docs/sdk.md +102 -52
- package/package.json +183 -52
- package/src/accepts.ts +91 -33
- package/src/body-kind.ts +67 -0
- package/src/body-limit.ts +102 -87
- package/src/build/index.ts +104 -54
- package/src/cli-args.ts +123 -0
- package/src/cli-go/auth.go +12 -1
- package/src/cli-go/config.go +29 -2
- package/src/cli-go/errors.go +28 -10
- package/src/cli-go/multipart.go +51 -27
- package/src/cli-go/output.go +58 -4
- package/src/cli-go/root.go +1 -1
- package/src/cli-go/version.go +2 -2
- package/src/cli.ts +154 -148
- package/src/client/error.ts +56 -1
- package/src/client/http.ts +479 -256
- package/src/client/index.ts +55 -17
- package/src/client/path.ts +169 -0
- package/src/client/redirect-policy.ts +19 -0
- package/src/client/sdk.ts +180 -101
- package/src/client/sse.ts +92 -66
- package/src/client/ws.ts +67 -31
- package/src/client-go/errors.go +41 -12
- package/src/client-go/export.go +44 -0
- package/src/client-go/invalidation.go +52 -54
- package/src/client-go/nullable.go +62 -0
- package/src/client-go/realtime.go +147 -110
- package/src/client-go/runtime.go +646 -88
- package/src/client-go/sse.go +104 -94
- package/src/client-go/transport.go +67 -27
- package/src/client-go/ws.go +49 -25
- package/src/client-info.ts +79 -0
- package/src/client-mcp/server.ts +133 -3
- package/src/client-python/_invalidation.py +207 -291
- package/src/client-python/_realtime.py +63 -40
- package/src/client-python/_runtime.py +501 -119
- package/src/client-python/_sse.py +130 -74
- package/src/client-python/_transport.py +37 -9
- package/src/client-python/_ws.py +50 -22
- package/src/client-rust/errors.rs +21 -1
- package/src/client-rust/invalidation.rs +202 -231
- package/src/client-rust/invalidation_sync.rs +16 -229
- package/src/client-rust/nullable.rs +17 -0
- package/src/client-rust/realtime.rs +217 -217
- package/src/client-rust/runtime.rs +602 -177
- package/src/client-rust/runtime_sync.rs +169 -145
- package/src/client-rust/sse.rs +142 -121
- package/src/client-rust/ws.rs +61 -37
- package/src/codegen-go-cli.ts +607 -728
- package/src/codegen-go.ts +710 -770
- package/src/codegen-ir.ts +381 -75
- package/src/codegen-lang.ts +517 -0
- package/src/codegen-load.ts +150 -0
- package/src/codegen-mcp.ts +207 -108
- package/src/codegen-python.ts +684 -816
- package/src/codegen-route-tree-intern.ts +51 -25
- package/src/codegen-rust.ts +946 -1240
- package/src/codegen-sanitize.ts +0 -4
- package/src/codegen-schema-naming.ts +19 -6
- package/src/codegen-sdk-model.ts +228 -0
- package/src/codegen.ts +1488 -1140
- package/src/context.ts +130 -11
- package/src/cookie-sign.ts +85 -16
- package/src/cookie.ts +66 -11
- package/src/cors.ts +55 -54
- package/src/crypto.ts +27 -5
- package/src/csrf.ts +57 -25
- package/src/curl-logger.ts +155 -114
- package/src/dict.ts +12 -0
- package/src/error.ts +4 -2
- package/src/etag.ts +58 -34
- package/src/feature-detect.ts +147 -0
- package/src/feature-slots.ts +70 -0
- package/src/fetch-request.ts +13 -0
- package/src/gen-process.ts +42 -0
- package/src/gen-write.ts +127 -0
- package/src/glob.ts +109 -0
- package/src/go-type-emitter.ts +406 -155
- package/src/honey-response.ts +36 -9
- package/src/i18n-slot.ts +10 -0
- package/src/i18n.ts +98 -23
- package/src/index.ts +1893 -1761
- package/src/init.ts +40 -28
- package/src/invalidate-check.ts +42 -4
- package/src/invoke-user.ts +95 -0
- package/src/ip-restrict.ts +47 -161
- package/src/ip.ts +194 -0
- package/src/logger.ts +65 -36
- package/src/media-type.ts +104 -0
- package/src/meta-spec-merge.ts +80 -0
- package/src/meta-spec.ts +0 -0
- package/src/middleware.ts +112 -12
- package/src/node-request.ts +242 -52
- package/src/node.ts +318 -75
- package/src/openapi/collect.ts +19 -44
- package/src/openapi/docs-page.ts +89 -0
- package/src/openapi/document.ts +503 -224
- package/src/openapi/json-schema-slot.ts +40 -0
- package/src/openapi/register.ts +7 -2
- package/src/openapi/scalar.ts +11 -10
- package/src/openapi/serve.ts +39 -0
- package/src/openapi/spec-factory.ts +50 -1
- package/src/openapi/spec.ts +53 -30
- package/src/openapi/swagger.ts +12 -11
- package/src/pattern.ts +245 -0
- package/src/peer.ts +53 -0
- package/src/plugin.ts +270 -185
- package/src/powered-by.ts +2 -2
- package/src/pretty-json.ts +25 -9
- package/src/producer-stream.ts +296 -0
- package/src/proxy.ts +271 -55
- package/src/python-type-emitter.ts +68 -56
- package/src/realtime/bus.ts +56 -9
- package/src/realtime/register.ts +13 -0
- package/src/realtime/route.ts +129 -80
- package/src/realtime/server.ts +298 -0
- package/src/request-id.ts +13 -3
- package/src/request-limits.ts +2 -0
- package/src/request-path.ts +165 -0
- package/src/request-to-curl.ts +28 -7
- package/src/response.ts +143 -103
- package/src/rust-type-emitter.ts +265 -170
- package/src/secure-headers.ts +8 -4
- package/src/serve-register.ts +4 -0
- package/src/serve-slot.ts +38 -0
- package/src/serve.ts +113 -25
- package/src/server-timing.ts +11 -7
- package/src/static.ts +85 -32
- package/src/telemetry/otel.ts +29 -7
- package/src/testing.ts +76 -14
- package/src/timeout.ts +15 -7
- package/src/tree.ts +405 -333
- package/src/trust.ts +117 -0
- package/src/ts-type-emitter.ts +18 -12
- package/src/type-emitter.ts +227 -89
- package/src/type-extractor.ts +562 -424
- package/src/types.ts +8 -0
- package/src/validation.ts +221 -96
- package/src/with-headers.ts +45 -0
- package/src/ws/bun.ts +53 -15
- package/src/ws/cloudflare.ts +81 -19
- package/src/ws/deno.ts +124 -36
- package/src/ws/node.ts +173 -79
- package/src/ws/session.ts +73 -0
- package/src/ws/shared.ts +140 -0
- package/src/ws-origin.ts +53 -0
- package/src/yaml.ts +45 -22
- package/dist/client-mcp/server.d.ts +0 -10
- package/dist/client-mcp/server.d.ts.map +0 -1
- package/dist/client-mcp/types.d.ts +0 -16
- package/dist/client-mcp/types.d.ts.map +0 -1
- package/dist/cloudflare/realtime.d.ts +0 -30
- package/dist/cloudflare/realtime.d.ts.map +0 -1
- package/dist/realtime/buffer.d.ts +0 -18
- package/dist/realtime/buffer.d.ts.map +0 -1
- package/dist/realtime/longpoll.d.ts +0 -23
- package/dist/realtime/longpoll.d.ts.map +0 -1
- package/dist/realtime/protocol.d.ts +0 -50
- package/dist/realtime/protocol.d.ts.map +0 -1
- package/dist/realtime/runtime.d.ts +0 -74
- package/dist/realtime/runtime.d.ts.map +0 -1
- package/src/cloudflare/realtime.ts +0 -234
- package/src/realtime/buffer.ts +0 -101
- package/src/realtime/longpoll.ts +0 -148
- package/src/realtime/protocol.ts +0 -128
- package/src/realtime/runtime.ts +0 -427
package/src/codegen.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import { InternPool } from "./codegen-route-tree-intern.ts"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
deriveErrorEnvelopeName,
|
|
4
|
+
deriveSchemaName,
|
|
5
|
+
isErrorEnvelope,
|
|
6
|
+
sanitizeComponentName,
|
|
7
|
+
shortHash,
|
|
8
|
+
} from "./codegen-schema-naming.ts"
|
|
3
9
|
import type { SchemaNameContext } from "./codegen-schema-naming.ts"
|
|
4
10
|
import { effectJsonSchemaFn, loadEffectJsonSchema, loadToJSONSchema, toJSONSchemaFn } from "./codegen-loaders.ts"
|
|
5
11
|
import { sanitizeZodJsonSchema } from "./codegen-sanitize.ts"
|
|
6
|
-
import {
|
|
7
|
-
|
|
12
|
+
import {
|
|
13
|
+
bodiesOf,
|
|
14
|
+
irErrorEnvelope,
|
|
15
|
+
irResolver,
|
|
16
|
+
methodsOf,
|
|
17
|
+
namespacesOf,
|
|
18
|
+
openApiPathParams,
|
|
19
|
+
schemaToIR,
|
|
20
|
+
toIR,
|
|
21
|
+
} from "./codegen-ir.ts"
|
|
22
|
+
import type { IR, IRBody, IRNamespace, IROperation, IRSchema } from "./codegen-ir.ts"
|
|
23
|
+
import { isJsonMedia, isTextMedia, mediaEssence } from "./codegen-sdk-model.ts"
|
|
8
24
|
import type { HoneyError } from "./error.ts"
|
|
9
25
|
import { ERROR_META } from "./errors.ts"
|
|
10
26
|
import type { ErrorMetaEntry } from "./errors.ts"
|
|
11
27
|
import type { InvalidateCheckConfig } from "./invalidate-check.ts"
|
|
28
|
+
import { publishableMetaKeys } from "./meta-spec.ts"
|
|
29
|
+
import { metaSpecOf } from "./meta-spec-merge.ts"
|
|
12
30
|
import type { Honey } from "./index.ts"
|
|
13
31
|
import {
|
|
14
32
|
generateOpenApiFromTree,
|
|
@@ -17,12 +35,16 @@ import {
|
|
|
17
35
|
type OpenApiSpec,
|
|
18
36
|
} from "./openapi/document.ts"
|
|
19
37
|
import { getJsonSchemaConverter, setJsonSchemaConverter } from "./openapi/json-schema-slot.ts"
|
|
20
|
-
import
|
|
38
|
+
import { collectRoutes, extractParams } from "./openapi/collect.ts"
|
|
39
|
+
import { parsePattern, patternParams } from "./pattern.ts"
|
|
40
|
+
import type { RouteHandler, RouteTree } from "./tree.ts"
|
|
41
|
+
import { forEachLeaf, ROUTE_TREE_VERSION } from "./tree.ts"
|
|
21
42
|
import { irToTs } from "./ts-type-emitter.ts"
|
|
22
|
-
import { createTypeEmitState, emitSchemaType } from "./type-emitter.ts"
|
|
43
|
+
import { createTypeEmitState, emitSchemaType, quoteKey } from "./type-emitter.ts"
|
|
23
44
|
import type { TypeEmitState } from "./type-emitter.ts"
|
|
24
|
-
import type { InputSchemaEntry, InputSchemasDef, OutputSchemaDef, StandardSchemaLike } from "./types.ts"
|
|
45
|
+
import type { InputSchemaEntry, InputSchemasDef, MetaSpecConfig, OutputSchemaDef, StandardSchemaLike } from "./types.ts"
|
|
25
46
|
import { EMPTY_OBJ, statusKeyToCode } from "./types.ts"
|
|
47
|
+
import { CROSS_ORIGIN_SAFE_HEADERS } from "./client/redirect-policy.ts"
|
|
26
48
|
|
|
27
49
|
export { prepareCodegen } from "./codegen-loaders.ts"
|
|
28
50
|
export type { InvalidateCheckConfig, InvalidateCheckLevel } from "./invalidate-check.ts"
|
|
@@ -57,21 +79,80 @@ const ERROR_EXPORT_NAMES: ReadonlyArray<string> = [
|
|
|
57
79
|
|
|
58
80
|
type ValibotSchema = Record<string, unknown> & { type: string }
|
|
59
81
|
|
|
60
|
-
|
|
82
|
+
/** Wrappers that make an object entry optional. */
|
|
83
|
+
const VALIBOT_OPTIONAL = new Set(["exact_optional", "nullish", "optional", "undefinedable"])
|
|
84
|
+
|
|
85
|
+
/** Pipe actions with a JSON Schema equivalent. Anything else constrains nothing in the document. */
|
|
86
|
+
function applyValibotPipe(schema: ValibotSchema, json: Record<string, unknown>): Record<string, unknown> {
|
|
87
|
+
const pipe = schema.pipe as Array<Record<string, unknown>> | undefined
|
|
88
|
+
if (!Array.isArray(pipe)) return json
|
|
89
|
+
const formats: Record<string, string> = {
|
|
90
|
+
email: "email",
|
|
91
|
+
ipv4: "ipv4",
|
|
92
|
+
ipv6: "ipv6",
|
|
93
|
+
iso_date: "date",
|
|
94
|
+
iso_date_time: "date-time",
|
|
95
|
+
iso_timestamp: "date-time",
|
|
96
|
+
url: "uri",
|
|
97
|
+
uuid: "uuid",
|
|
98
|
+
}
|
|
99
|
+
for (const action of pipe.slice(1)) {
|
|
100
|
+
const type = action.type as string
|
|
101
|
+
const req = action.requirement
|
|
102
|
+
if (type in formats) json.format = formats[type]
|
|
103
|
+
else if (type === "integer") json.type = "integer"
|
|
104
|
+
else if (type === "regex" && req instanceof RegExp) json.pattern = req.source
|
|
105
|
+
else if (type === "min_length" && typeof req === "number")
|
|
106
|
+
json[json.type === "array" ? "minItems" : "minLength"] = req
|
|
107
|
+
else if (type === "max_length" && typeof req === "number")
|
|
108
|
+
json[json.type === "array" ? "maxItems" : "maxLength"] = req
|
|
109
|
+
else if (type === "length" && typeof req === "number") {
|
|
110
|
+
json[json.type === "array" ? "minItems" : "minLength"] = req
|
|
111
|
+
json[json.type === "array" ? "maxItems" : "maxLength"] = req
|
|
112
|
+
} else if (type === "min_value" && typeof req === "number") json.minimum = req
|
|
113
|
+
else if (type === "max_value" && typeof req === "number") json.maximum = req
|
|
114
|
+
else if (type === "description" && typeof action.description === "string") json.description = action.description
|
|
115
|
+
}
|
|
116
|
+
return json
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function valibotObject(s: ValibotSchema, depth: number): Record<string, unknown> {
|
|
120
|
+
const entries = s.entries as Record<string, ValibotSchema>
|
|
121
|
+
if (Object.keys(entries).length === 0 && s.type === "object") return { type: "object" }
|
|
122
|
+
const properties: Record<string, unknown> = {}
|
|
123
|
+
const required: string[] = []
|
|
124
|
+
for (const k of Object.keys(entries)) {
|
|
125
|
+
const entry = entries[k]
|
|
126
|
+
properties[k] = valibotToJsonSchema(entry, depth + 1)
|
|
127
|
+
if (!VALIBOT_OPTIONAL.has(entry.type)) required.push(k)
|
|
128
|
+
}
|
|
129
|
+
const result: Record<string, unknown> = { properties, type: "object" }
|
|
130
|
+
if (required.length > 0) result.required = required
|
|
131
|
+
if (s.type === "strict_object") result.additionalProperties = false
|
|
132
|
+
else if (s.type === "object_with_rest") result.additionalProperties = valibotToJsonSchema(s.rest, depth + 1)
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function valibotToJsonSchema(schema: unknown, depth = 0): unknown {
|
|
137
|
+
/* a lazy schema can recurse without end; past this depth the node is unconstrained */
|
|
138
|
+
if (depth > 32) return {}
|
|
61
139
|
const s = schema as ValibotSchema
|
|
62
|
-
const
|
|
140
|
+
const walk = (child: unknown): unknown => valibotToJsonSchema(child, depth + 1)
|
|
63
141
|
|
|
64
|
-
switch (
|
|
142
|
+
switch (s.type) {
|
|
65
143
|
case "string":
|
|
66
|
-
return { type: "string" }
|
|
144
|
+
return applyValibotPipe(s, { type: "string" })
|
|
67
145
|
case "number":
|
|
68
|
-
return { type: "number" }
|
|
146
|
+
return applyValibotPipe(s, { type: "number" })
|
|
69
147
|
case "boolean":
|
|
70
148
|
return { type: "boolean" }
|
|
71
149
|
case "bigint":
|
|
72
|
-
return { type: "integer" }
|
|
150
|
+
return { format: "int64", type: "integer" }
|
|
73
151
|
case "date":
|
|
74
152
|
return { format: "date-time", type: "string" }
|
|
153
|
+
case "file":
|
|
154
|
+
case "blob":
|
|
155
|
+
return { contentEncoding: "binary", format: "binary", type: "string" }
|
|
75
156
|
case "undefined":
|
|
76
157
|
case "void":
|
|
77
158
|
return {}
|
|
@@ -80,69 +161,62 @@ function valibotToJsonSchema(schema: unknown): unknown {
|
|
|
80
161
|
case "any":
|
|
81
162
|
case "unknown":
|
|
82
163
|
return {}
|
|
83
|
-
case "literal":
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
case "
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (keys.length === 0) return { type: "object" }
|
|
91
|
-
const properties: Record<string, unknown> = {}
|
|
92
|
-
const required: string[] = []
|
|
93
|
-
for (const k of keys) {
|
|
94
|
-
const entry = entries[k] as ValibotSchema
|
|
95
|
-
if (entry.type === "optional") {
|
|
96
|
-
properties[k] = valibotToJsonSchema(entry.wrapped as unknown)
|
|
97
|
-
} else {
|
|
98
|
-
properties[k] = valibotToJsonSchema(entry)
|
|
99
|
-
required.push(k)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
const result: Record<string, unknown> = { properties, type: "object" }
|
|
103
|
-
if (required.length > 0) result.required = required
|
|
104
|
-
return result
|
|
105
|
-
}
|
|
164
|
+
case "literal":
|
|
165
|
+
return { const: s.literal }
|
|
166
|
+
case "object":
|
|
167
|
+
case "strict_object":
|
|
168
|
+
case "loose_object":
|
|
169
|
+
case "object_with_rest":
|
|
170
|
+
return valibotObject(s, depth)
|
|
106
171
|
case "array":
|
|
107
|
-
return { items:
|
|
172
|
+
return applyValibotPipe(s, { items: walk(s.item), type: "array" })
|
|
108
173
|
case "optional":
|
|
109
|
-
|
|
110
|
-
case "
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
return
|
|
174
|
+
case "exact_optional":
|
|
175
|
+
case "undefinedable":
|
|
176
|
+
case "non_nullable":
|
|
177
|
+
case "non_nullish":
|
|
178
|
+
case "non_optional": {
|
|
179
|
+
const inner = walk(s.wrapped) as Record<string, unknown>
|
|
180
|
+
const fallback = s.default
|
|
181
|
+
return fallback === undefined || typeof fallback === "function" ? inner : { ...inner, default: fallback }
|
|
117
182
|
}
|
|
183
|
+
case "nullable":
|
|
184
|
+
case "nullish":
|
|
185
|
+
return { anyOf: [walk(s.wrapped), { type: "null" }] }
|
|
118
186
|
case "union":
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
187
|
+
case "variant":
|
|
188
|
+
return { anyOf: (s.options as unknown[]).map(walk) }
|
|
122
189
|
case "intersect":
|
|
123
|
-
return {
|
|
124
|
-
allOf: (s.options as unknown[]).map((o) => valibotToJsonSchema(o)),
|
|
125
|
-
}
|
|
190
|
+
return { allOf: (s.options as unknown[]).map(walk) }
|
|
126
191
|
case "picklist":
|
|
127
192
|
return { enum: s.options }
|
|
128
193
|
case "enum": {
|
|
129
|
-
const enumObj = s.enum as Record<string, string>
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
case "record": {
|
|
194
|
+
const enumObj = s.enum as Record<string, string | number>
|
|
195
|
+
/* a TS numeric enum carries reverse mappings — keep the values */
|
|
133
196
|
return {
|
|
134
|
-
|
|
135
|
-
|
|
197
|
+
enum: Object.keys(enumObj)
|
|
198
|
+
.filter((k) => typeof enumObj[enumObj[k] as string] !== "number")
|
|
199
|
+
.map((k) => enumObj[k]),
|
|
136
200
|
}
|
|
137
201
|
}
|
|
138
|
-
case "
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
202
|
+
case "record":
|
|
203
|
+
return { additionalProperties: walk(s.value), type: "object" }
|
|
204
|
+
case "tuple":
|
|
205
|
+
case "strict_tuple":
|
|
206
|
+
case "loose_tuple":
|
|
207
|
+
case "tuple_with_rest": {
|
|
208
|
+
const items = (s.items as unknown[]).map(walk)
|
|
209
|
+
const result: Record<string, unknown> = { minItems: items.length, prefixItems: items, type: "array" }
|
|
210
|
+
if (s.type === "tuple_with_rest") result.items = walk(s.rest)
|
|
211
|
+
else if (s.type !== "loose_tuple") {
|
|
212
|
+
result.items = false
|
|
213
|
+
result.maxItems = items.length
|
|
145
214
|
}
|
|
215
|
+
return result
|
|
216
|
+
}
|
|
217
|
+
case "lazy": {
|
|
218
|
+
const getter = s.getter as ((input: unknown) => unknown) | undefined
|
|
219
|
+
return typeof getter === "function" ? walk(getter(undefined)) : {}
|
|
146
220
|
}
|
|
147
221
|
default:
|
|
148
222
|
return {}
|
|
@@ -222,9 +296,10 @@ function yupDescBase(desc: YupJsonDesc): unknown {
|
|
|
222
296
|
if (!Array.isArray(desc.innerType)) return { type: "array" }
|
|
223
297
|
const items = desc.innerType.map((i) => yupDescToJsonSchema(i))
|
|
224
298
|
return {
|
|
225
|
-
items,
|
|
299
|
+
items: false,
|
|
226
300
|
maxItems: items.length,
|
|
227
301
|
minItems: items.length,
|
|
302
|
+
prefixItems: items,
|
|
228
303
|
type: "array",
|
|
229
304
|
}
|
|
230
305
|
}
|
|
@@ -272,16 +347,6 @@ type RouteManifest = {
|
|
|
272
347
|
routes: RouteManifestEntry[]
|
|
273
348
|
}
|
|
274
349
|
|
|
275
|
-
function extractParams(path: string): string[] {
|
|
276
|
-
const params: string[] = []
|
|
277
|
-
for (const seg of path.split("/")) {
|
|
278
|
-
if (seg.startsWith(":")) {
|
|
279
|
-
params.push(seg.endsWith("?") ? seg.slice(1, -1) : seg.slice(1))
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return params
|
|
283
|
-
}
|
|
284
|
-
|
|
285
350
|
type CollectedRoute = {
|
|
286
351
|
handler: RouteHandler
|
|
287
352
|
method: string
|
|
@@ -292,30 +357,6 @@ function isMetaInternal(handler: RouteHandler): boolean {
|
|
|
292
357
|
return (handler.mt as { internal?: boolean } | null)?.internal === true
|
|
293
358
|
}
|
|
294
359
|
|
|
295
|
-
function walkTree(node: TreeNode, currentPath: string, routes: CollectedRoute[], includeSkipped?: boolean): void {
|
|
296
|
-
if (node.m !== null) {
|
|
297
|
-
for (const [method, handler] of Object.entries(node.m)) {
|
|
298
|
-
if (handler._skip && !includeSkipped) continue
|
|
299
|
-
routes.push({ handler, method, path: currentPath || "/" })
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
for (const [seg, child] of Object.entries(node.s)) {
|
|
304
|
-
walkTree(child, `${currentPath}/${seg}`, routes, includeSkipped)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (node.d !== null) {
|
|
308
|
-
walkTree(node.d.c, `${currentPath}/:${node.d.n}`, routes, includeSkipped)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (node.w !== null) {
|
|
312
|
-
for (const [method, handler] of Object.entries(node.w.m)) {
|
|
313
|
-
if (handler._skip && !includeSkipped) continue
|
|
314
|
-
routes.push({ handler, method, path: `${currentPath}/*${node.w.n}` })
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
360
|
type ErrorInfo = { errorKey: string; status: number; statusKey: string }
|
|
320
361
|
|
|
321
362
|
/* Longest suffix first so `*_not_found` wins over `*_found` (302). */
|
|
@@ -384,10 +425,18 @@ function introspectSchema(schema: StandardSchemaLike): unknown {
|
|
|
384
425
|
|
|
385
426
|
export { normalizeSecurity } from "./meta-spec.ts"
|
|
386
427
|
|
|
428
|
+
/** Locale-independent string order (UTF-16 code units), so output is the same on every machine. */
|
|
429
|
+
export function compareCodeUnits(a: string, b: string): number {
|
|
430
|
+
if (a === b) return 0
|
|
431
|
+
return a < b ? -1 : 1
|
|
432
|
+
}
|
|
433
|
+
|
|
387
434
|
export function canonicalizeSchema(schema: Record<string, unknown>): string {
|
|
388
435
|
return JSON.stringify(schema, (_, value) => {
|
|
389
436
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
390
|
-
return Object.fromEntries(
|
|
437
|
+
return Object.fromEntries(
|
|
438
|
+
Object.entries(value as Record<string, unknown>).sort(([a], [b]) => compareCodeUnits(a, b)),
|
|
439
|
+
)
|
|
391
440
|
}
|
|
392
441
|
return value
|
|
393
442
|
})
|
|
@@ -395,6 +444,7 @@ export function canonicalizeSchema(schema: Record<string, unknown>): string {
|
|
|
395
444
|
|
|
396
445
|
type SchemaSlot = {
|
|
397
446
|
canonical: string
|
|
447
|
+
contentType: string
|
|
398
448
|
context: SchemaNameContext
|
|
399
449
|
/* path key from the spec — used for context-based ref rewriting */
|
|
400
450
|
pathKey: string
|
|
@@ -408,6 +458,16 @@ function isObjectSchema(schema: unknown): schema is Record<string, unknown> {
|
|
|
408
458
|
return s.type === "object" || (typeof s.properties === "object" && s.properties !== null)
|
|
409
459
|
}
|
|
410
460
|
|
|
461
|
+
/**
|
|
462
|
+
* A response key as a number: `"200"` → 200, `"default"` → 0, `"4XX"` → 400. Never `NaN`, which
|
|
463
|
+
* would name a schema `…ResponseNaN` and make every `default` response collide.
|
|
464
|
+
*/
|
|
465
|
+
function statusNumber(key: string): number {
|
|
466
|
+
if (/^[1-5][0-9][0-9]$/.test(key)) return Number(key)
|
|
467
|
+
if (/^[1-5]XX$/i.test(key)) return Number(key[0]) * 100
|
|
468
|
+
return 0
|
|
469
|
+
}
|
|
470
|
+
|
|
411
471
|
function collectSchemaSlots(paths: Record<string, Record<string, Record<string, unknown>>>): SchemaSlot[] {
|
|
412
472
|
const slots: SchemaSlot[] = []
|
|
413
473
|
|
|
@@ -420,11 +480,11 @@ function collectSchemaSlots(paths: Record<string, Record<string, Record<string,
|
|
|
420
480
|
if (requestBody) {
|
|
421
481
|
const content = requestBody.content as Record<string, Record<string, unknown>> | undefined
|
|
422
482
|
if (content) {
|
|
423
|
-
for (const mediaType of Object.
|
|
483
|
+
for (const [contentType, mediaType] of Object.entries(content)) {
|
|
424
484
|
const schema = mediaType.schema as Record<string, unknown> | undefined
|
|
425
485
|
if (schema && !schema.$ref) {
|
|
426
486
|
const context: SchemaNameContext = { method: m, path: pathKey, role: "request" }
|
|
427
|
-
slots.push({ canonical: canonicalizeSchema(schema), context, pathKey, schema })
|
|
487
|
+
slots.push({ canonical: canonicalizeSchema(schema), contentType, context, pathKey, schema })
|
|
428
488
|
}
|
|
429
489
|
}
|
|
430
490
|
}
|
|
@@ -437,17 +497,17 @@ function collectSchemaSlots(paths: Record<string, Record<string, Record<string,
|
|
|
437
497
|
| Record<string, Record<string, unknown>>
|
|
438
498
|
| undefined
|
|
439
499
|
if (content) {
|
|
440
|
-
for (const mediaType of Object.
|
|
500
|
+
for (const [contentType, mediaType] of Object.entries(content)) {
|
|
441
501
|
const schema = mediaType.schema as Record<string, unknown> | undefined
|
|
442
502
|
if (schema && !schema.$ref) {
|
|
443
|
-
const statusNum =
|
|
503
|
+
const statusNum = statusNumber(status)
|
|
444
504
|
const context: SchemaNameContext = {
|
|
445
505
|
method: m,
|
|
446
506
|
path: pathKey,
|
|
447
507
|
role: "response",
|
|
448
508
|
status: statusNum,
|
|
449
509
|
}
|
|
450
|
-
slots.push({ canonical: canonicalizeSchema(schema), context, pathKey, schema })
|
|
510
|
+
slots.push({ canonical: canonicalizeSchema(schema), contentType, context, pathKey, schema })
|
|
451
511
|
}
|
|
452
512
|
}
|
|
453
513
|
}
|
|
@@ -473,6 +533,20 @@ type HoistedResult = {
|
|
|
473
533
|
schemas: Record<string, Record<string, unknown>>
|
|
474
534
|
}
|
|
475
535
|
|
|
536
|
+
/**
|
|
537
|
+
* A component name nobody holds yet. A clash gets the content hash, and a clash on the
|
|
538
|
+
* hashed name (24 bits collide) a counter — a name is never handed out twice.
|
|
539
|
+
*/
|
|
540
|
+
function uniqueName(base: string, canonical: string, taken: (name: string) => boolean): string {
|
|
541
|
+
if (!taken(base)) return base
|
|
542
|
+
const hashed = `${base}_${shortHash(canonical)}`
|
|
543
|
+
if (!taken(hashed)) return hashed
|
|
544
|
+
for (let n = 2; ; n++) {
|
|
545
|
+
const candidate = `${hashed}_${n}`
|
|
546
|
+
if (!taken(candidate)) return candidate
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
476
550
|
function walkNestedObjects(
|
|
477
551
|
schema: Record<string, unknown>,
|
|
478
552
|
slotName: string,
|
|
@@ -482,7 +556,8 @@ function walkNestedObjects(
|
|
|
482
556
|
): void {
|
|
483
557
|
const props = schema.properties as Record<string, unknown> | undefined
|
|
484
558
|
if (!props) return
|
|
485
|
-
for (const
|
|
559
|
+
for (const key of Object.keys(props)) {
|
|
560
|
+
const val = props[key]
|
|
486
561
|
if (!isObjectSchema(val)) continue
|
|
487
562
|
const child = val as Record<string, unknown>
|
|
488
563
|
const childPath = [...fieldPath, key]
|
|
@@ -505,7 +580,7 @@ function walkNestedObjects(
|
|
|
505
580
|
function collectHoistedSchemas(
|
|
506
581
|
slots: SchemaSlot[],
|
|
507
582
|
slotNames: Map<SchemaSlot, string>,
|
|
508
|
-
|
|
583
|
+
taken: (name: string) => boolean,
|
|
509
584
|
): HoistedResult {
|
|
510
585
|
const nestedMap = new Map<string, NestedEntry>()
|
|
511
586
|
for (const slot of slots) {
|
|
@@ -529,19 +604,25 @@ function collectHoistedSchemas(
|
|
|
529
604
|
const owner = entry.samples.slice().sort((a, b) => {
|
|
530
605
|
const na = a.slotName + a.fieldPath.join("")
|
|
531
606
|
const nb = b.slotName + b.fieldPath.join("")
|
|
532
|
-
return na
|
|
607
|
+
return compareCodeUnits(na, nb)
|
|
533
608
|
})[0]
|
|
534
609
|
|
|
535
|
-
const fieldPart = owner.fieldPath
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
610
|
+
const fieldPart = owner.fieldPath
|
|
611
|
+
.map((f) => sanitizeComponentName(f))
|
|
612
|
+
.map((f) => f.charAt(0).toUpperCase() + f.slice(1))
|
|
613
|
+
.join("")
|
|
614
|
+
const name = uniqueName(
|
|
615
|
+
sanitizeComponentName(`${owner.slotName}${fieldPart}`),
|
|
616
|
+
canonical,
|
|
617
|
+
(n) => taken(n) || Object.hasOwn(schemas, n),
|
|
618
|
+
)
|
|
540
619
|
schemas[name] = entry.schema
|
|
541
620
|
canonicalToName.set(canonical, name)
|
|
542
|
-
reservedNames.add(name)
|
|
543
621
|
}
|
|
544
622
|
|
|
623
|
+
/* nested hoists inside a hoisted schema point at their component too */
|
|
624
|
+
for (const name of Object.keys(schemas)) schemas[name] = rewriteNestedRefs(schemas[name], canonicalToName)
|
|
625
|
+
|
|
545
626
|
return { canonicalToName, schemas }
|
|
546
627
|
}
|
|
547
628
|
|
|
@@ -549,7 +630,8 @@ function collectHoistedSchemas(
|
|
|
549
630
|
* Builds a new paths object by shallow-copying only the containers that need schema
|
|
550
631
|
* replacement. Avoids structuredClone (which fails on arktype schemas containing
|
|
551
632
|
* native functions). Original schema objects are dropped; only $ref objects are new.
|
|
552
|
-
* Also rewrites nested inline object properties that were hoisted (canonicalToName)
|
|
633
|
+
* Also rewrites nested inline object properties that were hoisted (canonicalToName),
|
|
634
|
+
* at any depth — a hoisted name nothing points at would be an orphan component.
|
|
553
635
|
*/
|
|
554
636
|
|
|
555
637
|
function rewriteNestedRefs(
|
|
@@ -559,40 +641,72 @@ function rewriteNestedRefs(
|
|
|
559
641
|
const props = schema.properties as Record<string, unknown> | undefined
|
|
560
642
|
if (!props) return schema
|
|
561
643
|
let newProps: Record<string, unknown> | undefined
|
|
562
|
-
for (const
|
|
644
|
+
for (const key of Object.keys(props)) {
|
|
645
|
+
const val = props[key]
|
|
563
646
|
if (!isObjectSchema(val)) continue
|
|
564
647
|
const child = val as Record<string, unknown>
|
|
565
648
|
const name = canonicalToName.get(canonicalizeSchema(child))
|
|
566
|
-
|
|
649
|
+
const next = name ? { $ref: `#/components/schemas/${name}` } : rewriteNestedRefs(child, canonicalToName)
|
|
650
|
+
if (next === child) continue
|
|
567
651
|
if (!newProps) newProps = { ...props }
|
|
568
|
-
newProps[key] =
|
|
652
|
+
newProps[key] = next
|
|
569
653
|
}
|
|
570
654
|
if (!newProps) return schema
|
|
571
655
|
return { ...schema, properties: newProps }
|
|
572
656
|
}
|
|
573
657
|
|
|
658
|
+
function slotKey(
|
|
659
|
+
pathKey: string,
|
|
660
|
+
method: string,
|
|
661
|
+
role: string,
|
|
662
|
+
status: number | undefined,
|
|
663
|
+
contentType: string,
|
|
664
|
+
): string {
|
|
665
|
+
return `${pathKey}::${method}::${role}::${status ?? ""}::${contentType}`
|
|
666
|
+
}
|
|
667
|
+
|
|
574
668
|
function rewritePathsToRefs(
|
|
575
669
|
paths: Record<string, Record<string, Record<string, unknown>>>,
|
|
576
670
|
slots: SchemaSlot[],
|
|
577
671
|
slotNames: Map<SchemaSlot, string>,
|
|
578
672
|
hoistedCanonicalToName: Map<string, string>,
|
|
579
673
|
): Record<string, Record<string, Record<string, unknown>>> {
|
|
580
|
-
|
|
581
|
-
`${pathKey}::${method}::${role}::${status ?? ""}`
|
|
582
|
-
|
|
674
|
+
/* keyed by operation + status + content type: JSON and XML bodies of one status are two slots */
|
|
583
675
|
const lookup = new Map<string, string>()
|
|
584
676
|
for (const slot of slots) {
|
|
585
677
|
const name = slotNames.get(slot)
|
|
586
|
-
if (name)
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
678
|
+
if (!name) continue
|
|
679
|
+
const status = slot.context.role === "response" ? slot.context.status : undefined
|
|
680
|
+
lookup.set(slotKey(slot.pathKey, slot.context.method, slot.context.role, status, slot.contentType), name)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const rewriteContent = (
|
|
684
|
+
content: Record<string, Record<string, unknown>>,
|
|
685
|
+
keyFor: (ct: string) => string,
|
|
686
|
+
): Record<string, Record<string, unknown>> | null => {
|
|
687
|
+
const newContent: Record<string, Record<string, unknown>> = {}
|
|
688
|
+
let changed = false
|
|
689
|
+
for (const [ct, mediaType] of Object.entries(content)) {
|
|
690
|
+
const schema = mediaType.schema as Record<string, unknown> | undefined
|
|
691
|
+
if (!schema || schema.$ref) {
|
|
692
|
+
newContent[ct] = mediaType
|
|
693
|
+
continue
|
|
694
|
+
}
|
|
695
|
+
const name = lookup.get(keyFor(ct))
|
|
696
|
+
if (name) {
|
|
697
|
+
newContent[ct] = { ...mediaType, schema: { $ref: `#/components/schemas/${name}` } }
|
|
698
|
+
changed = true
|
|
699
|
+
continue
|
|
700
|
+
}
|
|
701
|
+
const rewritten = hoistedCanonicalToName.size > 0 ? rewriteNestedRefs(schema, hoistedCanonicalToName) : schema
|
|
702
|
+
if (rewritten !== schema) {
|
|
703
|
+
newContent[ct] = { ...mediaType, schema: rewritten }
|
|
704
|
+
changed = true
|
|
705
|
+
} else {
|
|
706
|
+
newContent[ct] = mediaType
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return changed ? newContent : null
|
|
596
710
|
}
|
|
597
711
|
|
|
598
712
|
const newPaths: Record<string, Record<string, Record<string, unknown>>> = {}
|
|
@@ -606,76 +720,23 @@ function rewritePathsToRefs(
|
|
|
606
720
|
let newOp = op
|
|
607
721
|
|
|
608
722
|
const requestBody = op.requestBody as Record<string, unknown> | undefined
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const newContent: Record<string, Record<string, unknown>> = {}
|
|
614
|
-
for (const [ct, mediaType] of Object.entries(content)) {
|
|
615
|
-
const schema = mediaType.schema as Record<string, unknown> | undefined
|
|
616
|
-
if (schema && !schema.$ref) {
|
|
617
|
-
if (reqName) {
|
|
618
|
-
newContent[ct] = {
|
|
619
|
-
...mediaType,
|
|
620
|
-
schema: { $ref: `#/components/schemas/${reqName}` },
|
|
621
|
-
}
|
|
622
|
-
} else if (hoistedCanonicalToName.size > 0) {
|
|
623
|
-
newContent[ct] = {
|
|
624
|
-
...mediaType,
|
|
625
|
-
schema: rewriteNestedRefs(schema, hoistedCanonicalToName),
|
|
626
|
-
}
|
|
627
|
-
} else {
|
|
628
|
-
newContent[ct] = mediaType
|
|
629
|
-
}
|
|
630
|
-
} else {
|
|
631
|
-
newContent[ct] = mediaType
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
if (Object.keys(newContent).length > 0) {
|
|
635
|
-
newOp = { ...op, requestBody: { ...requestBody, content: newContent } }
|
|
636
|
-
}
|
|
637
|
-
}
|
|
723
|
+
const reqContent = requestBody?.content as Record<string, Record<string, unknown>> | undefined
|
|
724
|
+
if (requestBody && reqContent) {
|
|
725
|
+
const next = rewriteContent(reqContent, (ct) => slotKey(pathKey, m, "request", undefined, ct))
|
|
726
|
+
if (next) newOp = { ...op, requestBody: { ...requestBody, content: next } }
|
|
638
727
|
}
|
|
639
728
|
|
|
640
|
-
const responses =
|
|
641
|
-
| Record<string, Record<string, unknown>>
|
|
642
|
-
| undefined
|
|
729
|
+
const responses = newOp.responses as Record<string, Record<string, unknown>> | undefined
|
|
643
730
|
if (responses) {
|
|
644
731
|
let newResponses: Record<string, Record<string, unknown>> | undefined
|
|
645
732
|
for (const [status, response] of Object.entries(responses)) {
|
|
646
|
-
const content =
|
|
647
|
-
| Record<string, Record<string, unknown>>
|
|
648
|
-
| undefined
|
|
733
|
+
const content = response.content as Record<string, Record<string, unknown>> | undefined
|
|
649
734
|
if (!content) continue
|
|
650
|
-
const statusNum =
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const schema = mediaType.schema as Record<string, unknown> | undefined
|
|
656
|
-
if (schema && !schema.$ref) {
|
|
657
|
-
if (respName) {
|
|
658
|
-
newContent[ct] = {
|
|
659
|
-
...mediaType,
|
|
660
|
-
schema: { $ref: `#/components/schemas/${respName}` },
|
|
661
|
-
}
|
|
662
|
-
changed = true
|
|
663
|
-
} else if (hoistedCanonicalToName.size > 0) {
|
|
664
|
-
const rewritten = rewriteNestedRefs(schema, hoistedCanonicalToName)
|
|
665
|
-
newContent[ct] = { ...mediaType, schema: rewritten }
|
|
666
|
-
if (rewritten !== schema) changed = true
|
|
667
|
-
else newContent[ct] = mediaType
|
|
668
|
-
} else {
|
|
669
|
-
newContent[ct] = mediaType
|
|
670
|
-
}
|
|
671
|
-
} else {
|
|
672
|
-
newContent[ct] = mediaType
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
if (changed) {
|
|
676
|
-
if (!newResponses) newResponses = { ...responses }
|
|
677
|
-
newResponses[status] = { ...(response as Record<string, unknown>), content: newContent }
|
|
678
|
-
}
|
|
735
|
+
const statusNum = statusNumber(status)
|
|
736
|
+
const next = rewriteContent(content, (ct) => slotKey(pathKey, m, "response", statusNum, ct))
|
|
737
|
+
if (!next) continue
|
|
738
|
+
if (!newResponses) newResponses = { ...responses }
|
|
739
|
+
newResponses[status] = { ...response, content: next }
|
|
679
740
|
}
|
|
680
741
|
if (newResponses) newOp = { ...newOp, responses: newResponses }
|
|
681
742
|
}
|
|
@@ -689,18 +750,35 @@ function rewritePathsToRefs(
|
|
|
689
750
|
return newPaths
|
|
690
751
|
}
|
|
691
752
|
|
|
753
|
+
/** Every `#/components/schemas/<name>` a value refers to. */
|
|
754
|
+
function collectComponentRefs(value: unknown, out: Set<string>): void {
|
|
755
|
+
if (Array.isArray(value)) {
|
|
756
|
+
for (const item of value) collectComponentRefs(item, out)
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
if (value === null || typeof value !== "object") return
|
|
760
|
+
for (const [key, child] of Object.entries(value)) {
|
|
761
|
+
if (key === "$ref" && typeof child === "string" && child.startsWith("#/components/schemas/")) {
|
|
762
|
+
out.add(child.slice("#/components/schemas/".length))
|
|
763
|
+
} else {
|
|
764
|
+
collectComponentRefs(child, out)
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
692
769
|
export function deduplicateSchemas(spec: OpenApiSpec): OpenApiSpec {
|
|
693
770
|
/* Sort paths lexicographically for determinism — input order must not affect output. */
|
|
694
771
|
const sortedPaths: Record<string, Record<string, Record<string, unknown>>> = {}
|
|
695
772
|
for (const key of Object.keys(spec.paths).sort()) sortedPaths[key] = spec.paths[key]
|
|
696
773
|
|
|
697
774
|
const slots = collectSchemaSlots(sortedPaths)
|
|
775
|
+
const existing = spec.components?.schemas ?? {}
|
|
698
776
|
|
|
699
777
|
/* Group slots by canonical shape — same shape = one component. */
|
|
700
778
|
const groups = new Map<string, SchemaSlot[]>()
|
|
701
779
|
for (const slot of slots) {
|
|
702
|
-
const
|
|
703
|
-
if (
|
|
780
|
+
const group = groups.get(slot.canonical)
|
|
781
|
+
if (group) group.push(slot)
|
|
704
782
|
else groups.set(slot.canonical, [slot])
|
|
705
783
|
}
|
|
706
784
|
|
|
@@ -709,29 +787,28 @@ export function deduplicateSchemas(spec: OpenApiSpec): OpenApiSpec {
|
|
|
709
787
|
* Tier 1: unique shape (group.length === 1) → operation-derived name
|
|
710
788
|
* Tier 2: shared error envelope → Err{status}* name
|
|
711
789
|
* Tier 3: shared non-error → first-slot operation-derived name
|
|
712
|
-
* Collision (two canonicals → same tier name
|
|
790
|
+
* Collision (two canonicals → same tier name, or a name an existing component holds) →
|
|
791
|
+
* hash suffix, re-checked until free.
|
|
713
792
|
*/
|
|
714
793
|
const canonicalToName = new Map<string, string>()
|
|
715
794
|
const nameToCanonical = new Map<string, string>()
|
|
716
795
|
|
|
717
796
|
for (const [canonical, groupSlots] of groups) {
|
|
718
797
|
const firstSlot = groupSlots[0]
|
|
719
|
-
let
|
|
798
|
+
let base: string
|
|
720
799
|
|
|
721
|
-
if (groupSlots.length
|
|
722
|
-
name = deriveSchemaName(firstSlot.context)
|
|
723
|
-
} else if (isErrorEnvelope(firstSlot.schema)) {
|
|
800
|
+
if (groupSlots.length > 1 && isErrorEnvelope(firstSlot.schema)) {
|
|
724
801
|
const fallbackStatus = firstSlot.context.role === "response" ? firstSlot.context.status : undefined
|
|
725
|
-
|
|
802
|
+
base = deriveErrorEnvelopeName(firstSlot.schema, fallbackStatus)
|
|
726
803
|
} else {
|
|
727
|
-
|
|
804
|
+
base = deriveSchemaName(firstSlot.context)
|
|
728
805
|
}
|
|
729
806
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
807
|
+
const name = uniqueName(
|
|
808
|
+
base,
|
|
809
|
+
canonical,
|
|
810
|
+
(n) => Object.hasOwn(existing, n) || (nameToCanonical.has(n) && nameToCanonical.get(n) !== canonical),
|
|
811
|
+
)
|
|
735
812
|
nameToCanonical.set(name, canonical)
|
|
736
813
|
canonicalToName.set(canonical, name)
|
|
737
814
|
}
|
|
@@ -746,27 +823,23 @@ export function deduplicateSchemas(spec: OpenApiSpec): OpenApiSpec {
|
|
|
746
823
|
/* Nested hoisting: collect shared nested objects, then rewrite inline → $ref.
|
|
747
824
|
* Error envelope slots (any tier) mark their nested schemas as envelope-owned so
|
|
748
825
|
* envelope-exclusive nesting (e.g. `fields`) is not hoisted under a per-spec name. */
|
|
749
|
-
const allTopLevelNames = new Set(slotNames.values())
|
|
750
826
|
const { schemas: hoistedSchemas, canonicalToName: hoistedCanonicalToName } = collectHoistedSchemas(
|
|
751
827
|
slots,
|
|
752
828
|
slotNames,
|
|
753
|
-
|
|
829
|
+
(n) => Object.hasOwn(existing, n) || nameToCanonical.has(n),
|
|
754
830
|
)
|
|
755
831
|
|
|
756
832
|
/* Build components map, rewriting nested fields → $refs where hoisted */
|
|
757
833
|
const extractedSchemas: Record<string, Record<string, unknown>> = {}
|
|
758
|
-
const seenNames = new Set<string>()
|
|
759
834
|
for (const slot of slots) {
|
|
760
835
|
const name = slotNames.get(slot)
|
|
761
|
-
if (name && !
|
|
762
|
-
seenNames.add(name)
|
|
836
|
+
if (name && !Object.hasOwn(extractedSchemas, name)) {
|
|
763
837
|
extractedSchemas[name] =
|
|
764
838
|
hoistedCanonicalToName.size > 0 ? rewriteNestedRefs(slot.schema, hoistedCanonicalToName) : slot.schema
|
|
765
839
|
}
|
|
766
840
|
}
|
|
767
|
-
Object.assign(extractedSchemas, hoistedSchemas)
|
|
768
841
|
|
|
769
|
-
const hasNewSchemas = Object.keys(extractedSchemas).length > 0
|
|
842
|
+
const hasNewSchemas = Object.keys(extractedSchemas).length > 0 || Object.keys(hoistedSchemas).length > 0
|
|
770
843
|
/* Nothing to extract and no existing components — return unchanged */
|
|
771
844
|
if (!hasNewSchemas && !spec.components) return spec
|
|
772
845
|
|
|
@@ -775,7 +848,17 @@ export function deduplicateSchemas(spec: OpenApiSpec): OpenApiSpec {
|
|
|
775
848
|
? rewritePathsToRefs(sortedPaths, slots, slotNames, hoistedCanonicalToName)
|
|
776
849
|
: sortedPaths
|
|
777
850
|
|
|
778
|
-
const mergedSchemas = { ...
|
|
851
|
+
const mergedSchemas: Record<string, Record<string, unknown>> = { ...existing, ...extractedSchemas }
|
|
852
|
+
|
|
853
|
+
/* a hoisted nested schema whose every user became a component $ref is still referenced from
|
|
854
|
+
that component; one referenced from nowhere is dropped */
|
|
855
|
+
const referenced = new Set<string>()
|
|
856
|
+
collectComponentRefs(paths, referenced)
|
|
857
|
+
collectComponentRefs(mergedSchemas, referenced)
|
|
858
|
+
for (const [name, schema] of Object.entries(hoistedSchemas)) {
|
|
859
|
+
if (referenced.has(name)) mergedSchemas[name] = schema
|
|
860
|
+
}
|
|
861
|
+
|
|
779
862
|
const components: OpenApiSpec["components"] = {
|
|
780
863
|
...spec.components,
|
|
781
864
|
...(Object.keys(mergedSchemas).length > 0 ? { schemas: mergedSchemas } : {}),
|
|
@@ -834,35 +917,53 @@ function cloneJson<T>(value: T): T {
|
|
|
834
917
|
}
|
|
835
918
|
}
|
|
836
919
|
|
|
920
|
+
/* Inlining expands each $ref; a graph of shared refs can grow exponentially (`L30` with two
|
|
921
|
+
fields that both point at `L29`, …), so the output is bounded and generation fails loudly. */
|
|
922
|
+
const MAX_INLINED_SCHEMA_NODES = 200_000
|
|
923
|
+
|
|
924
|
+
type InlineBudget = { nodes: number }
|
|
925
|
+
|
|
926
|
+
function chargeInline(budget: InlineBudget): void {
|
|
927
|
+
if (++budget.nodes > MAX_INLINED_SCHEMA_NODES) {
|
|
928
|
+
throw new Error(
|
|
929
|
+
`SDK codegen: inlined schemas exceed ${MAX_INLINED_SCHEMA_NODES} nodes (deeply shared $refs); ` +
|
|
930
|
+
"flatten the shared component chain",
|
|
931
|
+
)
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
837
935
|
function resolveSchema(
|
|
838
936
|
schema: Record<string, unknown>,
|
|
839
937
|
schemas: Record<string, Record<string, unknown>>,
|
|
938
|
+
budget: InlineBudget,
|
|
840
939
|
visited = new Set<string>(),
|
|
841
940
|
): Record<string, unknown> {
|
|
842
941
|
if (typeof schema.$ref === "string" && schema.$ref.startsWith("#/components/schemas/")) {
|
|
843
942
|
const name = schema.$ref.slice("#/components/schemas/".length)
|
|
844
943
|
/* circular ref guard — return empty schema which maps to unknown in IR */
|
|
845
944
|
if (visited.has(name)) return {}
|
|
846
|
-
const resolved = schemas[name]
|
|
945
|
+
const resolved = Object.hasOwn(schemas, name) ? schemas[name] : undefined
|
|
847
946
|
if (!resolved) throw new Error(`$ref points to nonexistent component: ${schema.$ref}`)
|
|
848
947
|
const next = new Set(visited)
|
|
849
948
|
next.add(name)
|
|
850
|
-
return resolveSchemaDeep(cloneJson(resolved), schemas, next)
|
|
949
|
+
return resolveSchemaDeep(cloneJson(resolved), schemas, budget, next)
|
|
851
950
|
}
|
|
852
|
-
return resolveSchemaDeep(schema, schemas, visited)
|
|
951
|
+
return resolveSchemaDeep(schema, schemas, budget, visited)
|
|
853
952
|
}
|
|
854
953
|
|
|
855
954
|
function resolveSchemaDeep(
|
|
856
955
|
schema: Record<string, unknown>,
|
|
857
956
|
schemas: Record<string, Record<string, unknown>>,
|
|
957
|
+
budget: InlineBudget,
|
|
858
958
|
visited: Set<string>,
|
|
859
959
|
): Record<string, unknown> {
|
|
960
|
+
chargeInline(budget)
|
|
860
961
|
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
861
962
|
const arr = schema[key]
|
|
862
963
|
if (Array.isArray(arr)) {
|
|
863
964
|
schema[key] = arr.map((item: unknown) => {
|
|
864
965
|
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
865
|
-
return resolveSchema(item as Record<string, unknown>, schemas, visited)
|
|
966
|
+
return resolveSchema(item as Record<string, unknown>, schemas, budget, visited)
|
|
866
967
|
}
|
|
867
968
|
return item
|
|
868
969
|
})
|
|
@@ -870,14 +971,14 @@ function resolveSchemaDeep(
|
|
|
870
971
|
}
|
|
871
972
|
/* resolve array items */
|
|
872
973
|
if (schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
|
|
873
|
-
schema.items = resolveSchema(schema.items as Record<string, unknown>, schemas, visited)
|
|
974
|
+
schema.items = resolveSchema(schema.items as Record<string, unknown>, schemas, budget, visited)
|
|
874
975
|
}
|
|
875
976
|
/* resolve object properties */
|
|
876
977
|
if (schema.properties && typeof schema.properties === "object") {
|
|
877
978
|
const props = schema.properties as Record<string, unknown>
|
|
878
979
|
for (const [k, v] of Object.entries(props)) {
|
|
879
980
|
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
880
|
-
props[k] = resolveSchema(v as Record<string, unknown>, schemas, visited)
|
|
981
|
+
props[k] = resolveSchema(v as Record<string, unknown>, schemas, budget, visited)
|
|
881
982
|
}
|
|
882
983
|
}
|
|
883
984
|
}
|
|
@@ -890,6 +991,7 @@ function resolveSchemaDeep(
|
|
|
890
991
|
schema.additionalProperties = resolveSchema(
|
|
891
992
|
schema.additionalProperties as Record<string, unknown>,
|
|
892
993
|
schemas,
|
|
994
|
+
budget,
|
|
893
995
|
visited,
|
|
894
996
|
)
|
|
895
997
|
}
|
|
@@ -901,6 +1003,7 @@ export function resolveRefs(spec: OpenApiSpecInput): OpenApiSpecInput {
|
|
|
901
1003
|
if (!schemas || Object.keys(schemas).length === 0) return spec
|
|
902
1004
|
|
|
903
1005
|
const paths = cloneJson(spec.paths)
|
|
1006
|
+
const budget: InlineBudget = { nodes: 0 }
|
|
904
1007
|
|
|
905
1008
|
for (const methods of Object.values(paths)) {
|
|
906
1009
|
for (const operation of Object.values(methods)) {
|
|
@@ -912,7 +1015,7 @@ export function resolveRefs(spec: OpenApiSpecInput): OpenApiSpecInput {
|
|
|
912
1015
|
if (content) {
|
|
913
1016
|
for (const [ct, mediaType] of Object.entries(content)) {
|
|
914
1017
|
const schema = mediaType.schema as Record<string, unknown> | undefined
|
|
915
|
-
if (schema) content[ct] = { ...mediaType, schema: resolveSchema(schema, schemas) }
|
|
1018
|
+
if (schema) content[ct] = { ...mediaType, schema: resolveSchema(schema, schemas, budget) }
|
|
916
1019
|
}
|
|
917
1020
|
}
|
|
918
1021
|
}
|
|
@@ -921,20 +1024,14 @@ export function resolveRefs(spec: OpenApiSpecInput): OpenApiSpecInput {
|
|
|
921
1024
|
if (responses) {
|
|
922
1025
|
for (const [status, response] of Object.entries(responses)) {
|
|
923
1026
|
const content = response.content as Record<string, Record<string, unknown>> | undefined
|
|
924
|
-
if (content)
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
content: {
|
|
931
|
-
...content,
|
|
932
|
-
[ct]: { ...mediaType, schema: resolveSchema(schema, schemas) },
|
|
933
|
-
},
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
1027
|
+
if (!content) continue
|
|
1028
|
+
/* every content type resolved, not only the last one written */
|
|
1029
|
+
const resolved: Record<string, Record<string, unknown>> = {}
|
|
1030
|
+
for (const [ct, mediaType] of Object.entries(content)) {
|
|
1031
|
+
const schema = mediaType.schema as Record<string, unknown> | undefined
|
|
1032
|
+
resolved[ct] = schema ? { ...mediaType, schema: resolveSchema(schema, schemas, budget) } : mediaType
|
|
937
1033
|
}
|
|
1034
|
+
responses[status] = { ...response, content: resolved }
|
|
938
1035
|
}
|
|
939
1036
|
}
|
|
940
1037
|
}
|
|
@@ -946,25 +1043,60 @@ export function resolveRefs(spec: OpenApiSpecInput): OpenApiSpecInput {
|
|
|
946
1043
|
return { ...spec, components, paths }
|
|
947
1044
|
}
|
|
948
1045
|
|
|
1046
|
+
/**
|
|
1047
|
+
* Zod node kinds JSON Schema has no word for. Zod emits `{}` for each of them under
|
|
1048
|
+
* `unrepresentable: "any"`; the degrade is per node, and the rest of the schema survives.
|
|
1049
|
+
*/
|
|
1050
|
+
const ZOD_UNREPRESENTABLE = new Set(["custom", "function", "map", "nan", "promise", "set", "symbol", "transform"])
|
|
1051
|
+
|
|
1052
|
+
type ZodOverrideCtx = { jsonSchema: Record<string, unknown>; zodSchema: unknown }
|
|
1053
|
+
|
|
1054
|
+
/** `override` for Zod's converter: give `Date` and `BigInt` their wire shape, record what degraded. */
|
|
1055
|
+
function zodOverride(degraded: Set<string>): (ctx: ZodOverrideCtx) => void {
|
|
1056
|
+
return (ctx) => {
|
|
1057
|
+
const def = zodDefOf(ctx.zodSchema)
|
|
1058
|
+
const kind = String(def?.type ?? "")
|
|
1059
|
+
if (kind === "date") {
|
|
1060
|
+
/* JSON.stringify writes a Date as an ISO string */
|
|
1061
|
+
ctx.jsonSchema.format = "date-time"
|
|
1062
|
+
ctx.jsonSchema.type = "string"
|
|
1063
|
+
return
|
|
1064
|
+
}
|
|
1065
|
+
if (kind === "bigint") {
|
|
1066
|
+
ctx.jsonSchema.format = "int64"
|
|
1067
|
+
ctx.jsonSchema.type = "integer"
|
|
1068
|
+
return
|
|
1069
|
+
}
|
|
1070
|
+
if (ZOD_UNREPRESENTABLE.has(kind) && Object.keys(ctx.jsonSchema).length === 0) degraded.add(kind)
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
949
1074
|
/** Zod 4 puts converters on the schema instance. Use those first so Workers
|
|
950
1075
|
* (where `import("zod")` does not resolve) still emit real JSON Schema. */
|
|
951
1076
|
function zodInstanceToJsonSchema(
|
|
952
1077
|
schema: StandardSchemaLike,
|
|
953
1078
|
io: "input" | "output",
|
|
1079
|
+
degraded: Set<string>,
|
|
954
1080
|
): Record<string, unknown> | undefined {
|
|
1081
|
+
const libraryOptions = { override: zodOverride(degraded), unrepresentable: "any" as const }
|
|
955
1082
|
const jsonSchema = (
|
|
956
1083
|
schema["~standard"] as {
|
|
957
|
-
jsonSchema?:
|
|
1084
|
+
jsonSchema?: Record<"input" | "output", ((params?: unknown) => unknown) | undefined>
|
|
958
1085
|
}
|
|
959
1086
|
).jsonSchema
|
|
960
1087
|
const fromStandard = jsonSchema?.[io]
|
|
961
1088
|
if (typeof fromStandard === "function") {
|
|
962
|
-
const result = fromStandard()
|
|
1089
|
+
const result = fromStandard({ libraryOptions, target: "draft-2020-12" })
|
|
963
1090
|
if (result && typeof result === "object") return result as Record<string, unknown>
|
|
964
1091
|
}
|
|
965
|
-
const inst = (schema as { toJSONSchema?: (opts?:
|
|
1092
|
+
const inst = (schema as { toJSONSchema?: (opts?: Record<string, unknown>) => unknown }).toJSONSchema
|
|
966
1093
|
if (typeof inst === "function") {
|
|
967
|
-
const result = inst.call(schema, { io })
|
|
1094
|
+
const result = inst.call(schema, { ...libraryOptions, io })
|
|
1095
|
+
if (result && typeof result === "object") return result as Record<string, unknown>
|
|
1096
|
+
}
|
|
1097
|
+
if (toJSONSchemaFn) {
|
|
1098
|
+
const convert = toJSONSchemaFn as (schema: unknown, opts: Record<string, unknown>) => unknown
|
|
1099
|
+
const result = convert(schema, { ...libraryOptions, io })
|
|
968
1100
|
if (result && typeof result === "object") return result as Record<string, unknown>
|
|
969
1101
|
}
|
|
970
1102
|
return undefined
|
|
@@ -997,32 +1129,47 @@ function zodDefOf(schema: unknown): Record<string, unknown> | undefined {
|
|
|
997
1129
|
return s._def ?? s.def ?? s._zod?.def
|
|
998
1130
|
}
|
|
999
1131
|
|
|
1000
|
-
function zodDefIsOptional(def: Record<string, unknown
|
|
1132
|
+
function zodDefIsOptional(def: Record<string, unknown>, io: "input" | "output"): boolean {
|
|
1001
1133
|
const t = String(def.typeName ?? def.type ?? "")
|
|
1002
1134
|
if (t === "ZodOptional" || t === "optional") return true
|
|
1135
|
+
/* a default fills the value in: optional on the way in, always present on the way out */
|
|
1136
|
+
if ((t === "ZodDefault" || t === "default" || t === "prefault") && io === "input") return true
|
|
1003
1137
|
if (
|
|
1004
1138
|
t === "ZodNullable" ||
|
|
1005
1139
|
t === "nullable" ||
|
|
1006
1140
|
t === "ZodDefault" ||
|
|
1007
1141
|
t === "default" ||
|
|
1142
|
+
t === "prefault" ||
|
|
1008
1143
|
t === "ZodCatch" ||
|
|
1009
|
-
t === "catch"
|
|
1144
|
+
t === "catch" ||
|
|
1145
|
+
t === "ZodReadonly" ||
|
|
1146
|
+
t === "readonly"
|
|
1010
1147
|
) {
|
|
1011
1148
|
const inner = def.innerType
|
|
1012
1149
|
if (inner) {
|
|
1013
1150
|
const innerDef = zodDefOf(inner)
|
|
1014
|
-
if (innerDef) return zodDefIsOptional(innerDef)
|
|
1151
|
+
if (innerDef) return zodDefIsOptional(innerDef, io)
|
|
1015
1152
|
}
|
|
1016
1153
|
}
|
|
1017
1154
|
return false
|
|
1018
1155
|
}
|
|
1019
1156
|
|
|
1157
|
+
function literalSchema(values: readonly unknown[]): Record<string, unknown> {
|
|
1158
|
+
if (values.length === 1) return { const: values[0] }
|
|
1159
|
+
return { enum: [...values] }
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1020
1162
|
/** Walk Zod 3/4 internals. Survives Workers bundles that drop `toJSONSchema`. */
|
|
1021
|
-
function zodDefToJsonSchema(
|
|
1163
|
+
function zodDefToJsonSchema(
|
|
1164
|
+
schema: unknown,
|
|
1165
|
+
io: "input" | "output" = "output",
|
|
1166
|
+
depth = 0,
|
|
1167
|
+
): Record<string, unknown> | undefined {
|
|
1022
1168
|
if (depth > 24) return {}
|
|
1023
1169
|
const def = zodDefOf(schema)
|
|
1024
1170
|
if (!def) return undefined
|
|
1025
1171
|
const typeName = String(def.typeName ?? def.type ?? "")
|
|
1172
|
+
const walk = (child: unknown): Record<string, unknown> => zodDefToJsonSchema(child, io, depth + 1) ?? {}
|
|
1026
1173
|
|
|
1027
1174
|
switch (typeName) {
|
|
1028
1175
|
case "ZodString":
|
|
@@ -1036,7 +1183,7 @@ function zodDefToJsonSchema(schema: unknown, depth = 0): Record<string, unknown>
|
|
|
1036
1183
|
return { type: "boolean" }
|
|
1037
1184
|
case "ZodBigInt":
|
|
1038
1185
|
case "bigint":
|
|
1039
|
-
return { type: "integer" }
|
|
1186
|
+
return { format: "int64", type: "integer" }
|
|
1040
1187
|
case "ZodDate":
|
|
1041
1188
|
case "date":
|
|
1042
1189
|
return { format: "date-time", type: "string" }
|
|
@@ -1058,14 +1205,25 @@ function zodDefToJsonSchema(schema: unknown, depth = 0): Record<string, unknown>
|
|
|
1058
1205
|
return {}
|
|
1059
1206
|
case "ZodLiteral":
|
|
1060
1207
|
case "literal": {
|
|
1061
|
-
const
|
|
1062
|
-
return
|
|
1208
|
+
const values = Array.isArray(def.values) ? (def.values as unknown[]) : [def.value]
|
|
1209
|
+
return literalSchema(values.map((v) => (typeof v === "bigint" ? Number(v) : v)))
|
|
1063
1210
|
}
|
|
1064
1211
|
case "ZodEnum":
|
|
1065
1212
|
case "enum": {
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1213
|
+
const raw = (def.values ?? def.entries) as unknown
|
|
1214
|
+
let list: unknown[]
|
|
1215
|
+
if (Array.isArray(raw)) list = raw
|
|
1216
|
+
else {
|
|
1217
|
+
/* a TS numeric enum carries reverse mappings: { A: 0, "0": "A" } — keep the values */
|
|
1218
|
+
const entries = raw as Record<string, unknown>
|
|
1219
|
+
list = Object.keys(entries)
|
|
1220
|
+
.filter((k) => typeof entries[entries[k] as string] !== "number")
|
|
1221
|
+
.map((k) => entries[k])
|
|
1222
|
+
}
|
|
1223
|
+
const types = new Set(list.map((v) => typeof v))
|
|
1224
|
+
if (types.size === 1 && types.has("string")) return { enum: list, type: "string" }
|
|
1225
|
+
if (types.size === 1 && types.has("number")) return { enum: list, type: "number" }
|
|
1226
|
+
return { enum: list }
|
|
1069
1227
|
}
|
|
1070
1228
|
case "ZodObject":
|
|
1071
1229
|
case "object": {
|
|
@@ -1074,60 +1232,88 @@ function zodDefToJsonSchema(schema: unknown, depth = 0): Record<string, unknown>
|
|
|
1074
1232
|
if (!shape) return { type: "object" }
|
|
1075
1233
|
const properties: Record<string, unknown> = {}
|
|
1076
1234
|
const required: string[] = []
|
|
1077
|
-
for (const
|
|
1078
|
-
const
|
|
1079
|
-
properties[key] =
|
|
1235
|
+
for (const key of Object.keys(shape)) {
|
|
1236
|
+
const prop = shape[key]
|
|
1237
|
+
properties[key] = walk(prop)
|
|
1080
1238
|
const propDef = zodDefOf(prop)
|
|
1081
|
-
if (!propDef || !zodDefIsOptional(propDef)) required.push(key)
|
|
1239
|
+
if (!propDef || !zodDefIsOptional(propDef, io)) required.push(key)
|
|
1082
1240
|
}
|
|
1083
|
-
const result: Record<string, unknown> = {
|
|
1241
|
+
const result: Record<string, unknown> = { properties, type: "object" }
|
|
1242
|
+
if (io === "output") result.additionalProperties = false
|
|
1084
1243
|
if (required.length > 0) result.required = required
|
|
1085
1244
|
return result
|
|
1086
1245
|
}
|
|
1087
1246
|
case "ZodArray":
|
|
1088
1247
|
case "array":
|
|
1089
|
-
return { items:
|
|
1248
|
+
return { items: walk(def.element ?? def.type), type: "array" }
|
|
1090
1249
|
case "ZodOptional":
|
|
1091
1250
|
case "optional":
|
|
1092
|
-
return zodDefToJsonSchema(def.innerType, depth + 1)
|
|
1251
|
+
return zodDefToJsonSchema(def.innerType, io, depth + 1)
|
|
1093
1252
|
case "ZodNullable":
|
|
1094
|
-
case "nullable":
|
|
1095
|
-
|
|
1096
|
-
return { anyOf: [inner, { type: "null" }] }
|
|
1097
|
-
}
|
|
1253
|
+
case "nullable":
|
|
1254
|
+
return { anyOf: [walk(def.innerType), { type: "null" }] }
|
|
1098
1255
|
case "ZodUnion":
|
|
1099
1256
|
case "ZodDiscriminatedUnion":
|
|
1100
1257
|
case "union":
|
|
1101
|
-
return { anyOf: ((def.options as unknown[]) ?? []).map(
|
|
1258
|
+
return { anyOf: ((def.options as unknown[]) ?? []).map(walk) }
|
|
1259
|
+
case "ZodIntersection":
|
|
1260
|
+
case "intersection":
|
|
1261
|
+
return { allOf: [walk(def.left), walk(def.right)] }
|
|
1102
1262
|
case "ZodRecord":
|
|
1103
1263
|
case "record":
|
|
1104
|
-
return {
|
|
1105
|
-
additionalProperties: zodDefToJsonSchema(def.valueType, depth + 1) ?? {},
|
|
1106
|
-
type: "object",
|
|
1107
|
-
}
|
|
1264
|
+
return { additionalProperties: walk(def.valueType), type: "object" }
|
|
1108
1265
|
case "ZodTuple":
|
|
1109
|
-
case "tuple":
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1266
|
+
case "tuple": {
|
|
1267
|
+
const items = ((def.items as unknown[]) ?? []).map(walk)
|
|
1268
|
+
const out: Record<string, unknown> = { prefixItems: items, type: "array" }
|
|
1269
|
+
if (def.rest) out.items = walk(def.rest)
|
|
1270
|
+
else {
|
|
1271
|
+
out.items = false
|
|
1272
|
+
out.maxItems = items.length
|
|
1113
1273
|
}
|
|
1274
|
+
out.minItems = items.length
|
|
1275
|
+
return out
|
|
1276
|
+
}
|
|
1114
1277
|
case "ZodDefault":
|
|
1115
|
-
case "ZodCatch":
|
|
1116
1278
|
case "default":
|
|
1279
|
+
case "prefault": {
|
|
1280
|
+
const inner = walk(def.innerType)
|
|
1281
|
+
const value = (def as { defaultValue?: unknown }).defaultValue
|
|
1282
|
+
const resolved = typeof value === "function" ? undefined : value
|
|
1283
|
+
return resolved === undefined ? inner : { ...inner, default: resolved }
|
|
1284
|
+
}
|
|
1285
|
+
case "ZodCatch":
|
|
1117
1286
|
case "catch":
|
|
1118
1287
|
case "ZodReadonly":
|
|
1119
1288
|
case "readonly":
|
|
1120
|
-
return zodDefToJsonSchema(def.innerType, depth + 1)
|
|
1289
|
+
return zodDefToJsonSchema(def.innerType, io, depth + 1)
|
|
1121
1290
|
case "ZodPipeline":
|
|
1122
1291
|
case "pipe":
|
|
1123
|
-
|
|
1292
|
+
/* a pipe validates `in` and returns `out` */
|
|
1293
|
+
return zodDefToJsonSchema(io === "input" ? def.in : def.out, io, depth + 1)
|
|
1124
1294
|
case "ZodBranded":
|
|
1125
|
-
return zodDefToJsonSchema(def.type, depth + 1)
|
|
1295
|
+
return zodDefToJsonSchema(def.type, io, depth + 1)
|
|
1296
|
+
case "ZodLazy":
|
|
1297
|
+
case "lazy": {
|
|
1298
|
+
const getter = def.getter as (() => unknown) | undefined
|
|
1299
|
+
return typeof getter === "function" ? zodDefToJsonSchema(getter(), io, depth + 1) : {}
|
|
1300
|
+
}
|
|
1126
1301
|
default:
|
|
1127
1302
|
return undefined
|
|
1128
1303
|
}
|
|
1129
1304
|
}
|
|
1130
1305
|
|
|
1306
|
+
/** How a schema that cannot be converted at all is handled. Set per `generateOpenApi` call. */
|
|
1307
|
+
let schemaFailureMode: "throw" | "warn" = "throw"
|
|
1308
|
+
|
|
1309
|
+
function schemaConversionFailed(what: string, io: "input" | "output", err: unknown): Record<string, unknown> {
|
|
1310
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
1311
|
+
const text = `[honey:codegen] schemaToJsonSchema could not convert ${what} (io=${io}): ${msg}`
|
|
1312
|
+
if (schemaFailureMode === "throw") throw new Error(text)
|
|
1313
|
+
console.warn(`${text}. Documented as an unconstrained schema.`)
|
|
1314
|
+
return {}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1131
1317
|
function schemaToJsonSchema(schema: StandardSchemaLike, io: "input" | "output" = "output"): unknown {
|
|
1132
1318
|
/* Pre-serialized JSON-schema blob or non-schema value — no ~standard marker.
|
|
1133
1319
|
* Arktype schemas are functions but still carry the marker, so pass those through too. */
|
|
@@ -1141,66 +1327,86 @@ function schemaToJsonSchema(schema: StandardSchemaLike, io: "input" | "output" =
|
|
|
1141
1327
|
const vendor = schema["~standard"].vendor
|
|
1142
1328
|
|
|
1143
1329
|
if (vendor === "zod") {
|
|
1330
|
+
const kind = (schema as { _zod?: { def?: { type?: string } } })?._zod?.def?.type ?? "unknown"
|
|
1331
|
+
const degraded = new Set<string>()
|
|
1332
|
+
let result: Record<string, unknown> | undefined
|
|
1144
1333
|
try {
|
|
1145
|
-
|
|
1146
|
-
zodInstanceToJsonSchema(schema, io) ??
|
|
1147
|
-
(toJSONSchemaFn?.(schema, { io }) as Record<string, unknown> | undefined) ??
|
|
1148
|
-
zodDefToJsonSchema(schema)
|
|
1149
|
-
if (result) {
|
|
1150
|
-
return sanitizeZodJsonSchema(result)
|
|
1151
|
-
}
|
|
1334
|
+
result = zodInstanceToJsonSchema(schema, io, degraded) ?? zodDefToJsonSchema(schema, io)
|
|
1152
1335
|
} catch (err) {
|
|
1153
|
-
|
|
1154
|
-
|
|
1336
|
+
return schemaConversionFailed(`zod/${kind}`, io, err)
|
|
1337
|
+
}
|
|
1338
|
+
if (!result) return schemaConversionFailed(`zod/${kind}`, io, new Error("no converter available"))
|
|
1339
|
+
if (degraded.size > 0) {
|
|
1155
1340
|
console.warn(
|
|
1156
|
-
`[honey:codegen] schemaToJsonSchema
|
|
1341
|
+
`[honey:codegen] schemaToJsonSchema: zod/${kind} (io=${io}) has ${[...degraded].sort().join(", ")} ` +
|
|
1342
|
+
"node(s) JSON Schema cannot express; each is documented as an unconstrained schema. " +
|
|
1343
|
+
'Describe them with .meta({ ... }) or z.toJSONSchema\'s "override".',
|
|
1157
1344
|
)
|
|
1158
1345
|
}
|
|
1346
|
+
return sanitizeZodJsonSchema(result)
|
|
1159
1347
|
}
|
|
1160
1348
|
|
|
1161
|
-
|
|
1162
|
-
return valibotToJsonSchema(schema)
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
if (vendor === "yup") {
|
|
1170
|
-
return yupToJsonSchema(schema)
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
if (vendor === "effect") {
|
|
1174
|
-
return effectToJsonSchema(schema)
|
|
1349
|
+
try {
|
|
1350
|
+
if (vendor === "valibot") return valibotToJsonSchema(schema)
|
|
1351
|
+
if (vendor === "arktype") return arkTypeToJsonSchema(schema)
|
|
1352
|
+
if (vendor === "yup") return yupToJsonSchema(schema)
|
|
1353
|
+
if (vendor === "effect") return effectToJsonSchema(schema)
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
return schemaConversionFailed(vendor, io, err)
|
|
1175
1356
|
}
|
|
1176
1357
|
|
|
1177
|
-
|
|
1358
|
+
/* no converter for this vendor: say so once per schema, never emit the introspection object */
|
|
1359
|
+
console.warn(
|
|
1360
|
+
`[honey:codegen] schemaToJsonSchema: no JSON Schema converter for vendor "${vendor}"; ` +
|
|
1361
|
+
"documented as an unconstrained schema.",
|
|
1362
|
+
)
|
|
1363
|
+
return {}
|
|
1178
1364
|
}
|
|
1179
1365
|
|
|
1180
1366
|
/* ---- Manifest generation ---- */
|
|
1181
1367
|
|
|
1368
|
+
export type GenerateManifestOptions = {
|
|
1369
|
+
/** Drop routes the predicate rejects, as `openapi({ filterRoutes })` does. */
|
|
1370
|
+
filterRoutes?: (route: OpenApiRouteInfo) => boolean
|
|
1371
|
+
/**
|
|
1372
|
+
* `"all"` (default, the `honey generate` artifact): every route and meta key.
|
|
1373
|
+
* `"published"` (a served `/manifest.json`): the OpenAPI document's visibility policy —
|
|
1374
|
+
* `meta.internal` routes are left out, and meta carries only keys the app's `metaSpec`
|
|
1375
|
+
* maps (built-ins included) and does not hide.
|
|
1376
|
+
*/
|
|
1377
|
+
visibility?: "all" | "published"
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1182
1380
|
export function generateManifest<TEnv, TCtx>(
|
|
1183
1381
|
app: Honey<TEnv, TCtx, unknown, unknown, unknown, string, string>,
|
|
1382
|
+
options: GenerateManifestOptions = {},
|
|
1184
1383
|
): RouteManifest {
|
|
1185
|
-
const tree = (app as unknown as { _tree: TreeNode })._tree
|
|
1186
1384
|
const factory = getErrorFactory(app)
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1385
|
+
const published = options.visibility === "published"
|
|
1386
|
+
const publishable = published ? publishableMetaKeys(metaSpecOf(app)) : null
|
|
1387
|
+
const collected: CollectedRoute[] = collectRoutes(app).filter(({ handler, method, path }) => {
|
|
1388
|
+
if (published && isMetaInternal(handler)) return false
|
|
1389
|
+
return !options.filterRoutes || options.filterRoutes({ meta: handler.mt ?? EMPTY_OBJ, method, path })
|
|
1390
|
+
})
|
|
1189
1391
|
|
|
1190
|
-
|
|
1392
|
+
/* each key resolved against the factory of a route that declares it — a mounted sub-app keeps its own */
|
|
1393
|
+
const allErrorKeys = new Map<string, Record<string, () => HoneyError> | null>()
|
|
1191
1394
|
|
|
1192
1395
|
const routes: RouteManifestEntry[] = collected.map(({ handler, method, path }) => {
|
|
1193
1396
|
const entry: RouteManifestEntry = {
|
|
1194
1397
|
errors: Array.from(handler.ek),
|
|
1195
|
-
meta:
|
|
1398
|
+
meta: publishable
|
|
1399
|
+
? Object.fromEntries(Object.entries(handler.mt ?? EMPTY_OBJ).filter(([key]) => publishable(key)))
|
|
1400
|
+
: (handler.mt ?? EMPTY_OBJ),
|
|
1196
1401
|
method,
|
|
1197
1402
|
middleware: handler.mw.map((mw) => mw.name || "anonymous"),
|
|
1198
1403
|
params: extractParams(path),
|
|
1199
1404
|
path,
|
|
1200
1405
|
}
|
|
1201
1406
|
|
|
1407
|
+
const routeFactory = (handler.fac as Record<string, () => HoneyError> | null | undefined) ?? factory
|
|
1202
1408
|
for (const ek of handler.ek) {
|
|
1203
|
-
allErrorKeys.
|
|
1409
|
+
if (!allErrorKeys.has(ek)) allErrorKeys.set(ek, routeFactory)
|
|
1204
1410
|
}
|
|
1205
1411
|
|
|
1206
1412
|
if (handler.iv) {
|
|
@@ -1235,7 +1441,7 @@ export function generateManifest<TEnv, TCtx>(
|
|
|
1235
1441
|
})
|
|
1236
1442
|
|
|
1237
1443
|
return {
|
|
1238
|
-
errors: Array.from(allErrorKeys).map((key) => resolveErrorInfo(key,
|
|
1444
|
+
errors: Array.from(allErrorKeys).map(([key, keyFactory]) => resolveErrorInfo(key, keyFactory)),
|
|
1239
1445
|
routes,
|
|
1240
1446
|
}
|
|
1241
1447
|
}
|
|
@@ -1253,6 +1459,13 @@ export async function generateOpenApi<TEnv, TCtx, TMeta = unknown>(
|
|
|
1253
1459
|
* nothing. `"off"` for a document served at runtime.
|
|
1254
1460
|
*/
|
|
1255
1461
|
invalidate?: InvalidateCheckConfig
|
|
1462
|
+
/**
|
|
1463
|
+
* A schema that cannot be converted at all: `"throw"` (default, `honey generate`) fails
|
|
1464
|
+
* the build; `"warn"` (a served document) logs it and documents an unconstrained schema.
|
|
1465
|
+
* A single node JSON Schema cannot express (`z.date()`, `z.custom()`) never fails either
|
|
1466
|
+
* way — only that node degrades.
|
|
1467
|
+
*/
|
|
1468
|
+
onSchemaError?: "throw" | "warn"
|
|
1256
1469
|
/** Named metaSpec profile — selects which emitted keys this document carries */
|
|
1257
1470
|
profile?: string
|
|
1258
1471
|
securitySchemes?: Record<string, unknown>
|
|
@@ -1260,21 +1473,23 @@ export async function generateOpenApi<TEnv, TCtx, TMeta = unknown>(
|
|
|
1260
1473
|
): Promise<OpenApiSpec> {
|
|
1261
1474
|
await Promise.all([loadToJSONSchema(), loadEffectJsonSchema()])
|
|
1262
1475
|
const prev = getJsonSchemaConverter()
|
|
1476
|
+
const prevMode = schemaFailureMode
|
|
1477
|
+
schemaFailureMode = options.onSchemaError ?? "throw"
|
|
1263
1478
|
setJsonSchemaConverter((schema, io) => schemaToJsonSchema(schema, io) as Record<string, unknown>)
|
|
1264
1479
|
try {
|
|
1265
|
-
const
|
|
1480
|
+
const { onSchemaError: _mode, ...rest } = options
|
|
1481
|
+
const spec = generateOpenApiFromTree(app, rest)
|
|
1266
1482
|
return deduplicateSchemas(spec)
|
|
1267
1483
|
} finally {
|
|
1268
1484
|
setJsonSchemaConverter(prev)
|
|
1485
|
+
schemaFailureMode = prevMode
|
|
1269
1486
|
}
|
|
1270
1487
|
}
|
|
1271
1488
|
|
|
1272
1489
|
export function extractSchemas<TEnv, TCtx>(
|
|
1273
1490
|
app: Honey<TEnv, TCtx, unknown, unknown, unknown, string, string>,
|
|
1274
1491
|
): Record<string, Record<string, unknown>> {
|
|
1275
|
-
const
|
|
1276
|
-
const collected: CollectedRoute[] = []
|
|
1277
|
-
walkTree(tree, "", collected)
|
|
1492
|
+
const collected: CollectedRoute[] = collectRoutes(app)
|
|
1278
1493
|
|
|
1279
1494
|
const result: Record<string, Record<string, unknown>> = {}
|
|
1280
1495
|
|
|
@@ -1311,213 +1526,72 @@ export function extractSchemas<TEnv, TCtx>(
|
|
|
1311
1526
|
|
|
1312
1527
|
/* ---- Static route tree codegen ---- */
|
|
1313
1528
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
/**
|
|
1323
|
-
* JSON Schema per (contentType, statusKey). Special case: `redirect` contentType holds
|
|
1324
|
-
* `Record<statusKey, true>` with no schema (declaration-only, no body to validate).
|
|
1325
|
-
* null = no output validation declared.
|
|
1326
|
-
*/
|
|
1327
|
-
outputSchemas: Record<string, Record<string, unknown>> | null
|
|
1328
|
-
path: string
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
function serializeInputSchemas(iv: InputSchemasDef | null): Record<string, Record<string, unknown>> | null {
|
|
1332
|
-
if (!iv) return null
|
|
1333
|
-
const result: Record<string, Record<string, unknown>> = {}
|
|
1334
|
-
for (const [source, entry] of Object.entries(iv)) {
|
|
1335
|
-
if (entry === undefined) continue
|
|
1336
|
-
const unwrapped = unwrapEntry(entry as InputSchemaEntry)
|
|
1337
|
-
result[source] = schemaToJsonSchema(unwrapped, "input") as Record<string, unknown>
|
|
1338
|
-
}
|
|
1339
|
-
return Object.keys(result).length > 0 ? result : null
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
function serializeOutputSchemas(os: OutputSchemaDef | null): Record<string, Record<string, unknown>> | null {
|
|
1343
|
-
if (!os) return null
|
|
1344
|
-
const result: Record<string, Record<string, unknown>> = {}
|
|
1345
|
-
for (const [contentType, schemas] of Object.entries(os)) {
|
|
1346
|
-
if (schemas === undefined) continue
|
|
1347
|
-
if (contentType === "redirect") {
|
|
1348
|
-
/* redirect map is Record<statusKey, true> — copy as-is, no schema conversion */
|
|
1349
|
-
result[contentType] = { ...(schemas as Record<string, true>) }
|
|
1350
|
-
continue
|
|
1351
|
-
}
|
|
1352
|
-
const converted: Record<string, unknown> = {}
|
|
1353
|
-
for (const [statusKey, schema] of Object.entries(schemas)) {
|
|
1354
|
-
if (schema === undefined) continue
|
|
1355
|
-
converted[statusKey] = schemaToJsonSchema(schema as StandardSchemaLike)
|
|
1356
|
-
}
|
|
1357
|
-
if (Object.keys(converted).length > 0) {
|
|
1358
|
-
result[contentType] = converted
|
|
1359
|
-
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Reject meta a generated file cannot reproduce. A route tree is JSON plus topology: a value
|
|
1531
|
+
* JSON cannot round-trip (`Date`, `RegExp`, `NaN`, `BigInt`, class instances) would be
|
|
1532
|
+
* emitted as something else, or interned together with a different value.
|
|
1533
|
+
*/
|
|
1534
|
+
function assertJsonValue(value: unknown, where: string, path: string): void {
|
|
1535
|
+
const fail = (what: string): never => {
|
|
1536
|
+
throw new Error(`${where}: ${path} is ${what} — route trees carry JSON values only`)
|
|
1360
1537
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
return {
|
|
1366
|
-
boundaryErrorKey: handler.bek,
|
|
1367
|
-
errorKeys: Array.from(handler.ek),
|
|
1368
|
-
inputSchemas: serializeInputSchemas(handler.iv as InputSchemasDef | null),
|
|
1369
|
-
meta: handler.mt,
|
|
1370
|
-
method,
|
|
1371
|
-
middlewareNames: handler.mw.map((mw) => mw.name || "anonymous"),
|
|
1372
|
-
outputSchemas: serializeOutputSchemas(handler.os as OutputSchemaDef | null),
|
|
1373
|
-
path,
|
|
1538
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return
|
|
1539
|
+
if (typeof value === "number") {
|
|
1540
|
+
if (!Number.isFinite(value)) fail(String(value))
|
|
1541
|
+
return
|
|
1374
1542
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
routes.push(buildRouteConfig(handler, method, currentPath || "/"))
|
|
1381
|
-
}
|
|
1543
|
+
if (value === undefined) return
|
|
1544
|
+
if (typeof value !== "object") fail(`a ${typeof value}`)
|
|
1545
|
+
if (Array.isArray(value)) {
|
|
1546
|
+
for (let k = 0; k < value.length; k++) assertJsonValue(value[k], where, `${path}[${k}]`)
|
|
1547
|
+
return
|
|
1382
1548
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1549
|
+
const proto = Object.getPrototypeOf(value) as unknown
|
|
1550
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
1551
|
+
const name = (value as { constructor?: { name?: string } }).constructor?.name ?? "an object"
|
|
1552
|
+
fail(`a ${name}`)
|
|
1386
1553
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
collectRoutesForTree(node.d.c, `${currentPath}/:${node.d.n}`, routes)
|
|
1554
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
1555
|
+
assertJsonValue((value as Record<string, unknown>)[key], where, `${path}.${key}`)
|
|
1390
1556
|
}
|
|
1557
|
+
}
|
|
1391
1558
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1559
|
+
/** Object-literal key that defines an own property even for `__proto__`. */
|
|
1560
|
+
function keyExpr(key: string): string {
|
|
1561
|
+
return key === "__proto__" ? '["__proto__"]' : JSON.stringify(key)
|
|
1397
1562
|
}
|
|
1398
1563
|
|
|
1399
|
-
type
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
name: string
|
|
1406
|
-
} | null
|
|
1564
|
+
type TreeNodeLike = {
|
|
1565
|
+
d: { c: TreeNodeLike; n: string } | null
|
|
1566
|
+
m: Record<string, string> | null
|
|
1567
|
+
s: Record<string, TreeNodeLike>
|
|
1568
|
+
w: { m: Record<string, string>; n: string } | null
|
|
1569
|
+
ws: string | null
|
|
1407
1570
|
}
|
|
1408
1571
|
|
|
1409
|
-
function
|
|
1410
|
-
|
|
1572
|
+
function methodMapExpr(m: Record<string, string>, intern: InternPool): string {
|
|
1573
|
+
const parts = Object.keys(m).map((method) => `${keyExpr(method)}: ${intern.expr(m[method])}`)
|
|
1574
|
+
return `S({ ${parts.join(", ")} })`
|
|
1411
1575
|
}
|
|
1412
1576
|
|
|
1413
|
-
function serializeNode(node:
|
|
1414
|
-
const
|
|
1577
|
+
function serializeNode(node: TreeNodeLike, intern: InternPool): string {
|
|
1578
|
+
const statics = Object.keys(node.s)
|
|
1415
1579
|
const sExpr =
|
|
1416
|
-
|
|
1417
|
-
? `S({ ${
|
|
1418
|
-
: "
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
: "null"
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
const mExpr =
|
|
1429
|
-
node.handlers.length > 0 ? `{ ${node.handlers.map((h) => `${h.method}: ${h.handler}`).join(", ")} }` : "null"
|
|
1430
|
-
|
|
1431
|
-
const args = [sExpr]
|
|
1432
|
-
if (mExpr !== "null" || dExpr !== "null" || wExpr !== "null") {
|
|
1433
|
-
args.push(mExpr)
|
|
1434
|
-
if (dExpr !== "null" || wExpr !== "null") {
|
|
1435
|
-
args.push(dExpr)
|
|
1436
|
-
if (wExpr !== "null") args.push(wExpr)
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1580
|
+
statics.length > 0
|
|
1581
|
+
? `S({ ${statics.map((k) => `${keyExpr(k)}: ${serializeNode(node.s[k], intern)}`).join(", ")} })`
|
|
1582
|
+
: "undefined"
|
|
1583
|
+
const args = [
|
|
1584
|
+
sExpr,
|
|
1585
|
+
node.m !== null ? methodMapExpr(node.m, intern) : "null",
|
|
1586
|
+
node.d !== null ? `{ n: ${JSON.stringify(node.d.n)}, c: ${serializeNode(node.d.c, intern)} }` : "null",
|
|
1587
|
+
node.w !== null ? `{ n: ${JSON.stringify(node.w.n)}, m: ${methodMapExpr(node.w.m, intern)} }` : "null",
|
|
1588
|
+
node.ws !== null ? intern.expr(node.ws) : "null",
|
|
1589
|
+
]
|
|
1590
|
+
while (args.length > 0 && (args[args.length - 1] === "null" || args[args.length - 1] === "undefined")) args.pop()
|
|
1439
1591
|
return `N(${args.join(", ")})`
|
|
1440
1592
|
}
|
|
1441
1593
|
|
|
1442
|
-
function
|
|
1443
|
-
if (Object.keys(node.children).length > 0) return true
|
|
1444
|
-
if (node.dynamic !== null) return nodeHasStatic(node.dynamic.child)
|
|
1445
|
-
return false
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
function selectorOf(route: RouteConfig): string {
|
|
1449
|
-
return `${route.method} ${route.path}`
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
function mapKeyExpr(intern: InternPool, selector: string): string {
|
|
1453
|
-
const id = intern.id(selector)
|
|
1454
|
-
return id !== undefined ? `[${id}]` : JSON.stringify(selector)
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
function buildInternPool(routes: RouteConfig[]): InternPool {
|
|
1458
|
-
const intern = new InternPool()
|
|
1459
|
-
for (const route of routes) {
|
|
1460
|
-
intern.count(route.boundaryErrorKey)
|
|
1461
|
-
intern.count(route.errorKeys)
|
|
1462
|
-
intern.count(route.meta)
|
|
1463
|
-
intern.count(selectorOf(route))
|
|
1464
|
-
}
|
|
1465
|
-
for (const route of routes) {
|
|
1466
|
-
intern.force(route.meta, "M")
|
|
1467
|
-
intern.force(selectorOf(route), "P")
|
|
1468
|
-
}
|
|
1469
|
-
intern.seal()
|
|
1470
|
-
return intern
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
function insertHandler(rootBuild: TreeBuild, route: RouteConfig, hName: string): void {
|
|
1474
|
-
const segments = route.path.split("/").filter((s) => s.length > 0)
|
|
1475
|
-
let node = rootBuild
|
|
1476
|
-
|
|
1477
|
-
for (let i = 0; i < segments.length; i++) {
|
|
1478
|
-
const seg = segments[i]
|
|
1479
|
-
if (seg.startsWith("*")) {
|
|
1480
|
-
const name = seg.length > 1 ? seg.slice(1) : "*"
|
|
1481
|
-
if (node.wildcard === null) {
|
|
1482
|
-
node.wildcard = { handlers: [], name }
|
|
1483
|
-
}
|
|
1484
|
-
node.wildcard.handlers.push({ handler: hName, method: route.method })
|
|
1485
|
-
return
|
|
1486
|
-
}
|
|
1487
|
-
if (seg.startsWith(":")) {
|
|
1488
|
-
const name = seg.endsWith("?") ? seg.slice(1, -1) : seg.slice(1)
|
|
1489
|
-
if (node.dynamic === null) {
|
|
1490
|
-
node.dynamic = { child: createTreeBuild(), name }
|
|
1491
|
-
}
|
|
1492
|
-
if (i === segments.length - 1) {
|
|
1493
|
-
node.dynamic.child.handlers.push({
|
|
1494
|
-
handler: hName,
|
|
1495
|
-
method: route.method,
|
|
1496
|
-
})
|
|
1497
|
-
} else {
|
|
1498
|
-
node = node.dynamic.child
|
|
1499
|
-
}
|
|
1500
|
-
continue
|
|
1501
|
-
}
|
|
1502
|
-
if (node.children[seg] === undefined) {
|
|
1503
|
-
node.children[seg] = createTreeBuild()
|
|
1504
|
-
}
|
|
1505
|
-
if (i === segments.length - 1) {
|
|
1506
|
-
node.children[seg].handlers.push({
|
|
1507
|
-
handler: hName,
|
|
1508
|
-
method: route.method,
|
|
1509
|
-
})
|
|
1510
|
-
} else {
|
|
1511
|
-
node = node.children[seg]
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
if (segments.length === 0) {
|
|
1516
|
-
node.handlers.push({ handler: hName, method: route.method })
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
function collectMetaShape(collected: CollectedRoute[]): {
|
|
1594
|
+
function collectMetaShape(entries: Array<{ mt: Record<string, unknown> | null | undefined }>): {
|
|
1521
1595
|
allKeys: Map<string, Set<string>>
|
|
1522
1596
|
keyCount: Map<string, number>
|
|
1523
1597
|
metaHandlerCount: number
|
|
@@ -1525,10 +1599,10 @@ function collectMetaShape(collected: CollectedRoute[]): {
|
|
|
1525
1599
|
const allKeys = new Map<string, Set<string>>()
|
|
1526
1600
|
const keyCount = new Map<string, number>()
|
|
1527
1601
|
let metaHandlerCount = 0
|
|
1528
|
-
for (const {
|
|
1529
|
-
if (
|
|
1602
|
+
for (const { mt } of entries) {
|
|
1603
|
+
if (mt === null || mt === undefined) continue
|
|
1530
1604
|
metaHandlerCount++
|
|
1531
|
-
for (const [k, v] of Object.entries(
|
|
1605
|
+
for (const [k, v] of Object.entries(mt)) {
|
|
1532
1606
|
if (!allKeys.has(k)) allKeys.set(k, new Set())
|
|
1533
1607
|
allKeys.get(k)?.add(emitLiteral(v))
|
|
1534
1608
|
keyCount.set(k, (keyCount.get(k) ?? 0) + 1)
|
|
@@ -1540,137 +1614,144 @@ function collectMetaShape(collected: CollectedRoute[]): {
|
|
|
1540
1614
|
function emitMetaShapeType(shape: ReturnType<typeof collectMetaShape>): string {
|
|
1541
1615
|
if (shape.allKeys.size === 0) return ""
|
|
1542
1616
|
const props = [...shape.allKeys.entries()]
|
|
1543
|
-
.sort(([a], [b]) => a
|
|
1617
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
1544
1618
|
.map(([key, types]) => {
|
|
1545
1619
|
const optional = (shape.keyCount.get(key) ?? 0) < shape.metaHandlerCount ? "?" : ""
|
|
1546
|
-
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key :
|
|
1620
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key)
|
|
1547
1621
|
return `\t${safeKey}${optional}: ${[...types].join(" | ")}`
|
|
1548
1622
|
})
|
|
1549
1623
|
return `export type MetaShape = {\n${props.join("\n")}\n} & Record<string, unknown>\n`
|
|
1550
1624
|
}
|
|
1551
1625
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
lines.push("const E = Object.create(null) as Record<string, TreeNode>")
|
|
1560
|
-
lines.push(
|
|
1561
|
-
'function N(s: Record<string, TreeNode>, m: Record<string, RouteHandler> | null = null, d: TreeNode["d"] = null, w: { n: string; m: Record<string, RouteHandler> } | null = null): TreeNode {',
|
|
1562
|
-
)
|
|
1563
|
-
lines.push('\treturn { s, d, w: w as TreeNode["w"], m: m as TreeNode["m"], ws: null }')
|
|
1564
|
-
lines.push("}")
|
|
1626
|
+
type EmitEntry = {
|
|
1627
|
+
bek: string | null
|
|
1628
|
+
ek: string[]
|
|
1629
|
+
id: string
|
|
1630
|
+
internalMeta: boolean
|
|
1631
|
+
mt: Record<string, unknown> | null
|
|
1632
|
+
}
|
|
1565
1633
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1634
|
+
/**
|
|
1635
|
+
* Emit a route tree module: topology with `RouteId` leaves plus per-route data (meta, error
|
|
1636
|
+
* keys, boundary key). No handler fields — the app that loads the tree registers its routes,
|
|
1637
|
+
* which bind to these ids; leaves it does not register are delegated to its catch-all.
|
|
1638
|
+
*/
|
|
1639
|
+
function emitRouteTree(tree: RouteTree): string {
|
|
1640
|
+
const order: string[] = []
|
|
1641
|
+
const seen = new Set<string>()
|
|
1642
|
+
forEachLeaf(tree.root, (_method, _path, id) => {
|
|
1643
|
+
if (!seen.has(id)) {
|
|
1644
|
+
seen.add(id)
|
|
1645
|
+
order.push(id)
|
|
1646
|
+
}
|
|
1647
|
+
})
|
|
1648
|
+
const entries: EmitEntry[] = []
|
|
1649
|
+
for (const id of order) {
|
|
1650
|
+
const entry = tree.routes[id]
|
|
1651
|
+
if (entry === undefined) throw new Error(`route tree leaf ${id} has no route entry`)
|
|
1652
|
+
const mt = entry.mt ?? null
|
|
1653
|
+
if (mt !== null) assertJsonValue(mt, `Route meta for ${id}`, "meta")
|
|
1654
|
+
entries.push({
|
|
1655
|
+
bek: entry.bek ?? null,
|
|
1656
|
+
ek: [...(entry.ek ?? [])],
|
|
1657
|
+
id,
|
|
1658
|
+
internalMeta: (mt as { internal?: unknown } | null)?.internal === true,
|
|
1659
|
+
mt,
|
|
1660
|
+
})
|
|
1569
1661
|
}
|
|
1570
1662
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1663
|
+
const intern = new InternPool()
|
|
1664
|
+
for (const e of entries) {
|
|
1665
|
+
intern.count(e.bek)
|
|
1666
|
+
if (e.ek.length > 0) intern.count(e.ek)
|
|
1667
|
+
intern.count(e.mt)
|
|
1668
|
+
intern.count(e.id)
|
|
1575
1669
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1670
|
+
for (const e of entries) {
|
|
1671
|
+
intern.force(e.mt, "M")
|
|
1672
|
+
intern.force(e.id, "P")
|
|
1579
1673
|
}
|
|
1674
|
+
intern.seal()
|
|
1675
|
+
|
|
1676
|
+
const metaShape = collectMetaShape(entries)
|
|
1677
|
+
const hasMetaShape = metaShape.allKeys.size > 0
|
|
1678
|
+
const lines: string[] = []
|
|
1679
|
+
lines.push('import type { RouteTree, TreeNode } from "@lovrozagar/honey/tree"')
|
|
1680
|
+
lines.push("")
|
|
1681
|
+
lines.push("function S<T>(o: Record<string, T>): Record<string, T> {")
|
|
1682
|
+
lines.push("\treturn Object.assign(Object.create(null) as Record<string, T>, o)")
|
|
1683
|
+
lines.push("}")
|
|
1684
|
+
lines.push(
|
|
1685
|
+
'function N(s: Record<string, TreeNode> = S({}), m: Record<string, string> | null = null, d: TreeNode["d"] = null, w: TreeNode["w"] = null, ws: string | null = null): TreeNode {',
|
|
1686
|
+
)
|
|
1687
|
+
lines.push("\treturn { d, m, s, w, ws }")
|
|
1688
|
+
lines.push("}")
|
|
1580
1689
|
|
|
1581
1690
|
const consts = intern.emitConstLines()
|
|
1582
1691
|
if (consts.length > 0) {
|
|
1583
1692
|
lines.push("")
|
|
1584
1693
|
for (const line of consts) {
|
|
1585
1694
|
if (hasMetaShape && /^const M\d+ = /.test(line)) lines.push(`${line} as unknown as MetaShape`)
|
|
1586
|
-
else lines.push(line)
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
if (routes.length > 0) {
|
|
1591
|
-
lines.push("")
|
|
1592
|
-
/* unique handler per route — sharing H objects would cross-wire .routeTree() patches */
|
|
1593
|
-
for (let i = 0; i < routes.length; i++) {
|
|
1594
|
-
const route = routes[i]
|
|
1595
|
-
const bekExpr = route.boundaryErrorKey !== null ? intern.expr(route.boundaryErrorKey) : "null"
|
|
1596
|
-
const ekSet = route.errorKeys.length > 0 ? `new Set(${intern.expr(route.errorKeys)})` : "new Set()"
|
|
1597
|
-
const mtExpr = route.meta !== null ? intern.expr(route.meta) : "null"
|
|
1598
|
-
lines.push(`const H${i}: RouteHandler = { bek: ${bekExpr}, fn: FN, mw: [], ek: ${ekSet}, mt: ${mtExpr} }`)
|
|
1695
|
+
else lines.push(line)
|
|
1599
1696
|
}
|
|
1600
1697
|
}
|
|
1601
1698
|
|
|
1602
1699
|
lines.push("")
|
|
1603
|
-
lines.push(`export const tree: TreeNode = ${serializeNode(
|
|
1700
|
+
lines.push(`export const tree: TreeNode = ${serializeNode(tree.root as TreeNodeLike, intern)}`)
|
|
1604
1701
|
lines.push("")
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1702
|
+
const routeLines = entries.map((e) => {
|
|
1703
|
+
const parts: string[] = []
|
|
1704
|
+
if (e.bek !== null) parts.push(`bek: ${intern.expr(e.bek)}`)
|
|
1705
|
+
if (e.ek.length > 0) parts.push(`ek: ${intern.expr(e.ek)}`)
|
|
1706
|
+
if (e.mt !== null) parts.push(`mt: ${intern.expr(e.mt)}`)
|
|
1707
|
+
return `\t[${intern.expr(e.id)}]: ${parts.length > 0 ? `{ ${parts.join(", ")} }` : "{}"}`
|
|
1708
|
+
})
|
|
1709
|
+
lines.push(`export const routes: RouteTree["routes"] = S({\n${routeLines.join(",\n")}\n})`)
|
|
1608
1710
|
lines.push("")
|
|
1609
1711
|
|
|
1610
1712
|
let code = lines.join("\n")
|
|
1611
|
-
if (collected === null) return code
|
|
1612
|
-
|
|
1613
|
-
const metaEntries: string[] = []
|
|
1614
|
-
for (const { handler, method, path } of collected) {
|
|
1615
|
-
if (handler.mt === null || Object.keys(handler.mt).length === 0) continue
|
|
1616
|
-
const selector = `${method} ${path}`
|
|
1617
|
-
metaEntries.push(`\t${mapKeyExpr(intern, selector)}: ${intern.expr(handler.mt)}`)
|
|
1618
|
-
}
|
|
1619
1713
|
|
|
1620
|
-
if (hasMetaShape
|
|
1621
|
-
code += `\n${emitMetaShapeType(metaShape)}\n`
|
|
1622
|
-
}
|
|
1714
|
+
if (hasMetaShape) code += `\n${emitMetaShapeType(metaShape)}\n`
|
|
1623
1715
|
|
|
1624
|
-
const selectors =
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
const sorted = [...selectors].sort()
|
|
1631
|
-
const parts = sorted.map((s) => {
|
|
1632
|
-
const id = intern.id(s)
|
|
1633
|
-
return id !== undefined ? `typeof ${id}` : JSON.stringify(s)
|
|
1716
|
+
const selectors = entries.filter((e) => !e.id.startsWith("WS ") && !e.internalMeta).map((e) => e.id)
|
|
1717
|
+
if (selectors.length > 0) {
|
|
1718
|
+
const sorted = [...new Set(selectors)].sort()
|
|
1719
|
+
const parts = sorted.map((sel) => {
|
|
1720
|
+
const ref = intern.id(sel)
|
|
1721
|
+
return ref !== undefined ? `typeof ${ref}` : JSON.stringify(sel)
|
|
1634
1722
|
})
|
|
1635
1723
|
code += `\nexport type RouteSelector = ${parts.join(" | ")}\n`
|
|
1636
1724
|
}
|
|
1637
1725
|
|
|
1726
|
+
const metaEntries = entries
|
|
1727
|
+
.filter((e) => e.mt !== null && Object.keys(e.mt).length > 0)
|
|
1728
|
+
.map((e) => `\t[${intern.expr(e.id)}]: ${intern.expr(e.mt)}`)
|
|
1729
|
+
const metaType = hasMetaShape ? "Record<string, MetaShape>" : "Record<string, Record<string, unknown>>"
|
|
1638
1730
|
if (metaEntries.length > 0) {
|
|
1639
|
-
|
|
1640
|
-
code += `export const meta: ${metaType} = {\n${metaEntries.join(",\n")}\n}\n`
|
|
1731
|
+
code += `export const meta: ${metaType} = S({\n${metaEntries.join(",\n")}\n})\n`
|
|
1641
1732
|
}
|
|
1642
|
-
|
|
1643
|
-
const hasMetaExport = metaEntries.length > 0
|
|
1644
|
-
code += `export const routeTree: RouteTree = { root: tree, ${hasMetaExport ? "meta, " : "meta: {}, "}handlers }\n`
|
|
1733
|
+
code += `export const routeTree: RouteTree = { v: ${ROUTE_TREE_VERSION}, root: tree, routes, ${metaEntries.length > 0 ? "meta" : "meta: {}"} }\n`
|
|
1645
1734
|
|
|
1646
1735
|
return code
|
|
1647
1736
|
}
|
|
1648
1737
|
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
function generateFromTreeRoot(root: TreeNode): string {
|
|
1654
|
-
const routes: RouteConfig[] = []
|
|
1655
|
-
collectRoutesForTree(root, "", routes)
|
|
1656
|
-
const collected: CollectedRoute[] = []
|
|
1657
|
-
walkTree(root, "", collected)
|
|
1658
|
-
return emitRouteTree(routes, collected)
|
|
1738
|
+
/** Emit a route tree module from a tree (a `mergeTree` result or an `app.toRouteTree()` snapshot). */
|
|
1739
|
+
export function generateRouteTree(tree: RouteTree): string {
|
|
1740
|
+
return emitRouteTree(tree)
|
|
1659
1741
|
}
|
|
1660
1742
|
|
|
1661
1743
|
export function generateRouteTreeFromApp<TEnv, TCtx>(
|
|
1662
1744
|
app: Honey<TEnv, TCtx, unknown, unknown, unknown, string, string>,
|
|
1663
1745
|
): string {
|
|
1664
|
-
|
|
1665
|
-
return generateFromTreeRoot(root)
|
|
1746
|
+
return emitRouteTree(app.toRouteTree())
|
|
1666
1747
|
}
|
|
1667
1748
|
|
|
1668
1749
|
/**
|
|
1669
1750
|
* Generate static route tree code from a pre-built RouteTree.
|
|
1670
1751
|
* Supports gateway patterns: import service trees → enrich meta → mergeTree → generate.
|
|
1671
1752
|
*/
|
|
1672
|
-
export function generateRouteTreeFromRouteTree(rt:
|
|
1673
|
-
return
|
|
1753
|
+
export function generateRouteTreeFromRouteTree(rt: RouteTree): string {
|
|
1754
|
+
return emitRouteTree(rt)
|
|
1674
1755
|
}
|
|
1675
1756
|
|
|
1676
1757
|
/* ---- Type codegen ---- */
|
|
@@ -1707,14 +1788,14 @@ function emitOutputType(handler: RouteHandler, state: TypeEmitState): string {
|
|
|
1707
1788
|
if (contentType === "redirect") {
|
|
1708
1789
|
const keys = Object.keys(schemas).filter((k) => schemas[k as keyof typeof schemas])
|
|
1709
1790
|
if (keys.length > 0) {
|
|
1710
|
-
ctEntries.push(`"redirect": { ${keys.map((k) => `${k}: true`).join("; ")} }`)
|
|
1791
|
+
ctEntries.push(`"redirect": { ${keys.map((k) => `${quoteKey(k)}: true`).join("; ")} }`)
|
|
1711
1792
|
}
|
|
1712
1793
|
continue
|
|
1713
1794
|
}
|
|
1714
1795
|
const statusEntries: string[] = []
|
|
1715
1796
|
for (const [statusKey, schema] of Object.entries(schemas)) {
|
|
1716
1797
|
if (schema === undefined) continue
|
|
1717
|
-
statusEntries.push(`${statusKey}: ${emitSchemaType(schema as StandardSchemaLike, state)}`)
|
|
1798
|
+
statusEntries.push(`${quoteKey(statusKey)}: ${emitSchemaType(schema as StandardSchemaLike, state)}`)
|
|
1718
1799
|
}
|
|
1719
1800
|
if (statusEntries.length > 0) {
|
|
1720
1801
|
ctEntries.push(`${JSON.stringify(contentType)}: { ${statusEntries.join("; ")} }`)
|
|
@@ -1728,8 +1809,7 @@ function emitMetaType(handler: RouteHandler): string {
|
|
|
1728
1809
|
if (!handler.mt) return "{}"
|
|
1729
1810
|
const entries: string[] = []
|
|
1730
1811
|
for (const [k, v] of Object.entries(handler.mt)) {
|
|
1731
|
-
|
|
1732
|
-
entries.push(`${key}: ${emitLiteral(v)}`)
|
|
1812
|
+
entries.push(`${quoteKey(k)}: ${emitLiteral(v)}`)
|
|
1733
1813
|
}
|
|
1734
1814
|
if (entries.length === 0) return "{}"
|
|
1735
1815
|
return `{ ${entries.join("; ")} }`
|
|
@@ -1739,7 +1819,9 @@ function emitLiteral(value: unknown): string {
|
|
|
1739
1819
|
if (value === null) return "null"
|
|
1740
1820
|
if (value === undefined) return "undefined"
|
|
1741
1821
|
if (typeof value === "string") return JSON.stringify(value)
|
|
1742
|
-
if (typeof value === "number"
|
|
1822
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "number"
|
|
1823
|
+
if (typeof value === "boolean") return String(value)
|
|
1824
|
+
if (typeof value === "bigint") return `${value}n`
|
|
1743
1825
|
if (Array.isArray(value)) {
|
|
1744
1826
|
if (value.length === 0) return "[]"
|
|
1745
1827
|
return `[${value.map((v) => emitLiteral(v)).join(", ")}]`
|
|
@@ -1772,9 +1854,9 @@ function emitErrorShapes(
|
|
|
1772
1854
|
const entry = meta?.[key]
|
|
1773
1855
|
if (entry?.schema) {
|
|
1774
1856
|
const schemaType = emitSchemaType(entry.schema as StandardSchemaLike, state)
|
|
1775
|
-
entries.push(`${key}: ${schemaType}`)
|
|
1857
|
+
entries.push(`${quoteKey(key)}: ${schemaType}`)
|
|
1776
1858
|
} else {
|
|
1777
|
-
entries.push(`${key}: null`)
|
|
1859
|
+
entries.push(`${quoteKey(key)}: null`)
|
|
1778
1860
|
}
|
|
1779
1861
|
}
|
|
1780
1862
|
return `{ ${entries.join("; ")} }`
|
|
@@ -1824,16 +1906,15 @@ function emitErrorsCtxType(handler: RouteHandler): string {
|
|
|
1824
1906
|
.sort()
|
|
1825
1907
|
.map(
|
|
1826
1908
|
(k) =>
|
|
1827
|
-
`${k}: (opts?: { cause?: unknown; fields?: Record<string, { error_key: string; message: string; path: string }[]>; headers?: Record<string, string>; vars?: Record<string, string | number> }) => HoneyError`,
|
|
1909
|
+
`${quoteKey(k)}: (opts?: { cause?: unknown; fields?: Record<string, { error_key: string; message: string; path: string }[]>; headers?: Record<string, string>; vars?: Record<string, string | number> }) => HoneyError`,
|
|
1828
1910
|
)
|
|
1829
1911
|
return `readonly errors: { ${entries.join("; ")} }`
|
|
1830
1912
|
}
|
|
1831
1913
|
|
|
1832
1914
|
/**
|
|
1833
|
-
*
|
|
1834
|
-
*
|
|
1835
|
-
*
|
|
1836
|
-
* Try exact match first, then strip leading segments until a match is found.
|
|
1915
|
+
* Per-route middleware type additions. The type extractor keys routes by their full path —
|
|
1916
|
+
* basePath included — exactly as the router registers them, so the lookup is exact: a route
|
|
1917
|
+
* with no entry gets the base ctx, never another route's additions.
|
|
1837
1918
|
*/
|
|
1838
1919
|
function resolveRouteMiddleware(
|
|
1839
1920
|
routeMiddleware: Record<string, string> | undefined,
|
|
@@ -1841,27 +1922,15 @@ function resolveRouteMiddleware(
|
|
|
1841
1922
|
fullPath: string,
|
|
1842
1923
|
): string | undefined {
|
|
1843
1924
|
if (!routeMiddleware) return undefined
|
|
1844
|
-
const
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
/* strip leading path segments (basePath) until match */
|
|
1848
|
-
let p = fullPath
|
|
1849
|
-
while (p.includes("/", 1)) {
|
|
1850
|
-
const nextSlash = p.indexOf("/", 1)
|
|
1851
|
-
p = p.slice(nextSlash)
|
|
1852
|
-
const key = `${method} ${p}`
|
|
1853
|
-
if (routeMiddleware[key]) return routeMiddleware[key]
|
|
1854
|
-
}
|
|
1855
|
-
return undefined
|
|
1925
|
+
const key = `${method} ${fullPath}`
|
|
1926
|
+
return Object.hasOwn(routeMiddleware, key) ? routeMiddleware[key] : undefined
|
|
1856
1927
|
}
|
|
1857
1928
|
|
|
1858
1929
|
export function generateTypes<TEnv, TCtx>(
|
|
1859
1930
|
app: Honey<TEnv, TCtx, unknown, unknown, unknown, string, string>,
|
|
1860
1931
|
options: GenerateTypesOptions,
|
|
1861
1932
|
): string {
|
|
1862
|
-
const
|
|
1863
|
-
const collected: CollectedRoute[] = []
|
|
1864
|
-
walkTree(tree, "", collected, true)
|
|
1933
|
+
const collected: CollectedRoute[] = collectRoutes(app, true)
|
|
1865
1934
|
|
|
1866
1935
|
const factory = getErrorFactory(app)
|
|
1867
1936
|
const errorMeta = getErrorMeta(factory)
|
|
@@ -1906,7 +1975,8 @@ export function generateTypes<TEnv, TCtx>(
|
|
|
1906
1975
|
const sorted = [...selectors].sort()
|
|
1907
1976
|
lines.push(`export type RouteSelector = ${sorted.map((s) => JSON.stringify(s)).join(" | ")}`)
|
|
1908
1977
|
lines.push("")
|
|
1909
|
-
|
|
1978
|
+
/* augment the module consumers import — an augmentation of any other name silently applies to nothing */
|
|
1979
|
+
lines.push('declare module "@lovrozagar/honey" {')
|
|
1910
1980
|
lines.push("\tinterface HoneyCodegen {")
|
|
1911
1981
|
lines.push(`\t\trouteSelector: RouteSelector`)
|
|
1912
1982
|
lines.push("\t}")
|
|
@@ -1946,7 +2016,7 @@ export function generateTypes<TEnv, TCtx>(
|
|
|
1946
2016
|
|
|
1947
2017
|
function buildMwType(method: string, path: string): string | undefined {
|
|
1948
2018
|
const key = `${method} ${path}`
|
|
1949
|
-
const props = routeProps
|
|
2019
|
+
const props = routeProps && Object.hasOwn(routeProps, key) ? routeProps[key] : undefined
|
|
1950
2020
|
if (props?.length) {
|
|
1951
2021
|
const entries = props.map((p) => {
|
|
1952
2022
|
const typeStr = subTypeMap.get(p.type) ?? p.type
|
|
@@ -1983,7 +2053,7 @@ export function generateTypes<TEnv, TCtx>(
|
|
|
1983
2053
|
routesLines.push("export type Routes = {")
|
|
1984
2054
|
|
|
1985
2055
|
for (const [path, methods] of byPath) {
|
|
1986
|
-
routesLines.push(`\t
|
|
2056
|
+
routesLines.push(`\t${JSON.stringify(path)}: {`)
|
|
1987
2057
|
for (const { handler, method } of methods) {
|
|
1988
2058
|
const mwType = buildMwType(method, path)
|
|
1989
2059
|
const mwAlias = mwType ? mwTypeMap.get(mwType) : undefined
|
|
@@ -1991,9 +2061,10 @@ export function generateTypes<TEnv, TCtx>(
|
|
|
1991
2061
|
const additions: string[] = []
|
|
1992
2062
|
const inputType = emitInputType(handler, emitState)
|
|
1993
2063
|
if (inputType !== "{}") additions.push(`input: ${inputType}`)
|
|
1994
|
-
|
|
2064
|
+
/* the names ctx.params carries at runtime — an unnamed wildcard is "*" */
|
|
2065
|
+
const params = patternParams(parsePattern(path))
|
|
1995
2066
|
if (params.length > 0) {
|
|
1996
|
-
const paramEntries = params.map((p) => `${p}: string`).join("; ")
|
|
2067
|
+
const paramEntries = params.map((p) => `${quoteKey(p)}: string`).join("; ")
|
|
1997
2068
|
additions.push(`readonly params: { ${paramEntries} }`)
|
|
1998
2069
|
}
|
|
1999
2070
|
const errorsCtx = emitErrorsCtxType(handler)
|
|
@@ -2002,13 +2073,16 @@ export function generateTypes<TEnv, TCtx>(
|
|
|
2002
2073
|
const errorType = emitErrorType(handler)
|
|
2003
2074
|
const metaType = emitMetaType(handler)
|
|
2004
2075
|
const outputType = emitOutputType(handler, emitState)
|
|
2005
|
-
routesLines.push(`\t\t${method}: {`)
|
|
2076
|
+
routesLines.push(`\t\t${quoteKey(method)}: {`)
|
|
2006
2077
|
routesLines.push(`\t\t\tctx: WithOutput<${ctxType}, ${outputType}>`)
|
|
2007
2078
|
routesLines.push(`\t\t\terrors: ${errorType}`)
|
|
2008
|
-
|
|
2079
|
+
/* a mounted sub-app's route keeps its own error factory */
|
|
2080
|
+
const routeFactory = (handler.fac as Record<string, () => HoneyError> | null | undefined) ?? factory
|
|
2081
|
+
const routeErrorMeta = routeFactory === factory ? errorMeta : getErrorMeta(routeFactory)
|
|
2082
|
+
const shapesType = emitErrorShapes(handler, routeErrorMeta, emitState)
|
|
2009
2083
|
if (shapesType) {
|
|
2010
2084
|
routesLines.push(`\t\t\terrorShapes: ${shapesType}`)
|
|
2011
|
-
const byStatusType = emitErrorsByStatus(handler,
|
|
2085
|
+
const byStatusType = emitErrorsByStatus(handler, routeErrorMeta, routeFactory, emitState)
|
|
2012
2086
|
if (byStatusType) {
|
|
2013
2087
|
routesLines.push(`\t\t\terrorsByStatus: ${byStatusType}`)
|
|
2014
2088
|
}
|
|
@@ -2194,6 +2268,8 @@ type ServiceEntry = {
|
|
|
2194
2268
|
realtime?: boolean
|
|
2195
2269
|
ws?: boolean
|
|
2196
2270
|
sse?: boolean
|
|
2271
|
+
/** The path param that is a wildcard: its value may span segments */
|
|
2272
|
+
wildcard?: string
|
|
2197
2273
|
}
|
|
2198
2274
|
|
|
2199
2275
|
type NestedServiceNode =
|
|
@@ -2214,6 +2290,8 @@ type GeneratedSDK = {
|
|
|
2214
2290
|
type SDKMethod = {
|
|
2215
2291
|
action: string
|
|
2216
2292
|
errorsByStatusType: string | null
|
|
2293
|
+
/** Full operationId (explicit or derived) */
|
|
2294
|
+
id: string
|
|
2217
2295
|
inputHasMandatory: boolean
|
|
2218
2296
|
inputType: string
|
|
2219
2297
|
realtime: boolean
|
|
@@ -2225,7 +2303,8 @@ type SDKMethod = {
|
|
|
2225
2303
|
|
|
2226
2304
|
export function extractOpenApiPathParams(path: string): string[] {
|
|
2227
2305
|
const params: string[] = []
|
|
2228
|
-
|
|
2306
|
+
/* any name between braces — `{user-id}` is one parameter, not `{user}` and a literal `-id` */
|
|
2307
|
+
const re = /\{([^{}/]+)\}/g
|
|
2229
2308
|
let match: RegExpExecArray | null = re.exec(path)
|
|
2230
2309
|
while (match !== null) {
|
|
2231
2310
|
params.push(match[1])
|
|
@@ -2253,87 +2332,76 @@ export function jsonSchemaToTS(schema: Record<string, unknown> | undefined, dept
|
|
|
2253
2332
|
/* a file part of a multipart body is sent as a Blob/File; the runtime appends it to FormData as is */
|
|
2254
2333
|
const FORM_BINARY_TS = "Blob"
|
|
2255
2334
|
|
|
2335
|
+
/* the TS SDK input field a request body fills, by its media type */
|
|
2336
|
+
function sdkBodyField(body: IRBody): "body" | "form" | "json" | null {
|
|
2337
|
+
if (body.kind === "stream") return "body"
|
|
2338
|
+
if (body.kind === "multipart") return "form"
|
|
2339
|
+
const essence = mediaEssence(body.contentType)
|
|
2340
|
+
if (isJsonMedia(body.contentType)) return "json"
|
|
2341
|
+
if (essence === "multipart/form-data" || essence === "application/x-www-form-urlencoded") return "form"
|
|
2342
|
+
return null
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2256
2345
|
/* extract input type for an operation */
|
|
2257
|
-
function emitSDKInputType(op:
|
|
2346
|
+
function emitSDKInputType(op: IROperation): { hasMandatory: boolean; type: string } {
|
|
2258
2347
|
const mandatoryParts: string[] = []
|
|
2259
2348
|
const optionalParts: string[] = []
|
|
2260
2349
|
|
|
2261
2350
|
/* path params — always required */
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
const allParameters = op.parameters as Array<Record<string, unknown>> | undefined
|
|
2265
|
-
const entries = pathParamNames
|
|
2266
|
-
.map((name) => {
|
|
2267
|
-
const paramDef = allParameters?.find((p) => p.name === name && p.in === "path")
|
|
2268
|
-
const schema = paramDef?.schema as Record<string, unknown> | undefined
|
|
2269
|
-
const tsType = schema ? jsonSchemaToTS(schema) : "string"
|
|
2270
|
-
const safeParamName = sdkSafeName(name)
|
|
2271
|
-
return `${safeParamName}: ${tsType}`
|
|
2272
|
-
})
|
|
2273
|
-
.join("; ")
|
|
2351
|
+
if (op.params.path.length > 0) {
|
|
2352
|
+
const entries = op.params.path.map((p) => `${sdkSafeName(p.name)}: ${irToTs(p.schema)}`).join("; ")
|
|
2274
2353
|
mandatoryParts.push(`params: { ${entries} }`)
|
|
2275
2354
|
}
|
|
2276
2355
|
|
|
2277
2356
|
/* query params */
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
const name = String(p.name)
|
|
2287
|
-
const required = p.required === true
|
|
2288
|
-
const schema = p.schema as Record<string, unknown> | undefined
|
|
2289
|
-
const tsType = jsonSchemaToTS(schema)
|
|
2290
|
-
const safeQueryName = sdkSafeName(name)
|
|
2291
|
-
return `${safeQueryName}${required ? "" : "?"}: ${tsType}`
|
|
2292
|
-
})
|
|
2293
|
-
const searchEntry = `search: { ${entries.join("; ")} }`
|
|
2294
|
-
if (hasRequired) {
|
|
2295
|
-
mandatoryParts.push(searchEntry)
|
|
2296
|
-
} else {
|
|
2297
|
-
optionalParts.push(searchEntry)
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2357
|
+
if (op.params.query.length > 0) {
|
|
2358
|
+
const entries = op.params.query
|
|
2359
|
+
.slice()
|
|
2360
|
+
.sort((a, b) => compareCodeUnits(a.name, b.name))
|
|
2361
|
+
.map((p) => `${sdkSafeName(p.name)}${p.required === true ? "" : "?"}: ${irToTs(p.schema)}`)
|
|
2362
|
+
const searchEntry = `search: { ${entries.join("; ")} }`
|
|
2363
|
+
if (op.params.query.some((p) => p.required === true)) mandatoryParts.push(searchEntry)
|
|
2364
|
+
else optionalParts.push(searchEntry)
|
|
2300
2365
|
}
|
|
2301
2366
|
|
|
2302
|
-
/*
|
|
2303
|
-
const
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
if (
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2367
|
+
/* header and cookie params — typed on top of the free-form per-call `headers` / `cookies` */
|
|
2368
|
+
for (const [declared, field] of [
|
|
2369
|
+
[op.params.header, "headers"],
|
|
2370
|
+
[op.params.cookie ?? [], "cookies"],
|
|
2371
|
+
] as const) {
|
|
2372
|
+
if (declared.length === 0) continue
|
|
2373
|
+
const entries = declared
|
|
2374
|
+
.slice()
|
|
2375
|
+
.sort((a, b) => compareCodeUnits(a.name, b.name))
|
|
2376
|
+
.map((p) => {
|
|
2377
|
+
/* sent as text: a header or cookie value is a string on the wire */
|
|
2378
|
+
const tsType = p.schema.kind === "scalar" && p.schema.enum ? irToTs(p.schema) : "string"
|
|
2379
|
+
return `${quoteKey(p.name)}${p.required === true ? "" : "?"}: ${tsType}`
|
|
2380
|
+
})
|
|
2381
|
+
const entry = `${field}: { ${entries.join("; ")} }`
|
|
2382
|
+
if (declared.some((p) => p.required === true)) mandatoryParts.push(entry)
|
|
2383
|
+
else optionalParts.push(entry)
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/* request body: one input field per kind; the preferred content type of each kind wins */
|
|
2387
|
+
const byField = new Map<string, IRBody>()
|
|
2388
|
+
for (const body of bodiesOf(op)) {
|
|
2389
|
+
const field = sdkBodyField(body)
|
|
2390
|
+
if (field && !byField.has(field)) byField.set(field, body)
|
|
2391
|
+
}
|
|
2392
|
+
/* raw bytes first, then json, then form: the order the fields always had */
|
|
2393
|
+
for (const field of ["body", "json", "form"] as const) {
|
|
2394
|
+
const body = byField.get(field)
|
|
2395
|
+
if (!body) continue
|
|
2396
|
+
let entry: string
|
|
2397
|
+
if (field === "body") entry = "body: ReadableStream<Uint8Array> | Blob | ArrayBuffer | Uint8Array"
|
|
2398
|
+
else if (field === "json") entry = `json: ${body.kind === "raw" ? irToTs(body.schema) : "unknown"}`
|
|
2399
|
+
else {
|
|
2400
|
+
const schema = body.kind === "raw" ? body.schema : body.kind === "multipart" ? body.schema : undefined
|
|
2401
|
+
entry = `form: ${schema ? irToTs(schema, 0, FORM_BINARY_TS) : "Record<string, unknown>"}`
|
|
2336
2402
|
}
|
|
2403
|
+
if (body.required) mandatoryParts.push(entry)
|
|
2404
|
+
else optionalParts.push(entry)
|
|
2337
2405
|
}
|
|
2338
2406
|
|
|
2339
2407
|
const allParts = [...mandatoryParts, ...optionalParts]
|
|
@@ -2342,49 +2410,31 @@ function emitSDKInputType(op: Record<string, unknown>, path: string): { hasManda
|
|
|
2342
2410
|
}
|
|
2343
2411
|
|
|
2344
2412
|
/* extract response type for an operation */
|
|
2345
|
-
function emitSDKResponseType(op:
|
|
2346
|
-
|
|
2347
|
-
if (
|
|
2348
|
-
|
|
2349
|
-
/* check for SSE — shape mirrors runtime _SSEEvent (event/id/retry all optional) */
|
|
2350
|
-
for (const response of Object.values(responses)) {
|
|
2351
|
-
const content = response.content as Record<string, unknown> | undefined
|
|
2352
|
-
if (content && "text/event-stream" in content)
|
|
2353
|
-
return "AsyncIterable<{ data: string; event?: string; id?: string; retry?: number }>"
|
|
2354
|
-
}
|
|
2413
|
+
function emitSDKResponseType(op: IROperation): string {
|
|
2414
|
+
/* SSE — shape mirrors runtime _SSEEvent (event/id/retry all optional) */
|
|
2415
|
+
if (op.extensions.sse) return "AsyncIterable<{ data: string; event?: string; id?: string; retry?: number }>"
|
|
2355
2416
|
|
|
2356
|
-
/* collect success response types (2xx) */
|
|
2417
|
+
/* collect success response types (2xx) — `default` and `4XX`-style keys are never success */
|
|
2357
2418
|
const successTypes: string[] = []
|
|
2358
|
-
for (const [status, response] of Object.entries(responses)) {
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
if (code === 204) {
|
|
2419
|
+
for (const [status, response] of Object.entries(op.responses)) {
|
|
2420
|
+
if (!/^2(?:[0-9][0-9]|XX)$/i.test(status)) continue
|
|
2421
|
+
const code = /^2XX$/i.test(status) ? 200 : Number(status)
|
|
2422
|
+
if (code === 204 || code === 205 || !response.contentType) {
|
|
2362
2423
|
successTypes.push("null")
|
|
2363
2424
|
continue
|
|
2364
2425
|
}
|
|
2365
|
-
|
|
2366
|
-
if (
|
|
2367
|
-
successTypes.push("
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
if (content["application/json"]?.schema) {
|
|
2371
|
-
successTypes.push(jsonSchemaToTS(content["application/json"].schema as Record<string, unknown>))
|
|
2372
|
-
} else if ("application/octet-stream" in content) {
|
|
2373
|
-
successTypes.push("ArrayBuffer")
|
|
2426
|
+
/* the same classes the runtime's body parser reads: JSON, text, otherwise bytes */
|
|
2427
|
+
if (isJsonMedia(response.contentType)) {
|
|
2428
|
+
successTypes.push(response.schema ? irToTs(response.schema) : "unknown")
|
|
2429
|
+
} else if (isTextMedia(response.contentType)) {
|
|
2430
|
+
successTypes.push("string")
|
|
2374
2431
|
} else {
|
|
2375
|
-
|
|
2376
|
-
const isText = keys.some((k) => k.startsWith("text/") || k === "application/xml")
|
|
2377
|
-
if (isText) {
|
|
2378
|
-
successTypes.push("string")
|
|
2379
|
-
} else if (keys.length > 0) {
|
|
2380
|
-
successTypes.push("ArrayBuffer")
|
|
2381
|
-
}
|
|
2432
|
+
successTypes.push("ArrayBuffer")
|
|
2382
2433
|
}
|
|
2383
2434
|
}
|
|
2384
2435
|
|
|
2385
2436
|
if (successTypes.length === 0) return "void"
|
|
2386
|
-
|
|
2387
|
-
return successTypes.join(" | ")
|
|
2437
|
+
return [...new Set(successTypes)].join(" | ")
|
|
2388
2438
|
}
|
|
2389
2439
|
|
|
2390
2440
|
/* detect whether a JSON schema matches the standard error envelope shape */
|
|
@@ -2424,25 +2474,19 @@ export function isStandardErrEnvelope(schema: Record<string, unknown>): { keys:
|
|
|
2424
2474
|
}
|
|
2425
2475
|
|
|
2426
2476
|
/* extract error types by status code for an operation (non-2xx responses) */
|
|
2427
|
-
function emitSDKErrorsByStatusType(op:
|
|
2428
|
-
const responses = op.responses as Record<string, Record<string, unknown>> | undefined
|
|
2429
|
-
if (!responses) return null
|
|
2430
|
-
|
|
2477
|
+
function emitSDKErrorsByStatusType(op: IROperation, resolve: (schema: IRSchema) => IRSchema): string | null {
|
|
2431
2478
|
const entries: string[] = []
|
|
2432
|
-
for (const [status, response] of Object.entries(responses)) {
|
|
2433
|
-
|
|
2434
|
-
if (
|
|
2435
|
-
|
|
2436
|
-
const
|
|
2437
|
-
const
|
|
2438
|
-
if (!schema) continue
|
|
2439
|
-
|
|
2440
|
-
const envelope = isStandardErrEnvelope(schema)
|
|
2479
|
+
for (const [status, response] of Object.entries(op.responses)) {
|
|
2480
|
+
/* only concrete error statuses: `default` and `4XX` have no single status to key on */
|
|
2481
|
+
if (!/^[45][0-9][0-9]$/.test(status)) continue
|
|
2482
|
+
if (!response.schema || !response.contentType || !isJsonMedia(response.contentType)) continue
|
|
2483
|
+
const code = Number(status)
|
|
2484
|
+
const envelope = irErrorEnvelope(response.schema, resolve)
|
|
2441
2485
|
if (envelope) {
|
|
2442
2486
|
const keyUnion = envelope.keys.map((k) => JSON.stringify(k)).join(" | ")
|
|
2443
2487
|
entries.push(`${code}: _ErrEnvelope<${envelope.status}, ${keyUnion}>`)
|
|
2444
2488
|
} else {
|
|
2445
|
-
entries.push(`${code}: ${
|
|
2489
|
+
entries.push(`${code}: ${irToTs(response.schema)}`)
|
|
2446
2490
|
}
|
|
2447
2491
|
}
|
|
2448
2492
|
|
|
@@ -2502,7 +2546,7 @@ function buildTypeAliases(
|
|
|
2502
2546
|
const aliases = new Map<string, string>()
|
|
2503
2547
|
const lines: string[] = []
|
|
2504
2548
|
let idx = 0
|
|
2505
|
-
for (const [typeStr, count] of [...counts.entries()].sort(([a], [b]) => a
|
|
2549
|
+
for (const [typeStr, count] of [...counts.entries()].sort(([a], [b]) => compareCodeUnits(a, b))) {
|
|
2506
2550
|
if (count >= minCount || typeStr.length > 200) {
|
|
2507
2551
|
const alias = `${prefix}${idx++}`
|
|
2508
2552
|
aliases.set(typeStr, alias)
|
|
@@ -2703,6 +2747,7 @@ function buildSDKTypes(
|
|
|
2703
2747
|
l.push("\tauthHeaderName?: string")
|
|
2704
2748
|
l.push("\tauthHeaderPrefix?: string")
|
|
2705
2749
|
l.push("\tonLog?: (entry: _LogEntry) => void")
|
|
2750
|
+
l.push('\tredirect?: "error" | "follow" | "manual" | "same-origin"')
|
|
2706
2751
|
l.push("\tsortSearchParams?: boolean")
|
|
2707
2752
|
l.push("\tsseMaxBufferChars?: number")
|
|
2708
2753
|
l.push("\tstate?: Record<string, unknown>")
|
|
@@ -2770,13 +2815,14 @@ function emitNestedMapNode(node: NestedServiceNode, indent: string): string[] {
|
|
|
2770
2815
|
const parts = [`method: ${JSON.stringify(entry.method)}`, `path: ${JSON.stringify(entry.path)}`]
|
|
2771
2816
|
if (entry.params) parts.push(`params: ${JSON.stringify(entry.params)}`)
|
|
2772
2817
|
if (entry.sse) parts.push("sse: true")
|
|
2818
|
+
if (entry.wildcard) parts.push(`wildcard: ${JSON.stringify(entry.wildcard)}`)
|
|
2773
2819
|
if (entry.ws) parts.push("ws: true")
|
|
2774
2820
|
if (entry.idempotent) parts.push("idempotent: true")
|
|
2775
2821
|
if (entry.invalidate) parts.push(`invalidate: ${JSON.stringify(entry.invalidate)}`)
|
|
2776
2822
|
return [`{ ${parts.join(", ")} }`]
|
|
2777
2823
|
}
|
|
2778
2824
|
const lines: string[] = ["{"]
|
|
2779
|
-
for (const [key, child] of [...node.children.entries()].sort(([a], [b]) => a
|
|
2825
|
+
for (const [key, child] of [...node.children.entries()].sort(([a], [b]) => compareCodeUnits(a, b))) {
|
|
2780
2826
|
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key)
|
|
2781
2827
|
const childLines = emitNestedMapNode(child, `${indent}\t`)
|
|
2782
2828
|
if (childLines.length === 1) {
|
|
@@ -2795,7 +2841,7 @@ function buildSDKMap(nestedMap: Map<string, NestedServiceNode>): string {
|
|
|
2795
2841
|
const l: string[] = []
|
|
2796
2842
|
|
|
2797
2843
|
l.push("export const serviceMap = {")
|
|
2798
|
-
for (const [key, node] of [...nestedMap.entries()].sort(([a], [b]) => a
|
|
2844
|
+
for (const [key, node] of [...nestedMap.entries()].sort(([a], [b]) => compareCodeUnits(a, b))) {
|
|
2799
2845
|
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key)
|
|
2800
2846
|
const childLines = emitNestedMapNode(node, "\t")
|
|
2801
2847
|
if (childLines.length === 1) {
|
|
@@ -3483,6 +3529,7 @@ type _ServiceEntry = {
|
|
|
3483
3529
|
\tparams?: readonly string[]
|
|
3484
3530
|
\tpath: string
|
|
3485
3531
|
\tsse?: boolean
|
|
3532
|
+
\twildcard?: string
|
|
3486
3533
|
\tws?: boolean
|
|
3487
3534
|
}
|
|
3488
3535
|
|
|
@@ -3511,6 +3558,16 @@ type _RequestMeta = {
|
|
|
3511
3558
|
\tselector: string
|
|
3512
3559
|
\tseqSnapshot: number
|
|
3513
3560
|
}
|
|
3561
|
+
|
|
3562
|
+
type _StaleEntry = { by: string[]; refreshed?: Set<string>; seq: number; until: number }
|
|
3563
|
+
|
|
3564
|
+
/* a path parameter value that would change the request's path once a URL parser normalizes it */
|
|
3565
|
+
class _PathParamError extends Error {
|
|
3566
|
+
\tconstructor(message: string) {
|
|
3567
|
+
\t\tsuper(message)
|
|
3568
|
+
\t\tthis.name = "PathParamError"
|
|
3569
|
+
\t}
|
|
3570
|
+
}
|
|
3514
3571
|
`
|
|
3515
3572
|
}
|
|
3516
3573
|
|
|
@@ -3555,7 +3612,8 @@ export class ${n}<TThrow extends boolean = false> {
|
|
|
3555
3612
|
\t#resourceCache = new Map<string, Record<string, unknown>>()
|
|
3556
3613
|
\t#searchSerializer: (query: Record<string, unknown>) => URLSearchParams
|
|
3557
3614
|
\t#staleTime: number
|
|
3558
|
-
\t#staleUntil: Map<string,
|
|
3615
|
+
\t#staleUntil: Map<string, _StaleEntry> | null
|
|
3616
|
+
\t#stalePatterns = new Map<string, Map<string, _StaleEntry>>()
|
|
3559
3617
|
\t#patternRegexCache = new Map<string, RegExp>()
|
|
3560
3618
|
\t#invalidationSeq = 0
|
|
3561
3619
|
\t#staleMaxEntries: number
|
|
@@ -3564,6 +3622,8 @@ export class ${n}<TThrow extends boolean = false> {
|
|
|
3564
3622
|
\t#sseMaxBufferChars: number
|
|
3565
3623
|
\t#disposeCtrl = new AbortController()
|
|
3566
3624
|
\t#disposed = false
|
|
3625
|
+
\t#refreshing: Promise<string | null> | null = null
|
|
3626
|
+
\t#token: string | null = null
|
|
3567
3627
|
`
|
|
3568
3628
|
}
|
|
3569
3629
|
|
|
@@ -3576,9 +3636,7 @@ function sdkClientConstructor(n: string): string {
|
|
|
3576
3636
|
\t\tthis.#fetchFn = config.fetch ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : globalThis.fetch)
|
|
3577
3637
|
\t\tthis.#searchSerializer = config.buildSearchParams ?? ((q: Record<string, unknown>) => this.#serializeSearch(q))
|
|
3578
3638
|
\t\tthis.#staleTime = config.invalidation?.staleTime ?? 0
|
|
3579
|
-
\t\tthis.#staleUntil = this.#staleTime > 0
|
|
3580
|
-
\t\t\t? new Map<string, { by: string[]; seq: number; until: number }>()
|
|
3581
|
-
\t\t\t: null
|
|
3639
|
+
\t\tthis.#staleUntil = this.#staleTime > 0 ? new Map<string, _StaleEntry>() : null
|
|
3582
3640
|
\t\tthis.#staleMaxEntries = config.invalidation?.staleMaxEntries ?? 1000
|
|
3583
3641
|
\t\tthis.#maxSourcesPerTarget = Math.max(config.invalidation?.maxSourcesPerTarget ?? 16, 1)
|
|
3584
3642
|
\t\tthis.#maxErrorMessageChars = config.maxErrorMessageChars ?? 512
|
|
@@ -3589,41 +3647,45 @@ function sdkClientConstructor(n: string): string {
|
|
|
3589
3647
|
function sdkClientProxy(n: string): string {
|
|
3590
3648
|
return `
|
|
3591
3649
|
\t\tconst self = this
|
|
3650
|
+
\t\tconst callable = (entry: _ServiceEntry, name: string): ((input?: Record<string, unknown>) => unknown) => {
|
|
3651
|
+
\t\t\tconst entryPath = self.#toColonParams(entry.path, entry.wildcard)
|
|
3652
|
+
\t\t\tlet fn: (input?: Record<string, unknown>) => unknown
|
|
3653
|
+
\t\t\tif (entry.ws) {
|
|
3654
|
+
\t\t\t\tfn = (input?: Record<string, unknown>) => self.#connectWS(entry, entryPath, (input ?? {}) as _RequestOptions)
|
|
3655
|
+
\t\t\t} else if (entry.sse) {
|
|
3656
|
+
\t\t\t\tfn = (input?: Record<string, unknown>) => self.#requestSSE(entry, entryPath, (input ?? {}) as _RequestOptions)
|
|
3657
|
+
\t\t\t} else {
|
|
3658
|
+
\t\t\t\tfn = (input?: Record<string, unknown>) => self.#request(entry, input ?? {})
|
|
3659
|
+
\t\t\t}
|
|
3660
|
+
\t\t\tObject.defineProperty(fn, "name", { value: name })
|
|
3661
|
+
\t\t\treturn fn
|
|
3662
|
+
\t\t}
|
|
3663
|
+
\t\tconst isEntry = (node: unknown): node is _ServiceEntry =>
|
|
3664
|
+
\t\t\ttypeof node === "object" && node !== null && Object.hasOwn(node, "method") && typeof (node as Record<string, unknown>)["method"] === "string"
|
|
3665
|
+
\t\t/* only the map's own keys are operations — never toString, constructor or then */
|
|
3666
|
+
\t\tconst childOf = (node: _ServiceMapNode, key: string): _ServiceMapNode | undefined =>
|
|
3667
|
+
\t\t\tObject.hasOwn(node, key) ? (node as Record<string, _ServiceMapNode>)[key] : undefined
|
|
3668
|
+
|
|
3592
3669
|
\t\tfunction makeNodeProxy(node: _ServiceMapNode, path: string[]): object {
|
|
3593
3670
|
\t\t\tconst actionCache = new Map<string, (input?: Record<string, unknown>) => unknown>()
|
|
3594
3671
|
\t\t\tconst childCache = new Map<string, object>()
|
|
3595
3672
|
\t\t\treturn new Proxy({} as Record<string, unknown>, {
|
|
3596
|
-
\t\t\t\tget: (
|
|
3597
|
-
\t\t\t\t\tif (typeof key === "symbol") return
|
|
3598
|
-
\t\t\t\t\tconst child = (node
|
|
3599
|
-
\t\t\t\t\tif (child === undefined) return
|
|
3673
|
+
\t\t\t\tget: (target, key: string | symbol) => {
|
|
3674
|
+
\t\t\t\t\tif (typeof key === "symbol") return Reflect.get(target, key)
|
|
3675
|
+
\t\t\t\t\tconst child = childOf(node, key)
|
|
3676
|
+
\t\t\t\t\tif (child === undefined) return Reflect.get(target, key)
|
|
3600
3677
|
|
|
3601
|
-
\t\t\t\t\
|
|
3602
|
-
\t\t\t\t\tif (typeof (child as Record<string, unknown>)["method"] === "string") {
|
|
3678
|
+
\t\t\t\t\tif (isEntry(child)) {
|
|
3603
3679
|
\t\t\t\t\t\tconst cached = actionCache.get(key)
|
|
3604
3680
|
\t\t\t\t\t\tif (cached) return cached
|
|
3605
|
-
\t\t\t\t\t\tconst
|
|
3606
|
-
\t\t\t\t\t\tconst entryPath = self.#toColonParams(entry.path)
|
|
3607
|
-
\t\t\t\t\t\tlet fn: (input?: Record<string, unknown>) => unknown
|
|
3608
|
-
\t\t\t\t\t\tif (entry.ws) {
|
|
3609
|
-
\t\t\t\t\t\t\tfn = (input?: Record<string, unknown>) =>
|
|
3610
|
-
\t\t\t\t\t\t\t\tself.#connectWS(entry, entryPath, (input ?? {}) as _RequestOptions)
|
|
3611
|
-
\t\t\t\t\t\t} else if (entry.sse) {
|
|
3612
|
-
\t\t\t\t\t\t\tfn = (input?: Record<string, unknown>) =>
|
|
3613
|
-
\t\t\t\t\t\t\t\tself.#requestSSE(entry, entryPath, (input ?? {}) as _RequestOptions)
|
|
3614
|
-
\t\t\t\t\t\t} else {
|
|
3615
|
-
\t\t\t\t\t\t\tfn = (input?: Record<string, unknown>) =>
|
|
3616
|
-
\t\t\t\t\t\t\t\tself.#request(entry, input ?? {})
|
|
3617
|
-
\t\t\t\t\t\t}
|
|
3618
|
-
\t\t\t\t\t\tObject.defineProperty(fn, "name", { value: [...path, key].join(".") })
|
|
3681
|
+
\t\t\t\t\t\tconst fn = callable(child, [...path, key].join("."))
|
|
3619
3682
|
\t\t\t\t\t\tactionCache.set(key, fn)
|
|
3620
3683
|
\t\t\t\t\t\treturn fn
|
|
3621
3684
|
\t\t\t\t\t}
|
|
3622
3685
|
|
|
3623
|
-
\t\t\t\t\t/* namespace: recurse */
|
|
3624
3686
|
\t\t\t\t\tconst cachedChild = childCache.get(key)
|
|
3625
3687
|
\t\t\t\t\tif (cachedChild) return cachedChild
|
|
3626
|
-
\t\t\t\t\tconst childProxy = makeNodeProxy(child
|
|
3688
|
+
\t\t\t\t\tconst childProxy = makeNodeProxy(child, [...path, key])
|
|
3627
3689
|
\t\t\t\t\tchildCache.set(key, childProxy)
|
|
3628
3690
|
\t\t\t\t\treturn childProxy
|
|
3629
3691
|
\t\t\t\t},
|
|
@@ -3632,57 +3694,32 @@ function sdkClientProxy(n: string): string {
|
|
|
3632
3694
|
|
|
3633
3695
|
\t\treturn new Proxy(this, {
|
|
3634
3696
|
\t\t\tget: (target, key: string | symbol) => {
|
|
3635
|
-
\t\t\t\
|
|
3697
|
+
\t\t\t\t/* the client's own members (state, dispose, toString, ...) always win */
|
|
3698
|
+
\t\t\t\tif (typeof key === "symbol" || key in target) return Reflect.get(target, key)
|
|
3636
3699
|
|
|
3637
3700
|
\t\t\t\tconst cached = target.#resourceCache.get(key)
|
|
3638
3701
|
\t\t\t\tif (cached) return cached
|
|
3639
3702
|
|
|
3640
|
-
\t\t\t\tconst node = (serviceMap as _ServiceMap)
|
|
3641
|
-
\t\t\t\tif (node === undefined) return
|
|
3703
|
+
\t\t\t\tconst node = childOf(serviceMap as _ServiceMap, key)
|
|
3704
|
+
\t\t\t\tif (node === undefined) return undefined
|
|
3642
3705
|
|
|
3643
3706
|
\t\t\t\t/* root-level leaf (single-segment operationId) */
|
|
3644
|
-
\t\t\t\tif (
|
|
3645
|
-
\t\t\t\t\tconst
|
|
3646
|
-
\t\t\t\t\tconst entryPath = target.#toColonParams(entry.path)
|
|
3647
|
-
\t\t\t\t\tlet fn: (input?: Record<string, unknown>) => unknown
|
|
3648
|
-
\t\t\t\t\tif (entry.ws) {
|
|
3649
|
-
\t\t\t\t\t\tfn = (input?: Record<string, unknown>) =>
|
|
3650
|
-
\t\t\t\t\t\t\ttarget.#connectWS(entry, entryPath, (input ?? {}) as _RequestOptions)
|
|
3651
|
-
\t\t\t\t\t} else if (entry.sse) {
|
|
3652
|
-
\t\t\t\t\t\tfn = (input?: Record<string, unknown>) =>
|
|
3653
|
-
\t\t\t\t\t\t\ttarget.#requestSSE(entry, entryPath, (input ?? {}) as _RequestOptions)
|
|
3654
|
-
\t\t\t\t\t} else {
|
|
3655
|
-
\t\t\t\t\t\tfn = (input?: Record<string, unknown>) =>
|
|
3656
|
-
\t\t\t\t\t\t\ttarget.#request(entry, input ?? {})
|
|
3657
|
-
\t\t\t\t\t}
|
|
3658
|
-
\t\t\t\t\tObject.defineProperty(fn, "name", { value: key })
|
|
3707
|
+
\t\t\t\tif (isEntry(node)) {
|
|
3708
|
+
\t\t\t\t\tconst fn = callable(node, key)
|
|
3659
3709
|
\t\t\t\t\ttarget.#resourceCache.set(key, fn as unknown as Record<string, unknown>)
|
|
3660
3710
|
\t\t\t\t\treturn fn
|
|
3661
3711
|
\t\t\t\t}
|
|
3662
3712
|
|
|
3663
3713
|
\t\t\t\t/* namespace node — check for single _call promotion */
|
|
3664
|
-
\t\t\t\tconst
|
|
3665
|
-
\t\t\t\tconst
|
|
3666
|
-
\t\t\t\tif (nodeKeys.length === 1 &&
|
|
3667
|
-
\t\t\t\t\tconst
|
|
3668
|
-
\t\t\t\t\tconst entryPath = target.#toColonParams(entry.path)
|
|
3669
|
-
\t\t\t\t\tlet fn: (input?: Record<string, unknown>) => unknown
|
|
3670
|
-
\t\t\t\t\tif (entry.ws) {
|
|
3671
|
-
\t\t\t\t\t\tfn = (input?: Record<string, unknown>) =>
|
|
3672
|
-
\t\t\t\t\t\t\ttarget.#connectWS(entry, entryPath, (input ?? {}) as _RequestOptions)
|
|
3673
|
-
\t\t\t\t\t} else if (entry.sse) {
|
|
3674
|
-
\t\t\t\t\t\tfn = (input?: Record<string, unknown>) =>
|
|
3675
|
-
\t\t\t\t\t\t\ttarget.#requestSSE(entry, entryPath, (input ?? {}) as _RequestOptions)
|
|
3676
|
-
\t\t\t\t\t} else {
|
|
3677
|
-
\t\t\t\t\t\tfn = (input?: Record<string, unknown>) =>
|
|
3678
|
-
\t\t\t\t\t\t\ttarget.#request(entry, input ?? {})
|
|
3679
|
-
\t\t\t\t\t}
|
|
3680
|
-
\t\t\t\t\tObject.defineProperty(fn, "name", { value: key })
|
|
3714
|
+
\t\t\t\tconst nodeKeys = Object.keys(node)
|
|
3715
|
+
\t\t\t\tconst only = childOf(node, "_call")
|
|
3716
|
+
\t\t\t\tif (nodeKeys.length === 1 && only !== undefined && isEntry(only)) {
|
|
3717
|
+
\t\t\t\t\tconst fn = callable(only, key)
|
|
3681
3718
|
\t\t\t\t\ttarget.#resourceCache.set(key, fn as unknown as Record<string, unknown>)
|
|
3682
3719
|
\t\t\t\t\treturn fn
|
|
3683
3720
|
\t\t\t\t}
|
|
3684
3721
|
|
|
3685
|
-
\t\t\t\tconst proxy = makeNodeProxy(node
|
|
3722
|
+
\t\t\t\tconst proxy = makeNodeProxy(node, [key])
|
|
3686
3723
|
\t\t\t\ttarget.#resourceCache.set(key, proxy as Record<string, unknown>)
|
|
3687
3724
|
\t\t\t\treturn proxy
|
|
3688
3725
|
\t\t\t},
|
|
@@ -3693,44 +3730,113 @@ function sdkClientProxy(n: string): string {
|
|
|
3693
3730
|
|
|
3694
3731
|
function sdkClientInterpolatePath(): string {
|
|
3695
3732
|
return `
|
|
3696
|
-
\t
|
|
3697
|
-
\t
|
|
3698
|
-
\t\
|
|
3699
|
-
\t\t\
|
|
3700
|
-
\t\t
|
|
3701
|
-
\t\
|
|
3733
|
+
\t/* Keep in sync with client/path.ts — :name (whole segment), *name (rest), {name} (inside a segment). */
|
|
3734
|
+
\t#encodeSegmentValue(key: string, value: string): string {
|
|
3735
|
+
\t\tif (value === "" || value === "." || value === "..") {
|
|
3736
|
+
\t\t\tthrow new _PathParamError(\`Invalid path param \${JSON.stringify(key)}: \${JSON.stringify(value)} is not a path segment\`)
|
|
3737
|
+
\t\t}
|
|
3738
|
+
\t\treturn encodeURIComponent(value)
|
|
3739
|
+
\t}
|
|
3740
|
+
|
|
3741
|
+
\t#encodeWildcardValue(key: string, value: string): string {
|
|
3742
|
+
\t\tif (value === "") return ""
|
|
3743
|
+
\t\treturn value.split("/").map((part) => this.#encodeSegmentValue(key, part)).join("/")
|
|
3744
|
+
\t}
|
|
3745
|
+
|
|
3746
|
+
\t#paramValue(params: Record<string, string> | undefined, key: string): string | undefined {
|
|
3747
|
+
\t\tif (!params || !Object.hasOwn(params, key)) return undefined
|
|
3748
|
+
\t\tconst raw = params[key] as unknown
|
|
3749
|
+
\t\tif (raw === undefined || raw === null) return undefined
|
|
3750
|
+
\t\treturn typeof raw === "string" ? raw : String(raw)
|
|
3751
|
+
\t}
|
|
3752
|
+
|
|
3753
|
+
\t#interpolatePath(path: string, params: Record<string, string> | undefined, partial = false): string {
|
|
3754
|
+
\t\tconst segments = path.split("/")
|
|
3755
|
+
\t\tconst out: string[] = []
|
|
3756
|
+
\t\tfor (let i = 0; i < segments.length; i++) {
|
|
3757
|
+
\t\t\tconst seg = segments[i] ?? ""
|
|
3758
|
+
\t\t\tif (seg.length > 1 && seg.charCodeAt(0) === 58) {
|
|
3759
|
+
\t\t\t\tconst optional = seg.endsWith("?")
|
|
3760
|
+
\t\t\t\tconst name = optional ? seg.slice(1, -1) : seg.slice(1)
|
|
3761
|
+
\t\t\t\tconst value = this.#paramValue(params, name)
|
|
3762
|
+
\t\t\t\tif (value === undefined) {
|
|
3763
|
+
\t\t\t\t\tif (partial) { out.push(seg); continue }
|
|
3764
|
+
\t\t\t\t\tif (optional) continue
|
|
3765
|
+
\t\t\t\t\tthrow new _PathParamError(\`Missing path param: \${name}\`)
|
|
3766
|
+
\t\t\t\t}
|
|
3767
|
+
\t\t\t\tout.push(this.#encodeSegmentValue(name, value))
|
|
3768
|
+
\t\t\t\tcontinue
|
|
3769
|
+
\t\t\t}
|
|
3770
|
+
\t\t\tif (seg.charCodeAt(0) === 42 && i === segments.length - 1) {
|
|
3771
|
+
\t\t\t\tconst name = seg.length > 1 ? seg.slice(1) : "*"
|
|
3772
|
+
\t\t\t\tconst value = this.#paramValue(params, name)
|
|
3773
|
+
\t\t\t\tif (value === undefined) {
|
|
3774
|
+
\t\t\t\t\tif (partial) { out.push(seg); continue }
|
|
3775
|
+
\t\t\t\t\tif (seg.length === 1) { out.push(""); continue }
|
|
3776
|
+
\t\t\t\t\tthrow new _PathParamError(\`Missing path param: \${name}\`)
|
|
3777
|
+
\t\t\t\t}
|
|
3778
|
+
\t\t\t\tout.push(this.#encodeWildcardValue(name, value))
|
|
3779
|
+
\t\t\t\tcontinue
|
|
3780
|
+
\t\t\t}
|
|
3781
|
+
\t\t\tout.push(
|
|
3782
|
+
\t\t\t\tseg.replace(/\\{([^{}/]+)\\}/g, (match: string, name: string) => {
|
|
3783
|
+
\t\t\t\t\tconst value = this.#paramValue(params, name)
|
|
3784
|
+
\t\t\t\t\tif (value === undefined) {
|
|
3785
|
+
\t\t\t\t\t\tif (partial) return match
|
|
3786
|
+
\t\t\t\t\t\tthrow new _PathParamError(\`Missing path param: \${name}\`)
|
|
3787
|
+
\t\t\t\t\t}
|
|
3788
|
+
\t\t\t\t\treturn this.#encodeSegmentValue(name, value)
|
|
3789
|
+
\t\t\t\t}),
|
|
3790
|
+
\t\t\t)
|
|
3791
|
+
\t\t}
|
|
3792
|
+
\t\treturn out.join("/")
|
|
3793
|
+
\t}
|
|
3794
|
+
|
|
3795
|
+
\t#hasPlaceholder(path: string): boolean {
|
|
3796
|
+
\t\tfor (const seg of path.split("/")) {
|
|
3797
|
+
\t\t\tif (seg.length > 1 && seg.charCodeAt(0) === 58) return true
|
|
3798
|
+
\t\t\tif (seg.charCodeAt(0) === 42) return true
|
|
3799
|
+
\t\t\tif (/\\{[^{}/]+\\}/.test(seg)) return true
|
|
3800
|
+
\t\t}
|
|
3801
|
+
\t\treturn false
|
|
3702
3802
|
\t}
|
|
3703
3803
|
`
|
|
3704
3804
|
}
|
|
3705
3805
|
|
|
3706
3806
|
function sdkClientToColonParams(): string {
|
|
3707
3807
|
return `
|
|
3708
|
-
\t
|
|
3709
|
-
\t
|
|
3808
|
+
\t/* OpenAPI template → route pattern: a whole-segment {name} is :name, the wildcard *name; {name} inside a segment stays */
|
|
3809
|
+
\t#toColonParams(path: string, wildcard?: string): string {
|
|
3810
|
+
\t\treturn path
|
|
3811
|
+
\t\t\t.split("/")
|
|
3812
|
+
\t\t\t.map((seg) => {
|
|
3813
|
+
\t\t\t\tconst m = /^\\{([^{}/]+)\\}$/.exec(seg)
|
|
3814
|
+
\t\t\t\tif (!m) return seg
|
|
3815
|
+
\t\t\t\treturn m[1] === wildcard ? \`*\${m[1]}\` : \`:\${m[1]}\`
|
|
3816
|
+
\t\t\t})
|
|
3817
|
+
\t\t\t.join("/")
|
|
3710
3818
|
\t}
|
|
3711
3819
|
`
|
|
3712
3820
|
}
|
|
3713
3821
|
|
|
3714
3822
|
function sdkClientResolveInvalidationTargets(): string {
|
|
3715
3823
|
return `
|
|
3824
|
+
\t/* params that are present are substituted; a target left partly unresolved stays a narrower pattern */
|
|
3716
3825
|
\t#resolveInvalidationTargets(
|
|
3717
3826
|
\t\ttargets: readonly string[],
|
|
3718
3827
|
\t\tparams: Record<string, string> | undefined,
|
|
3719
3828
|
\t): string[] {
|
|
3720
3829
|
\t\tconst resolved: string[] = []
|
|
3721
3830
|
\t\tfor (const target of targets) {
|
|
3722
|
-
\t\t\tif (!params || !target.includes(":")) { resolved.push(target); continue }
|
|
3723
3831
|
\t\t\tconst spaceIdx = target.indexOf(" ")
|
|
3832
|
+
\t\t\tif (spaceIdx <= 0) continue
|
|
3724
3833
|
\t\t\tconst targetMethod = target.slice(0, spaceIdx)
|
|
3725
3834
|
\t\t\tconst targetPath = target.slice(spaceIdx + 1)
|
|
3726
|
-
\t\t\
|
|
3727
|
-
\t\t\
|
|
3728
|
-
\t\t\t
|
|
3729
|
-
\t\t\t\
|
|
3730
|
-
\t\t\t
|
|
3731
|
-
\t\t\t})
|
|
3732
|
-
\t\t\tif (hasUnresolved) continue
|
|
3733
|
-
\t\t\tresolved.push(\`\${targetMethod} \${replaced}\`)
|
|
3835
|
+
\t\t\ttry {
|
|
3836
|
+
\t\t\t\tresolved.push(\`\${targetMethod} \${this.#interpolatePath(targetPath, params, true)}\`)
|
|
3837
|
+
\t\t\t} catch {
|
|
3838
|
+
\t\t\t\t/* a param value that is not a path segment marks nothing */
|
|
3839
|
+
\t\t\t}
|
|
3734
3840
|
\t\t}
|
|
3735
3841
|
\t\treturn resolved
|
|
3736
3842
|
\t}
|
|
@@ -3742,11 +3848,27 @@ function sdkClientPathMatchesPattern(): string {
|
|
|
3742
3848
|
\t#pathMatchesPattern(concretePath: string, pattern: string): boolean {
|
|
3743
3849
|
\t\tlet re = this.#patternRegexCache.get(pattern)
|
|
3744
3850
|
\t\tif (!re) {
|
|
3745
|
-
\t\t\tconst
|
|
3746
|
-
\t\t\
|
|
3747
|
-
\t\t\
|
|
3748
|
-
\t\t\
|
|
3749
|
-
\t\t\
|
|
3851
|
+
\t\t\tconst esc = (text: string) => text.replace(/[.*+?^\${}()|[\\]\\\\]/g, "\\\\$&")
|
|
3852
|
+
\t\t\tconst segments = pattern.split("/")
|
|
3853
|
+
\t\t\tlet source = ""
|
|
3854
|
+
\t\t\tfor (let i = 0; i < segments.length; i++) {
|
|
3855
|
+
\t\t\t\tconst seg = segments[i] ?? ""
|
|
3856
|
+
\t\t\t\tconst sep = i === 0 ? "" : "/"
|
|
3857
|
+
\t\t\t\tif (seg.length > 1 && seg.charCodeAt(0) === 58) {
|
|
3858
|
+
\t\t\t\t\tsource += seg.endsWith("?") ? \`(?:\${sep}[^/]+)?\` : \`\${sep}[^/]+\`
|
|
3859
|
+
\t\t\t\t\tcontinue
|
|
3860
|
+
\t\t\t\t}
|
|
3861
|
+
\t\t\t\tif (seg.charCodeAt(0) === 42 && i === segments.length - 1) {
|
|
3862
|
+
\t\t\t\t\tsource += \`(?:\${sep}.*)?\`
|
|
3863
|
+
\t\t\t\t\tcontinue
|
|
3864
|
+
\t\t\t\t}
|
|
3865
|
+
\t\t\t\tsource += sep + seg.split(/\\{[^{}/]+\\}/).map(esc).join("[^/]+")
|
|
3866
|
+
\t\t\t}
|
|
3867
|
+
\t\t\tre = new RegExp(\`^\${source}$\`)
|
|
3868
|
+
\t\t\tif (this.#patternRegexCache.size >= 1024) {
|
|
3869
|
+
\t\t\t\tconst oldest = this.#patternRegexCache.keys().next().value
|
|
3870
|
+
\t\t\t\tif (oldest !== undefined) this.#patternRegexCache.delete(oldest)
|
|
3871
|
+
\t\t\t}
|
|
3750
3872
|
\t\t\tthis.#patternRegexCache.set(pattern, re)
|
|
3751
3873
|
\t\t}
|
|
3752
3874
|
\t\treturn re.test(concretePath)
|
|
@@ -3756,35 +3878,31 @@ function sdkClientPathMatchesPattern(): string {
|
|
|
3756
3878
|
|
|
3757
3879
|
function sdkClientLookupStale(): string {
|
|
3758
3880
|
return `
|
|
3881
|
+
\t/* Keep in sync with client/sdk.ts StaleIndex — exact keys and pattern keys are kept apart, so a
|
|
3882
|
+
\t concrete path containing ":" is never read as a pattern, and a lookup scans only its method. */
|
|
3759
3883
|
\t#lookupStale(
|
|
3760
3884
|
\t\tconcreteSelector: string,
|
|
3761
3885
|
\t\tconcretePath: string,
|
|
3762
3886
|
\t\tmethod: string,
|
|
3763
3887
|
\t\tnow: number,
|
|
3764
3888
|
\t): { by: string[]; isStale: boolean } {
|
|
3765
|
-
\t\tconst
|
|
3766
|
-
|
|
3889
|
+
\t\tconst by = new Set<string>()
|
|
3767
3890
|
\t\tconst exact = this.#staleUntil?.get(concreteSelector)
|
|
3768
|
-
\t\tif (exact
|
|
3769
|
-
\t\
|
|
3770
|
-
|
|
3771
|
-
\t\
|
|
3772
|
-
\t\
|
|
3773
|
-
\t\
|
|
3774
|
-
\t\t\
|
|
3775
|
-
\t\t\t\tif (
|
|
3776
|
-
\t\t\t\
|
|
3777
|
-
\t\t\t\
|
|
3778
|
-
\t\t\t\
|
|
3779
|
-
\t\t\t\tif (keyMethod !== method) continue
|
|
3780
|
-
\t\t\t\tif (this.#pathMatchesPattern(concretePath, keyPattern)) {
|
|
3781
|
-
\t\t\t\t\tallBy.push(...entry.by)
|
|
3891
|
+
\t\tif (exact) {
|
|
3892
|
+
\t\t\tif (exact.until > now) for (const m of exact.by) by.add(m)
|
|
3893
|
+
\t\t\telse this.#staleUntil?.delete(concreteSelector)
|
|
3894
|
+
\t\t}
|
|
3895
|
+
\t\tconst table = this.#stalePatterns.get(method)
|
|
3896
|
+
\t\tif (table) {
|
|
3897
|
+
\t\t\tfor (const [key, entry] of table) {
|
|
3898
|
+
\t\t\t\tif (entry.until <= now) { table.delete(key); continue }
|
|
3899
|
+
\t\t\t\tif (entry.refreshed?.has(concretePath)) continue
|
|
3900
|
+
\t\t\t\tif (this.#pathMatchesPattern(concretePath, key.slice(key.indexOf(" ") + 1))) {
|
|
3901
|
+
\t\t\t\t\tfor (const m of entry.by) by.add(m)
|
|
3782
3902
|
\t\t\t\t}
|
|
3783
3903
|
\t\t\t}
|
|
3784
|
-
\t\t\tfor (const key of expired) this.#staleUntil.delete(key)
|
|
3785
3904
|
\t\t}
|
|
3786
|
-
|
|
3787
|
-
\t\treturn { by: [...new Set(allBy)], isStale: allBy.length > 0 }
|
|
3905
|
+
\t\treturn { by: [...by], isStale: by.size > 0 }
|
|
3788
3906
|
\t}
|
|
3789
3907
|
`
|
|
3790
3908
|
}
|
|
@@ -3793,7 +3911,8 @@ function sdkClientCreateTypedWebSocket(): string {
|
|
|
3793
3911
|
return `
|
|
3794
3912
|
\t#createTypedWebSocket(url: string, protocols?: string | string[]): _TypedWebSocket {
|
|
3795
3913
|
\t\tconst ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url)
|
|
3796
|
-
\t\
|
|
3914
|
+
\t\t/* keyed by event and handler: one function may listen to several events */
|
|
3915
|
+
\t\tconst listenerMap = new Map<string, WeakMap<(...args: never[]) => void, EventListener>>()
|
|
3797
3916
|
\t\tconst sendBuffer: Array<ArrayBuffer | ArrayBufferView | string> = []
|
|
3798
3917
|
\t\tlet buffering = true
|
|
3799
3918
|
|
|
@@ -3802,10 +3921,16 @@ function sdkClientCreateTypedWebSocket(): string {
|
|
|
3802
3921
|
\t\t\tfor (const msg of sendBuffer) ws.send(msg as Parameters<WebSocket["send"]>[0])
|
|
3803
3922
|
\t\t\tsendBuffer.length = 0
|
|
3804
3923
|
\t\t})
|
|
3805
|
-
|
|
3806
|
-
\t\
|
|
3924
|
+
\t\t/* a socket that never opens must not hold messages forever */
|
|
3925
|
+
\t\tconst drop = () => {
|
|
3807
3926
|
\t\t\tbuffering = false
|
|
3808
3927
|
\t\t\tsendBuffer.length = 0
|
|
3928
|
+
\t\t}
|
|
3929
|
+
\t\tws.addEventListener("close", drop)
|
|
3930
|
+
\t\tws.addEventListener("error", drop)
|
|
3931
|
+
|
|
3932
|
+
\t\tfunction close(code?: number, reason?: string) {
|
|
3933
|
+
\t\t\tdrop()
|
|
3809
3934
|
\t\t\tws.close(code, reason)
|
|
3810
3935
|
\t\t}
|
|
3811
3936
|
|
|
@@ -3813,7 +3938,7 @@ function sdkClientCreateTypedWebSocket(): string {
|
|
|
3813
3938
|
\t\t\tlet wrapped: EventListener
|
|
3814
3939
|
\t\t\tswitch (event) {
|
|
3815
3940
|
\t\t\t\tcase "message":
|
|
3816
|
-
\t\t\t\t\twrapped = (e: Event) => (handler as (data:
|
|
3941
|
+
\t\t\t\t\twrapped = (e: Event) => (handler as (data: unknown) => void)((e as MessageEvent).data)
|
|
3817
3942
|
\t\t\t\t\tbreak
|
|
3818
3943
|
\t\t\t\tcase "open":
|
|
3819
3944
|
\t\t\t\t\twrapped = () => (handler as () => void)()
|
|
@@ -3827,28 +3952,36 @@ function sdkClientCreateTypedWebSocket(): string {
|
|
|
3827
3952
|
\t\t\t\tdefault:
|
|
3828
3953
|
\t\t\t\t\treturn
|
|
3829
3954
|
\t\t\t}
|
|
3830
|
-
\t\t\
|
|
3955
|
+
\t\t\tlet byHandler = listenerMap.get(event)
|
|
3956
|
+
\t\t\tif (!byHandler) {
|
|
3957
|
+
\t\t\t\tbyHandler = new WeakMap()
|
|
3958
|
+
\t\t\t\tlistenerMap.set(event, byHandler)
|
|
3959
|
+
\t\t\t}
|
|
3960
|
+
\t\t\tbyHandler.set(handler, wrapped)
|
|
3831
3961
|
\t\t\tws.addEventListener(event, wrapped)
|
|
3832
3962
|
\t\t}
|
|
3833
3963
|
|
|
3834
3964
|
\t\tfunction off(event: string, handler: (...args: never[]) => void): void {
|
|
3835
|
-
\t\t\tconst
|
|
3965
|
+
\t\t\tconst byHandler = listenerMap.get(event)
|
|
3966
|
+
\t\t\tconst wrapped = byHandler?.get(handler)
|
|
3836
3967
|
\t\t\tif (wrapped) {
|
|
3837
3968
|
\t\t\t\tws.removeEventListener(event, wrapped)
|
|
3838
|
-
\t\t\t\
|
|
3969
|
+
\t\t\t\tbyHandler?.delete(handler)
|
|
3839
3970
|
\t\t\t}
|
|
3840
3971
|
\t\t}
|
|
3841
3972
|
|
|
3842
3973
|
\t\tfunction send(data: ArrayBuffer | ArrayBufferView | object | string) {
|
|
3843
|
-
\t\t\tlet payload: ArrayBuffer | ArrayBufferView | string
|
|
3974
|
+
\t\t\tlet payload: ArrayBuffer | ArrayBufferView | string | Blob
|
|
3844
3975
|
\t\t\tif (typeof data === "string") {
|
|
3845
3976
|
\t\t\t\tpayload = data
|
|
3846
3977
|
\t\t\t} else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
3847
3978
|
\t\t\t\tpayload = data
|
|
3979
|
+
\t\t\t} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
3980
|
+
\t\t\t\tpayload = data
|
|
3848
3981
|
\t\t\t} else {
|
|
3849
3982
|
\t\t\t\tpayload = JSON.stringify(data)
|
|
3850
3983
|
\t\t\t}
|
|
3851
|
-
\t\t\tif (buffering) { sendBuffer.push(payload) } else { ws.send(payload as Parameters<WebSocket["send"]>[0]) }
|
|
3984
|
+
\t\t\tif (buffering) { sendBuffer.push(payload as ArrayBuffer | ArrayBufferView | string) } else if (ws.readyState === 1) { ws.send(payload as Parameters<WebSocket["send"]>[0]) }
|
|
3852
3985
|
\t\t}
|
|
3853
3986
|
|
|
3854
3987
|
\t\tconst typed: _TypedWebSocket = { close, off, on, get readyState() { return ws.readyState }, send }
|
|
@@ -3906,12 +4039,15 @@ function sdkClientResolveBaseURL(): string {
|
|
|
3906
4039
|
function sdkClientBuildURL(): string {
|
|
3907
4040
|
return `
|
|
3908
4041
|
\t#buildURL(path: string, opts: _RequestOptions): string {
|
|
3909
|
-
\t\
|
|
3910
|
-
\t\tif (opts.params) resolvedPath = this.#interpolatePath(path, opts.params)
|
|
4042
|
+
\t\tconst resolvedPath = this.#interpolatePath(path, opts.params)
|
|
3911
4043
|
\t\tconst baseUrl = this.#resolveBaseURL(this.#config.baseURL)
|
|
3912
4044
|
\t\tconst basePath = baseUrl.pathname.endsWith("/") ? baseUrl.pathname : \`\${baseUrl.pathname}/\`
|
|
3913
4045
|
\t\tconst relative = resolvedPath.startsWith("/") ? resolvedPath.slice(1) : resolvedPath
|
|
3914
4046
|
\t\tconst url = new URL(\`\${basePath}\${relative}\`, baseUrl)
|
|
4047
|
+
\t\t/* a request (and its credentials) never leaves the configured origin */
|
|
4048
|
+
\t\tif (url.origin !== baseUrl.origin) {
|
|
4049
|
+
\t\t\tthrow new _PathParamError(\`Invalid path param: \${JSON.stringify(path)} resolves outside \${baseUrl.origin}\`)
|
|
4050
|
+
\t\t}
|
|
3915
4051
|
\t\tfor (const [k, v] of baseUrl.searchParams.entries()) url.searchParams.append(k, v)
|
|
3916
4052
|
\t\tif (opts.search) {
|
|
3917
4053
|
\t\t\tconst sp = this.#searchSerializer(opts.search)
|
|
@@ -3963,9 +4099,14 @@ function sdkClientBuildHeaders(): string {
|
|
|
3963
4099
|
\t\t\t}
|
|
3964
4100
|
\t\t}
|
|
3965
4101
|
|
|
4102
|
+
\t\t/* a token from onAuthExpired replaces the configured one until the next refresh */
|
|
4103
|
+
\t\tif (this.#token !== null) {
|
|
4104
|
+
\t\t\theaders.set(this.#config.authHeaderName ?? "Authorization", \`\${this.#config.authHeaderPrefix ?? "Bearer "}\${this.#token}\`)
|
|
4105
|
+
\t\t}
|
|
4106
|
+
|
|
3966
4107
|
\t\tif (opts.headers) {
|
|
3967
4108
|
\t\t\tfor (const [k, v] of Object.entries(opts.headers)) {
|
|
3968
|
-
\t\t\t\
|
|
4109
|
+
\t\t\t\tif (v !== undefined) headers.set(k, v)
|
|
3969
4110
|
\t\t\t}
|
|
3970
4111
|
\t\t}
|
|
3971
4112
|
|
|
@@ -3990,24 +4131,16 @@ function sdkClientBuildHeaders(): string {
|
|
|
3990
4131
|
|
|
3991
4132
|
function sdkClientDoRequest(): string {
|
|
3992
4133
|
return `
|
|
3993
|
-
\
|
|
3994
|
-
\t
|
|
3995
|
-
\t\tpath: string,
|
|
3996
|
-
\t\topts: _RequestOptions,
|
|
3997
|
-
\t\tisRetry: boolean,
|
|
3998
|
-
\t\trequestMeta?: _RequestMeta,
|
|
3999
|
-
\t): Promise<{ response: Response }> {
|
|
4000
|
-
\t\tconst url = this.#buildURL(path, opts)
|
|
4001
|
-
\t\tconst headers = await this.#buildHeaders(opts, { method, path })
|
|
4002
|
-
\t\tlet body: BodyInit | undefined
|
|
4003
|
-
|
|
4134
|
+
\t/* the body of a request, and the content-type it needs — shared by plain requests and SSE */
|
|
4135
|
+
\t#buildBody(opts: _RequestOptions, headers: Headers): BodyInit | undefined {
|
|
4004
4136
|
\t\tif (opts.body !== undefined) {
|
|
4005
|
-
\t\t\
|
|
4006
|
-
\t\t\
|
|
4007
|
-
\t\t}
|
|
4137
|
+
\t\t\tif (!headers.has("content-type")) headers.set("content-type", "application/octet-stream")
|
|
4138
|
+
\t\t\treturn opts.body as BodyInit
|
|
4139
|
+
\t\t}
|
|
4140
|
+
\t\tif (opts.json !== undefined) {
|
|
4008
4141
|
\t\t\theaders.set("content-type", "application/json")
|
|
4009
4142
|
\t\t\ttry {
|
|
4010
|
-
\t\t\t\
|
|
4143
|
+
\t\t\t\treturn JSON.stringify(opts.json)
|
|
4011
4144
|
\t\t\t} catch (e) {
|
|
4012
4145
|
\t\t\t\tthrow new _ClientError({
|
|
4013
4146
|
\t\t\t\t\tbody: opts.json,
|
|
@@ -4017,50 +4150,75 @@ function sdkClientDoRequest(): string {
|
|
|
4017
4150
|
\t\t\t\t\tstatus: 0,
|
|
4018
4151
|
\t\t\t\t})
|
|
4019
4152
|
\t\t\t}
|
|
4020
|
-
\t\t}
|
|
4021
|
-
\t\
|
|
4022
|
-
\t\
|
|
4023
|
-
\t\t\t
|
|
4024
|
-
\t\
|
|
4025
|
-
\t\t\t
|
|
4026
|
-
\t\t\t\
|
|
4027
|
-
\t\t\t\t
|
|
4028
|
-
\t\t\t\t
|
|
4029
|
-
\t\t
|
|
4030
|
-
\t\
|
|
4031
|
-
\t\t\
|
|
4032
|
-
\t\t\
|
|
4033
|
-
|
|
4034
|
-
\t\t\
|
|
4035
|
-
\t\t
|
|
4036
|
-
\t\
|
|
4037
|
-
\t\t\
|
|
4038
|
-
\t\t\
|
|
4039
|
-
\t\t\t\
|
|
4040
|
-
\t\t\t\t\
|
|
4041
|
-
\t\t\t\t\
|
|
4042
|
-
\t\t\t\t\t\t\tif (item instanceof File || item instanceof Blob) fd.append(k, item)
|
|
4043
|
-
\t\t\t\t\t\t\telse fd.append(k, String(item))
|
|
4044
|
-
\t\t\t\t\t\t}
|
|
4045
|
-
\t\t\t\t\t} else if (v instanceof File || v instanceof Blob) {
|
|
4046
|
-
\t\t\t\t\t\tfd.append(k, v)
|
|
4047
|
-
\t\t\t\t\t} else {
|
|
4048
|
-
\t\t\t\t\t\tfd.append(k, String(v))
|
|
4049
|
-
\t\t\t\t\t}
|
|
4153
|
+
\t\t}
|
|
4154
|
+
\t\tif (opts.form === undefined) return undefined
|
|
4155
|
+
\t\tconst isFile = (v: unknown) =>
|
|
4156
|
+
\t\t\t(typeof File !== "undefined" && v instanceof File) || (typeof Blob !== "undefined" && v instanceof Blob)
|
|
4157
|
+
\t\tconst hasFiles = Object.values(opts.form).some(
|
|
4158
|
+
\t\t\t(v) =>
|
|
4159
|
+
\t\t\t\tisFile(v) ||
|
|
4160
|
+
\t\t\t\t(typeof FileList !== "undefined" && v instanceof FileList) ||
|
|
4161
|
+
\t\t\t\t(Array.isArray(v) && v.some(isFile)),
|
|
4162
|
+
\t\t)
|
|
4163
|
+
\t\tconst text = (v: unknown): string | null => {
|
|
4164
|
+
\t\t\tif (v === undefined || v === null || typeof v === "symbol") return null
|
|
4165
|
+
\t\t\tif (v instanceof Date) return v.toISOString()
|
|
4166
|
+
\t\t\tif (typeof v === "object") throw new TypeError("form: nested objects cannot be form-encoded; send json instead")
|
|
4167
|
+
\t\t\treturn String(v)
|
|
4168
|
+
\t\t}
|
|
4169
|
+
\t\tif (hasFiles) {
|
|
4170
|
+
\t\t\tconst fd = new FormData()
|
|
4171
|
+
\t\t\tfor (const [k, v] of Object.entries(opts.form)) {
|
|
4172
|
+
\t\t\t\tif (typeof FileList !== "undefined" && v instanceof FileList) {
|
|
4173
|
+
\t\t\t\t\tfor (let i = 0; i < v.length; i++) { const f = v[i]; if (f) fd.append(k, f) }
|
|
4174
|
+
\t\t\t\t\tcontinue
|
|
4050
4175
|
\t\t\t\t}
|
|
4051
|
-
\t\t\t\
|
|
4052
|
-
\t\t\t
|
|
4053
|
-
\t\t\t\
|
|
4054
|
-
\t\t\t\tconst
|
|
4055
|
-
\t\t\t\
|
|
4056
|
-
\t\t\t\t\
|
|
4057
|
-
\t\t\t\t\tif (v instanceof Date) { sp.set(k, v.toISOString()); continue }
|
|
4058
|
-
\t\t\t\t\tif (typeof v === "symbol") continue
|
|
4059
|
-
\t\t\t\t\tsp.set(k, String(v))
|
|
4176
|
+
\t\t\t\tfor (const item of Array.isArray(v) ? v : [v]) {
|
|
4177
|
+
\t\t\t\t\tif (isFile(item)) fd.append(k, item as Blob)
|
|
4178
|
+
\t\t\t\t\telse {
|
|
4179
|
+
\t\t\t\t\t\tconst s = text(item)
|
|
4180
|
+
\t\t\t\t\t\tif (s !== null) fd.append(k, s)
|
|
4181
|
+
\t\t\t\t\t}
|
|
4060
4182
|
\t\t\t\t}
|
|
4061
|
-
\t\t\t\tbody = sp.toString()
|
|
4062
4183
|
\t\t\t}
|
|
4184
|
+
\t\t\treturn fd
|
|
4185
|
+
\t\t}
|
|
4186
|
+
\t\theaders.set("content-type", "application/x-www-form-urlencoded")
|
|
4187
|
+
\t\tconst sp = new URLSearchParams()
|
|
4188
|
+
\t\t/* arrays repeat the key, as they do in multipart and in the query */
|
|
4189
|
+
\t\tfor (const [k, v] of Object.entries(opts.form)) {
|
|
4190
|
+
\t\t\tfor (const item of Array.isArray(v) ? v : [v]) {
|
|
4191
|
+
\t\t\t\tconst s = text(item)
|
|
4192
|
+
\t\t\t\tif (s !== null) sp.append(k, s)
|
|
4193
|
+
\t\t\t}
|
|
4194
|
+
\t\t}
|
|
4195
|
+
\t\treturn sp.toString()
|
|
4196
|
+
\t}
|
|
4197
|
+
|
|
4198
|
+
\t/* a stream body is sent once and needs duplex: "half" (Node's fetch throws without it) */
|
|
4199
|
+
\t#requestInit(init: RequestInit): RequestInit {
|
|
4200
|
+
\t\tif (typeof ReadableStream !== "undefined" && init.body instanceof ReadableStream) {
|
|
4201
|
+
\t\t\t;(init as RequestInit & { duplex?: string }).duplex = "half"
|
|
4063
4202
|
\t\t}
|
|
4203
|
+
\t\treturn init
|
|
4204
|
+
\t}
|
|
4205
|
+
|
|
4206
|
+
\t/* the Request a response hook sees; a stream body was consumed by the send, so it is left off */
|
|
4207
|
+
\t#hookRequest(url: string, init: RequestInit): Request {
|
|
4208
|
+
\t\tconst consumed = typeof ReadableStream !== "undefined" && init.body instanceof ReadableStream
|
|
4209
|
+
\t\treturn new Request(url, consumed ? { ...init, body: null } : init)
|
|
4210
|
+
\t}
|
|
4211
|
+
|
|
4212
|
+
\tasync #doRequest(
|
|
4213
|
+
\t\tmethod: string,
|
|
4214
|
+
\t\tpath: string,
|
|
4215
|
+
\t\topts: _RequestOptions,
|
|
4216
|
+
\t\tisRetry: boolean,
|
|
4217
|
+
\t\trequestMeta?: _RequestMeta,
|
|
4218
|
+
\t): Promise<{ done: () => void; response: Response }> {
|
|
4219
|
+
\t\tconst url = this.#buildURL(path, opts)
|
|
4220
|
+
\t\tconst headers = await this.#buildHeaders(opts, { method, path })
|
|
4221
|
+
\t\tlet body = this.#buildBody(opts, headers)
|
|
4064
4222
|
|
|
4065
4223
|
\t\tif (this.#config.onRequest) {
|
|
4066
4224
|
\t\t\tconst reqCtx: { body?: BodyInit; headers: Headers; invalidatedBy?: string[]; isStale?: boolean; method: string; path: string; selector?: string; state: Record<string, unknown>; url: string } = { body, headers, method, path, state: this.#config.state ?? {}, url }
|
|
@@ -4076,7 +4234,7 @@ function sdkClientDoRequest(): string {
|
|
|
4076
4234
|
\t\t}
|
|
4077
4235
|
|
|
4078
4236
|
\t\tconst { signal, cleanup } = this.#buildSignal(opts)
|
|
4079
|
-
\t\tconst init
|
|
4237
|
+
\t\tconst init = this.#requestInit({ body, headers, method, signal })
|
|
4080
4238
|
\t\tif (this.#config.credentials) init.credentials = this.#config.credentials
|
|
4081
4239
|
\t\tif (this.#config.mode) init.mode = this.#config.mode
|
|
4082
4240
|
|
|
@@ -4086,7 +4244,7 @@ function sdkClientDoRequest(): string {
|
|
|
4086
4244
|
|
|
4087
4245
|
\t\ttry {
|
|
4088
4246
|
\t\t\tsignal?.throwIfAborted()
|
|
4089
|
-
\t\t\tlet response = await this.#
|
|
4247
|
+
\t\t\tlet response = await this.#send(url, init)
|
|
4090
4248
|
|
|
4091
4249
|
\t\t\t/* FormData, Blob, and strings can be sent again; a stream was consumed by the first attempt */
|
|
4092
4250
|
\t\t\tif (response.status === 401 && this.#config.onAuthExpired && !isRetry && !(body instanceof ReadableStream)) {
|
|
@@ -4094,11 +4252,12 @@ function sdkClientDoRequest(): string {
|
|
|
4094
4252
|
\t\t\t\tconst authPrefix = this.#config.authHeaderPrefix ?? "Bearer "
|
|
4095
4253
|
\t\t\t\tconst sent = new Headers(headers).get(authName)
|
|
4096
4254
|
\t\t\t\tconst rejectedToken = sent?.startsWith(authPrefix) ? sent.slice(authPrefix.length) : (sent ?? null)
|
|
4097
|
-
\t\t\t\tconst newToken = await this.#
|
|
4098
|
-
\t\t\t\tif (newToken !== null
|
|
4255
|
+
\t\t\t\tconst newToken = await this.#refreshToken(rejectedToken)
|
|
4256
|
+
\t\t\t\tif (newToken !== null) {
|
|
4099
4257
|
\t\t\t\t\tconst retryHeaders = new Headers(headers)
|
|
4100
4258
|
\t\t\t\t\tretryHeaders.set(authName, \`\${authPrefix}\${newToken}\`)
|
|
4101
|
-
\t\t\t\t\
|
|
4259
|
+
\t\t\t\t\tawait response.body?.cancel().catch(() => {})
|
|
4260
|
+
\t\t\t\t\tresponse = await this.#send(url, { ...init, headers: retryHeaders })
|
|
4102
4261
|
\t\t\t\t}
|
|
4103
4262
|
\t\t\t}
|
|
4104
4263
|
|
|
@@ -4115,12 +4274,14 @@ function sdkClientDoRequest(): string {
|
|
|
4115
4274
|
\t\t\t\t\tisRetry,
|
|
4116
4275
|
\t\t\t\t\tmethod,
|
|
4117
4276
|
\t\t\t\t\tpath,
|
|
4118
|
-
\t\t\t\t\trequest:
|
|
4277
|
+
\t\t\t\t\trequest: this.#hookRequest(url, init),
|
|
4119
4278
|
\t\t\t\t\tresponse,
|
|
4120
4279
|
\t\t\t\t\tretry: () => {
|
|
4121
4280
|
\t\t\t\t\t\tif (isRetry) throw new Error("Max 1 retry per request")
|
|
4281
|
+
\t\t\t\t\t\tif (body instanceof ReadableStream) throw new Error("A streamed request body cannot be sent again")
|
|
4122
4282
|
\t\t\t\t\t\treturn this.#doRequest(method, path, opts, true, requestMeta)
|
|
4123
4283
|
\t\t\t\t\t\t\t.then(async (r) => {
|
|
4284
|
+
\t\t\t\t\t\t\t\tr.done()
|
|
4124
4285
|
\t\t\t\t\t\t\t\tif (!r.response.ok) throw await this.#parseAsClientError(r.response)
|
|
4125
4286
|
\t\t\t\t\t\t\t\treturn r.response
|
|
4126
4287
|
\t\t\t\t\t\t\t})
|
|
@@ -4142,8 +4303,10 @@ function sdkClientDoRequest(): string {
|
|
|
4142
4303
|
\t\t\t\t}
|
|
4143
4304
|
\t\t\t}
|
|
4144
4305
|
|
|
4145
|
-
\t\t\
|
|
4306
|
+
\t\t\t/* the caller releases the timer after reading the body: the timeout covers the whole exchange */
|
|
4307
|
+
\t\t\treturn { done: cleanup, response }
|
|
4146
4308
|
\t\t} catch (err) {
|
|
4309
|
+
\t\t\tcleanup()
|
|
4147
4310
|
\t\t\tconst _errStatus = typeof (err as { status?: unknown })?.status === "number" ? (err as { status: number }).status : undefined
|
|
4148
4311
|
\t\t\tthis.#config.onLog?.({
|
|
4149
4312
|
\t\t\t\tduration_ms: Date.now() - _logStart,
|
|
@@ -4154,8 +4317,82 @@ function sdkClientDoRequest(): string {
|
|
|
4154
4317
|
\t\t\t\t...(_errStatus !== undefined ? { status: _errStatus } : {}),
|
|
4155
4318
|
\t\t\t})
|
|
4156
4319
|
\t\t\tthrow err
|
|
4157
|
-
\t\t}
|
|
4158
|
-
\t
|
|
4320
|
+
\t\t}
|
|
4321
|
+
\t}
|
|
4322
|
+
|
|
4323
|
+
\t/* one refresh for every 401 in flight; the new token is kept for later requests */
|
|
4324
|
+
\t#refreshToken(rejectedToken: string | null): Promise<string | null> {
|
|
4325
|
+
\t\tconst hook = this.#config.onAuthExpired
|
|
4326
|
+
\t\tif (!hook) return Promise.resolve(null)
|
|
4327
|
+
\t\tif (rejectedToken !== null && this.#token !== null && rejectedToken !== this.#token) {
|
|
4328
|
+
\t\t\t/* another request already refreshed past the rejected token */
|
|
4329
|
+
\t\t\treturn Promise.resolve(this.#token)
|
|
4330
|
+
\t\t}
|
|
4331
|
+
\t\tthis.#refreshing ??= Promise.resolve()
|
|
4332
|
+
\t\t\t.then(() => hook({ rejectedToken }))
|
|
4333
|
+
\t\t\t.then((token) => {
|
|
4334
|
+
\t\t\t\tthis.#token = typeof token === "string" && token.length > 0 ? token : null
|
|
4335
|
+
\t\t\t\treturn this.#token
|
|
4336
|
+
\t\t\t})
|
|
4337
|
+
\t\t\t.finally(() => {
|
|
4338
|
+
\t\t\t\tthis.#refreshing = null
|
|
4339
|
+
\t\t\t})
|
|
4340
|
+
\t\treturn this.#refreshing
|
|
4341
|
+
\t}
|
|
4342
|
+
|
|
4343
|
+
\t/* Same rules as client/http.ts; the cross-origin header allowlist comes from
|
|
4344
|
+
\t client/redirect-policy.ts. "same-origin" (default) follows a redirect only while it stays on
|
|
4345
|
+
\t the base URL's origin; "follow" also follows cross-origin ones, carrying only allowlisted
|
|
4346
|
+
\t headers and never a replayed body.
|
|
4347
|
+
\t Browsers hide redirect targets from script, so there the platform follows. */
|
|
4348
|
+
\tasync #send(url: string, init: RequestInit): Promise<Response> {
|
|
4349
|
+
\t\tconst policy = this.#config.redirect ?? "same-origin"
|
|
4350
|
+
\t\tconst g = globalThis as { document?: unknown; WorkerGlobalScope?: unknown }
|
|
4351
|
+
\t\tconst platformFollows = typeof g.document !== "undefined" || typeof g.WorkerGlobalScope !== "undefined"
|
|
4352
|
+
\t\tif (policy === "manual" || policy === "error" || platformFollows) {
|
|
4353
|
+
\t\t\treturn this.#fetchFn(url, policy === "same-origin" ? init : { ...init, redirect: policy })
|
|
4354
|
+
\t\t}
|
|
4355
|
+
\t\tconst origin = new URL(url).origin
|
|
4356
|
+
\t\tlet current = new URL(url)
|
|
4357
|
+
\t\tlet currentInit: RequestInit = { ...init, redirect: "manual" }
|
|
4358
|
+
\t\tconst replayable = (b: unknown) =>
|
|
4359
|
+
\t\t\tb == null || typeof b === "string" || (typeof Blob !== "undefined" && b instanceof Blob) || (typeof FormData !== "undefined" && b instanceof FormData) || b instanceof URLSearchParams || b instanceof ArrayBuffer || ArrayBuffer.isView(b)
|
|
4360
|
+
\t\tfor (let hop = 0; ; hop++) {
|
|
4361
|
+
\t\t\tconst response = await this.#fetchFn(current.toString(), currentInit)
|
|
4362
|
+
\t\t\tconst location = response.headers.get("location")
|
|
4363
|
+
\t\t\tif (![301, 302, 303, 307, 308].includes(response.status) || location === null || hop >= 20) return response
|
|
4364
|
+
\t\t\tlet target: URL
|
|
4365
|
+
\t\t\ttry {
|
|
4366
|
+
\t\t\t\ttarget = new URL(location, current)
|
|
4367
|
+
\t\t\t} catch {
|
|
4368
|
+
\t\t\t\treturn response
|
|
4369
|
+
\t\t\t}
|
|
4370
|
+
\t\t\tif (target.protocol !== "http:" && target.protocol !== "https:") return response
|
|
4371
|
+
\t\t\tconst crossOrigin = target.origin !== origin
|
|
4372
|
+
\t\t\tif (crossOrigin && policy !== "follow") return response
|
|
4373
|
+
\t\t\tconst nextInit: RequestInit = { ...currentInit, headers: new Headers(currentInit.headers) }
|
|
4374
|
+
\t\t\tif (response.status === 307 || response.status === 308) {
|
|
4375
|
+
\t\t\t\tif (currentInit.body != null && (crossOrigin || !replayable(currentInit.body))) return response
|
|
4376
|
+
\t\t\t} else {
|
|
4377
|
+
\t\t\t\tconst method = (currentInit.method ?? "GET").toUpperCase()
|
|
4378
|
+
\t\t\t\tif (response.status === 303 ? method !== "HEAD" : method === "POST") {
|
|
4379
|
+
\t\t\t\t\tnextInit.method = "GET"
|
|
4380
|
+
\t\t\t\t\tnextInit.body = undefined
|
|
4381
|
+
\t\t\t\t\t;(nextInit.headers as Headers).delete("content-type")
|
|
4382
|
+
\t\t\t\t}
|
|
4383
|
+
\t\t\t}
|
|
4384
|
+
\t\t\tif (crossOrigin) {
|
|
4385
|
+
\t\t\t\t/* only allowlisted headers cross: no credential, configured, per-call or hook header */
|
|
4386
|
+
\t\t\t\tconst safe = new Headers()
|
|
4387
|
+
\t\t\t\tfor (const name of [${CROSS_ORIGIN_SAFE_HEADERS.map((h) => JSON.stringify(h)).join(", ")}]) {
|
|
4388
|
+
\t\t\t\t\tconst value = (nextInit.headers as Headers).get(name)
|
|
4389
|
+
\t\t\t\t\tif (value !== null) safe.set(name, value)
|
|
4390
|
+
\t\t\t\t}
|
|
4391
|
+
\t\t\t\tnextInit.headers = safe
|
|
4392
|
+
\t\t\t}
|
|
4393
|
+
\t\t\tawait response.body?.cancel().catch(() => {})
|
|
4394
|
+
\t\t\tcurrent = target
|
|
4395
|
+
\t\t\tcurrentInit = nextInit
|
|
4159
4396
|
\t\t}
|
|
4160
4397
|
\t}
|
|
4161
4398
|
`
|
|
@@ -4174,7 +4411,8 @@ function sdkClientParseBody(): string {
|
|
|
4174
4411
|
\t\tif (ct === "application/octet-stream" || ct === "application/pdf") {
|
|
4175
4412
|
\t\t\treturn response.arrayBuffer()
|
|
4176
4413
|
\t\t}
|
|
4177
|
-
\t\
|
|
4414
|
+
\t\t/* the classes the generated types use: text/*, XML, otherwise bytes */
|
|
4415
|
+
\t\tif (ct.startsWith("text/") || ct === "application/xml" || ct.endsWith("+xml")) return response.text()
|
|
4178
4416
|
\t\t/* unknown content type \u2014 binary-safe fallback */
|
|
4179
4417
|
\t\treturn response.arrayBuffer()
|
|
4180
4418
|
\t}
|
|
@@ -4258,11 +4496,13 @@ function sdkClientRequestThrow(): string {
|
|
|
4258
4496
|
\t\topts: _RequestOptions,
|
|
4259
4497
|
\t\trequestMeta?: _RequestMeta,
|
|
4260
4498
|
\t): Promise<unknown> {
|
|
4261
|
-
\t\tconst { response } = await this.#doRequest(method, path, opts, false, requestMeta)
|
|
4262
|
-
\t\
|
|
4263
|
-
\t\t\
|
|
4499
|
+
\t\tconst { done, response } = await this.#doRequest(method, path, opts, false, requestMeta)
|
|
4500
|
+
\t\ttry {
|
|
4501
|
+
\t\t\tif (!response.ok) throw await this.#parseAsClientError(response)
|
|
4502
|
+
\t\t\treturn await this.#parseBody(response)
|
|
4503
|
+
\t\t} finally {
|
|
4504
|
+
\t\t\tdone()
|
|
4264
4505
|
\t\t}
|
|
4265
|
-
\t\treturn this.#parseBody(response)
|
|
4266
4506
|
\t}
|
|
4267
4507
|
`
|
|
4268
4508
|
}
|
|
@@ -4280,33 +4520,34 @@ function sdkClientRequestSafe(): string {
|
|
|
4280
4520
|
\t\tresponse: Response
|
|
4281
4521
|
\t\tstatus: number
|
|
4282
4522
|
\t}> {
|
|
4283
|
-
\t\tlet doResponse: { response: Response }
|
|
4523
|
+
\t\tlet doResponse: { done: () => void; response: Response }
|
|
4284
4524
|
\t\ttry {
|
|
4285
4525
|
\t\t\tdoResponse = await this.#doRequest(method, path, opts, false, requestMeta)
|
|
4286
4526
|
\t\t} catch (e) {
|
|
4287
4527
|
\t\t\tif (e instanceof _ClientError) return { data: null, error: e, response: e.response, status: e.status }
|
|
4288
4528
|
\t\t\tthrow e
|
|
4289
4529
|
\t\t}
|
|
4290
|
-
\t\tconst { response } = doResponse
|
|
4291
|
-
|
|
4292
|
-
\t\tif (!response.ok) {
|
|
4293
|
-
\t\t\tconst preserved = response.clone()
|
|
4294
|
-
\t\t\tconst error = await this.#parseErrorBody(response)
|
|
4295
|
-
\t\t\treturn {
|
|
4296
|
-
\t\t\t\tdata: null,
|
|
4297
|
-
\t\t\t\terror,
|
|
4298
|
-
\t\t\t\tresponse: preserved,
|
|
4299
|
-
\t\t\t\tstatus: response.status,
|
|
4300
|
-
\t\t\t}
|
|
4301
|
-
\t\t}
|
|
4302
|
-
|
|
4303
|
-
\t\tlet data: unknown
|
|
4530
|
+
\t\tconst { done, response } = doResponse
|
|
4304
4531
|
\t\ttry {
|
|
4305
|
-
\t\t\
|
|
4306
|
-
\t\t
|
|
4307
|
-
\t\t\
|
|
4532
|
+
\t\t\tif (!response.ok) {
|
|
4533
|
+
\t\t\t\tconst preserved = response.clone()
|
|
4534
|
+
\t\t\t\tconst parsed = await this.#parseErrorBody(response)
|
|
4535
|
+
\t\t\t\t/* a failed request always has a truthy error: the JSON body, or a ClientError for any other body */
|
|
4536
|
+
\t\t\t\tconst error = parsed !== undefined && parsed !== null && parsed !== false && parsed !== "" && parsed !== 0
|
|
4537
|
+
\t\t\t\t\t? parsed
|
|
4538
|
+
\t\t\t\t\t: await this.#parseAsClientError(preserved.clone())
|
|
4539
|
+
\t\t\t\treturn { data: null, error, response: preserved, status: response.status }
|
|
4540
|
+
\t\t\t}
|
|
4541
|
+
\t\t\tlet data: unknown
|
|
4542
|
+
\t\t\ttry {
|
|
4543
|
+
\t\t\t\tdata = await this.#parseBody(response)
|
|
4544
|
+
\t\t\t} catch (e) {
|
|
4545
|
+
\t\t\t\treturn { data: null, error: e, response, status: response.status }
|
|
4546
|
+
\t\t\t}
|
|
4547
|
+
\t\t\treturn { data, error: null, response, status: response.status }
|
|
4548
|
+
\t\t} finally {
|
|
4549
|
+
\t\t\tdone()
|
|
4308
4550
|
\t\t}
|
|
4309
|
-
\t\treturn { data, error: null, response, status: response.status }
|
|
4310
4551
|
\t}
|
|
4311
4552
|
`
|
|
4312
4553
|
}
|
|
@@ -4314,33 +4555,33 @@ function sdkClientRequestSafe(): string {
|
|
|
4314
4555
|
function sdkClientRequest(): string {
|
|
4315
4556
|
return `
|
|
4316
4557
|
\tasync #request(entry: _ServiceEntry, input: Record<string, unknown>): Promise<unknown> {
|
|
4317
|
-
\t\tconst path = this.#toColonParams(entry.path)
|
|
4558
|
+
\t\tconst path = this.#toColonParams(entry.path, entry.wildcard)
|
|
4318
4559
|
\t\tconst method = entry.method
|
|
4319
|
-
\t\
|
|
4560
|
+
\t\t/* a copy: the caller's input object is never written to, so reusing it never reuses a key */
|
|
4561
|
+
\t\tconst opts = { ...input } as _RequestOptions
|
|
4320
4562
|
\t\tif (entry.idempotent) {
|
|
4321
4563
|
\t\t\tconst existing = opts.headers?.["Idempotency-Key"] ?? opts.headers?.["idempotency-key"]
|
|
4322
4564
|
\t\t\tif (existing === undefined) {
|
|
4323
|
-
\t\t\t\tconst key = opts.idempotencyKey ??
|
|
4565
|
+
\t\t\t\tconst key = opts.idempotencyKey ?? this.#newRequestId()
|
|
4324
4566
|
\t\t\t\topts.headers = { ...(opts.headers ?? {}), "Idempotency-Key": key }
|
|
4325
4567
|
\t\t\t}
|
|
4326
4568
|
\t\t}
|
|
4327
4569
|
\t\tconst params = opts.params
|
|
4328
|
-
\t\
|
|
4329
|
-
\t\
|
|
4330
|
-
\t\t\
|
|
4331
|
-
\t\t
|
|
4332
|
-
\t\t\
|
|
4333
|
-
\t\t\
|
|
4334
|
-
\t\t\t\
|
|
4335
|
-
\t\t\t\
|
|
4336
|
-
\t\t\t\
|
|
4337
|
-
\t\t\t\
|
|
4338
|
-
\t\t\t\
|
|
4339
|
-
\t\t\t
|
|
4340
|
-
\t\t\
|
|
4341
|
-
\t\t\
|
|
4570
|
+
\t\tlet cp: string
|
|
4571
|
+
\t\ttry {
|
|
4572
|
+
\t\t\tcp = this.#interpolatePath(path, params)
|
|
4573
|
+
\t\t} catch (e) {
|
|
4574
|
+
\t\t\tif (!(e instanceof _PathParamError)) throw e
|
|
4575
|
+
\t\t\tconst err = new _ClientError({
|
|
4576
|
+
\t\t\t\tbody: null,
|
|
4577
|
+
\t\t\t\tdata: null,
|
|
4578
|
+
\t\t\t\tmessage: \`\${e.message} for \${method} \${entry.path}\`,
|
|
4579
|
+
\t\t\t\tresponse: Response.error(),
|
|
4580
|
+
\t\t\t\tstatus: 0,
|
|
4581
|
+
\t\t\t})
|
|
4582
|
+
\t\t\tif (!this.#config.throwOnError) return { data: null, error: err, response: err.response, status: 0 }
|
|
4583
|
+
\t\t\tthrow err
|
|
4342
4584
|
\t\t}
|
|
4343
|
-
\t\tconst cp = params ? this.#interpolatePath(path, params) : path
|
|
4344
4585
|
\t\tconst cs = \`\${method} \${cp}\`
|
|
4345
4586
|
\t\tconst requestMeta = this.#buildRequestMeta(cs, cp, method)
|
|
4346
4587
|
\t\tif (!this.#config.throwOnError) {
|
|
@@ -4370,16 +4611,16 @@ function sdkClientRequestSSE(): string {
|
|
|
4370
4611
|
function sdkClientConnectWS(): string {
|
|
4371
4612
|
return `
|
|
4372
4613
|
\t#connectWS(entry: _ServiceEntry, path: string, opts: _RequestOptions): _TypedWebSocket {
|
|
4373
|
-
\t\
|
|
4374
|
-
\t\
|
|
4375
|
-
\t\
|
|
4376
|
-
\t\
|
|
4377
|
-
\t\
|
|
4378
|
-
\t\tconst ws = this.#createTypedWebSocket(
|
|
4614
|
+
\t\tconst built = new URL(this.#buildURL(path, opts))
|
|
4615
|
+
\t\t/* only the scheme changes: an https URL elsewhere in the query is left alone */
|
|
4616
|
+
\t\tif (built.protocol === "https:") built.protocol = "wss:"
|
|
4617
|
+
\t\telse if (built.protocol === "http:") built.protocol = "ws:"
|
|
4618
|
+
\t\tif (opts.reconnectToken) built.searchParams.append("reconnect_token", opts.reconnectToken)
|
|
4619
|
+
\t\tconst ws = this.#createTypedWebSocket(built.toString(), opts.protocols)
|
|
4379
4620
|
\t\tif (entry.invalidate && entry.invalidate.length > 0) {
|
|
4380
4621
|
\t\t\tconst invalidate = entry.invalidate
|
|
4381
4622
|
\t\t\tconst params = opts.params
|
|
4382
|
-
\t\t\tconst cs = \`WS \${
|
|
4623
|
+
\t\t\tconst cs = \`WS \${this.#interpolatePath(path, params)}\`
|
|
4383
4624
|
\t\t\tws.on("open", () => this.#markStale(invalidate, params, cs))
|
|
4384
4625
|
\t\t}
|
|
4385
4626
|
\t\treturn ws
|
|
@@ -4401,43 +4642,66 @@ function sdkClientBuildRequestMeta(): string {
|
|
|
4401
4642
|
function sdkClientMarkStale(): string {
|
|
4402
4643
|
return `
|
|
4403
4644
|
\t#markStale(invalidate: readonly string[], params: Record<string, string> | undefined, mutationSelector: string): void {
|
|
4404
|
-
\t\
|
|
4645
|
+
\t\tconst exactTable = this.#staleUntil
|
|
4646
|
+
\t\tif (!exactTable || invalidate.length === 0) return
|
|
4405
4647
|
\t\tconst seq = ++this.#invalidationSeq
|
|
4406
4648
|
\t\tconst until = Date.now() + this.#staleTime
|
|
4407
|
-
\t\
|
|
4408
|
-
\t\
|
|
4409
|
-
\t\t\tconst
|
|
4649
|
+
\t\tfor (const target of this.#resolveInvalidationTargets(invalidate, params)) {
|
|
4650
|
+
\t\t\tconst spaceIdx = target.indexOf(" ")
|
|
4651
|
+
\t\t\tconst method = target.slice(0, spaceIdx)
|
|
4652
|
+
\t\t\tconst isPattern = this.#hasPlaceholder(target.slice(spaceIdx + 1))
|
|
4653
|
+
\t\t\tlet table: Map<string, _StaleEntry>
|
|
4654
|
+
\t\t\tif (isPattern) {
|
|
4655
|
+
\t\t\t\ttable = this.#stalePatterns.get(method) ?? new Map<string, _StaleEntry>()
|
|
4656
|
+
\t\t\t\tthis.#stalePatterns.set(method, table)
|
|
4657
|
+
\t\t\t} else {
|
|
4658
|
+
\t\t\t\ttable = exactTable
|
|
4659
|
+
\t\t\t}
|
|
4660
|
+
\t\t\tconst existing = table.get(target)
|
|
4410
4661
|
\t\t\tif (existing) {
|
|
4411
|
-
\t\t\t\tif (!existing.by.includes(mutationSelector)
|
|
4662
|
+
\t\t\t\tif (!existing.by.includes(mutationSelector)) {
|
|
4663
|
+
\t\t\t\t\texisting.by.push(mutationSelector)
|
|
4664
|
+
\t\t\t\t\tif (existing.by.length > this.#maxSourcesPerTarget) existing.by.shift()
|
|
4665
|
+
\t\t\t\t}
|
|
4412
4666
|
\t\t\t\texisting.until = until
|
|
4413
4667
|
\t\t\t\texisting.seq = seq
|
|
4414
|
-
\t\t\t
|
|
4668
|
+
\t\t\t\t/* a new mutation re-marks every instance */
|
|
4669
|
+
\t\t\t\texisting.refreshed?.clear()
|
|
4670
|
+
\t\t\t} else {
|
|
4671
|
+
\t\t\t\ttable.set(target, { by: [mutationSelector], refreshed: isPattern ? new Set<string>() : undefined, seq, until })
|
|
4672
|
+
\t\t\t}
|
|
4673
|
+
\t\t}
|
|
4674
|
+
\t\tif (exactTable.size > this.#staleMaxEntries) {
|
|
4675
|
+
\t\t\tconst now = Date.now()
|
|
4676
|
+
\t\t\tfor (const [k, entry] of exactTable) if (entry.until <= now) exactTable.delete(k)
|
|
4677
|
+
\t\t\t/* still over: drop the oldest marks */
|
|
4678
|
+
\t\t\tfor (const k of exactTable.keys()) {
|
|
4679
|
+
\t\t\t\tif (exactTable.size <= this.#staleMaxEntries) break
|
|
4680
|
+
\t\t\t\texactTable.delete(k)
|
|
4681
|
+
\t\t\t}
|
|
4415
4682
|
\t\t}
|
|
4416
|
-
\t\tif (this.#staleUntil.size > this.#staleMaxEntries)
|
|
4417
|
-
\t\t\tfor (const [k, entry] of this.#staleUntil)
|
|
4418
|
-
\t\t\t\tif (entry.until <= Date.now()) this.#staleUntil.delete(k)
|
|
4419
4683
|
\t}
|
|
4420
4684
|
`
|
|
4421
4685
|
}
|
|
4422
4686
|
|
|
4423
4687
|
function sdkClientClearStale(): string {
|
|
4424
4688
|
return `
|
|
4689
|
+
\t/* a successful stale read clears that resource only: its exact mark, and this path's share of a pattern mark */
|
|
4425
4690
|
\t#clearStale(concreteSelector: string, concretePath: string, method: string, seqSnapshot: number): void {
|
|
4426
4691
|
\t\tif (!this.#staleUntil) return
|
|
4427
4692
|
\t\tconst exact = this.#staleUntil.get(concreteSelector)
|
|
4428
4693
|
\t\tif (exact && exact.seq <= seqSnapshot) this.#staleUntil.delete(concreteSelector)
|
|
4429
|
-
\t\tconst
|
|
4430
|
-
\t\
|
|
4431
|
-
\t\
|
|
4432
|
-
\t\t\tif (!
|
|
4433
|
-
\t\t\
|
|
4434
|
-
\t\t\
|
|
4435
|
-
\t\t\tconst
|
|
4436
|
-
\t\t\tif (
|
|
4437
|
-
\t\t\t\ttoDelete.push(key)
|
|
4694
|
+
\t\tconst table = this.#stalePatterns.get(method)
|
|
4695
|
+
\t\tif (!table) return
|
|
4696
|
+
\t\tfor (const [key, entry] of table) {
|
|
4697
|
+
\t\t\tif (entry.seq > seqSnapshot || !entry.refreshed) continue
|
|
4698
|
+
\t\t\tif (!this.#pathMatchesPattern(concretePath, key.slice(key.indexOf(" ") + 1))) continue
|
|
4699
|
+
\t\t\tif (entry.refreshed.size >= 1024) {
|
|
4700
|
+
\t\t\t\tconst oldest = entry.refreshed.values().next().value
|
|
4701
|
+
\t\t\t\tif (oldest !== undefined) entry.refreshed.delete(oldest)
|
|
4438
4702
|
\t\t\t}
|
|
4703
|
+
\t\t\tentry.refreshed.add(concretePath)
|
|
4439
4704
|
\t\t}
|
|
4440
|
-
\t\tfor (const key of toDelete) this.#staleUntil.delete(key)
|
|
4441
4705
|
\t}
|
|
4442
4706
|
`
|
|
4443
4707
|
}
|
|
@@ -4449,6 +4713,7 @@ function sdkClientDispose(): string {
|
|
|
4449
4713
|
\t\tthis.#disposed = true
|
|
4450
4714
|
\t\tthis.#disposeCtrl.abort()
|
|
4451
4715
|
\t\tthis.#staleUntil?.clear()
|
|
4716
|
+
\t\tthis.#stalePatterns.clear()
|
|
4452
4717
|
\t\tthis.#resourceCache.clear()
|
|
4453
4718
|
\t\tthis.#patternRegexCache.clear()
|
|
4454
4719
|
\t}
|
|
@@ -4465,23 +4730,25 @@ function sdkClientDoSSE(): string {
|
|
|
4465
4730
|
\t\tif (opts.lastEventId) {
|
|
4466
4731
|
\t\t\theaders.set("last-event-id", opts.lastEventId)
|
|
4467
4732
|
\t\t}
|
|
4733
|
+
\t\t/* a streamed operation carries its body like any other request */
|
|
4734
|
+
\t\tlet body = this.#buildBody(opts, headers)
|
|
4468
4735
|
\t\tconst { signal, cleanup } = this.#buildSignal(opts, true)
|
|
4469
|
-
\t\tlet reader: ReadableStreamDefaultReader<Uint8Array> | undefined
|
|
4470
4736
|
\t\ttry {
|
|
4471
4737
|
\t\t\tif (this.#config.onRequest) {
|
|
4472
|
-
\t\t\t\tconst reqCtx: { headers: Headers; invalidatedBy?: string[]; isStale?: boolean; method: string; path: string; selector?: string; state: Record<string, unknown>; url: string } = { headers, method, path, state: this.#config.state ?? {}, url }
|
|
4738
|
+
\t\t\t\tconst reqCtx: { body?: BodyInit; headers: Headers; invalidatedBy?: string[]; isStale?: boolean; method: string; path: string; selector?: string; state: Record<string, unknown>; url: string } = { body, headers, method, path, state: this.#config.state ?? {}, url }
|
|
4473
4739
|
\t\t\t\tfor (const hook of this.#config.onRequest) {
|
|
4474
4740
|
\t\t\t\t\tawait hook(reqCtx)
|
|
4475
4741
|
\t\t\t\t}
|
|
4742
|
+
\t\t\t\tif (reqCtx.body !== body) body = reqCtx.body
|
|
4476
4743
|
\t\t\t}
|
|
4477
|
-
\t\t\tconst sseInit
|
|
4744
|
+
\t\t\tconst sseInit = this.#requestInit({ body, headers, method, signal })
|
|
4478
4745
|
\t\t\tif (this.#config.credentials) sseInit.credentials = this.#config.credentials
|
|
4479
4746
|
\t\t\tif (this.#config.mode) sseInit.mode = this.#config.mode
|
|
4480
4747
|
\t\t\tsignal?.throwIfAborted()
|
|
4481
4748
|
\t\t\tlet response = await this.#fetchFn(url, sseInit)
|
|
4482
4749
|
\t\t\tif (this.#config.onResponse) {
|
|
4483
4750
|
\t\t\t\tconst resCtx: { invalidatedBy?: string[]; isRetry: boolean; isStale?: boolean; method: string; path: string; request: Request; response: Response; retry: () => Promise<Response>; selector?: string; state: Record<string, unknown>; url: string } = {
|
|
4484
|
-
\t\t\t\t\tisRetry: false, method, path, request:
|
|
4751
|
+
\t\t\t\t\tisRetry: false, method, path, request: this.#hookRequest(url, sseInit), response,
|
|
4485
4752
|
\t\t\t\t\tretry: () => { throw new Error("SSE streams do not support retry") },
|
|
4486
4753
|
\t\t\t\t\tstate: this.#config.state ?? {}, url,
|
|
4487
4754
|
\t\t\t\t}
|
|
@@ -4494,36 +4761,12 @@ function sdkClientDoSSE(): string {
|
|
|
4494
4761
|
\t\t\t\tthrow await this.#parseAsClientError(response)
|
|
4495
4762
|
\t\t\t}
|
|
4496
4763
|
\t\t\tif (entry.invalidate && entry.invalidate.length > 0) {
|
|
4497
|
-
\t\t\t\tconst cs = \`\${method} \${
|
|
4764
|
+
\t\t\t\tconst cs = \`\${method} \${this.#interpolatePath(path, opts.params)}\`
|
|
4498
4765
|
\t\t\t\tthis.#markStale(entry.invalidate, opts.params, cs)
|
|
4499
4766
|
\t\t\t}
|
|
4500
4767
|
\t\t\tif (!response.body) return
|
|
4501
|
-
\t\t\
|
|
4502
|
-
\t\t\treader = response.body.getReader()
|
|
4503
|
-
\t\t\tlet buffer = ""
|
|
4504
|
-
\t\t\tconst maxBuffer = this.#sseMaxBufferChars
|
|
4505
|
-
\t\t\twhile (true) {
|
|
4506
|
-
\t\t\t\tconst { done, value } = await reader.read()
|
|
4507
|
-
\t\t\t\tif (done) break
|
|
4508
|
-
\t\t\t\tbuffer += decoder.decode(value, { stream: true })
|
|
4509
|
-
\t\t\t\tif (buffer.length > maxBuffer) throw new Error(\`SSE buffer exceeded \${maxBuffer} characters\`)
|
|
4510
|
-
\t\t\t\tconst blocks = buffer.split(/\\r\\n\\r\\n|\\r\\n\\r|\\r\\n\\n|\\r\\r\\n|\\n\\r\\n|\\n\\r|\\r\\r|\\n\\n/)
|
|
4511
|
-
\t\t\t\tbuffer = blocks.pop() ?? ""
|
|
4512
|
-
\t\t\t\tfor (const block of blocks) {
|
|
4513
|
-
\t\t\t\t\tif (block.trim() === "") continue
|
|
4514
|
-
\t\t\t\t\tconst event = this.#parseSSEBlock(block)
|
|
4515
|
-
\t\t\t\t\tif (event) yield event
|
|
4516
|
-
\t\t\t\t}
|
|
4517
|
-
\t\t\t}
|
|
4518
|
-
\t\t\tif (buffer.trim() !== "") {
|
|
4519
|
-
\t\t\t\tconst event = this.#parseSSEBlock(buffer)
|
|
4520
|
-
\t\t\t\tif (event) yield event
|
|
4521
|
-
\t\t\t}
|
|
4768
|
+
\t\t\tyield* this.#parseSSE(response.body)
|
|
4522
4769
|
\t\t} finally {
|
|
4523
|
-
\t\t\tif (reader) {
|
|
4524
|
-
\t\t\t\ttry { await reader.cancel() } catch {}
|
|
4525
|
-
\t\t\t\treader.releaseLock()
|
|
4526
|
-
\t\t\t}
|
|
4527
4770
|
\t\t\tcleanup()
|
|
4528
4771
|
\t\t}
|
|
4529
4772
|
\t}
|
|
@@ -4532,50 +4775,101 @@ function sdkClientDoSSE(): string {
|
|
|
4532
4775
|
|
|
4533
4776
|
function sdkClientParseSSEBlock(): string {
|
|
4534
4777
|
return `
|
|
4535
|
-
\t
|
|
4536
|
-
\t
|
|
4537
|
-
\t
|
|
4538
|
-
\
|
|
4778
|
+
\t/* Keep in sync with client/sse.ts — the WHATWG event-stream rules: lines end at CRLF, LF or CR;
|
|
4779
|
+
\t a leading BOM is skipped; id is sticky and ignored if it holds NUL; retry is digits only; an
|
|
4780
|
+
\t event still open when the stream ends is discarded, never dispatched. */
|
|
4781
|
+
\tasync *#parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<_SSEEvent> {
|
|
4782
|
+
\t\tconst decoder = new TextDecoder()
|
|
4783
|
+
\t\tconst reader = stream.getReader()
|
|
4784
|
+
\t\tconst maxBuffer = this.#sseMaxBufferChars
|
|
4785
|
+
\t\tlet pending = ""
|
|
4786
|
+
\t\tlet first = true
|
|
4787
|
+
\t\tlet data: string[] = []
|
|
4788
|
+
\t\tlet dataSize = 0
|
|
4789
|
+
\t\tlet hasData = false
|
|
4539
4790
|
\t\tlet event: string | undefined
|
|
4540
|
-
\t\tlet
|
|
4791
|
+
\t\tlet lastId = ""
|
|
4541
4792
|
\t\tlet retry: number | undefined
|
|
4542
|
-
\t\
|
|
4543
|
-
\t\t\
|
|
4544
|
-
\t\t\
|
|
4545
|
-
\t\t\
|
|
4546
|
-
\t\t\
|
|
4547
|
-
\t\t\
|
|
4548
|
-
\t\t\
|
|
4549
|
-
\t\t\t\tfield = line
|
|
4550
|
-
\t\t\t\tval = ""
|
|
4551
|
-
\t\t\t} else {
|
|
4552
|
-
\t\t\t\tfield = line.slice(0, colonIdx)
|
|
4553
|
-
\t\t\t\tval = line.slice(colonIdx + 1)
|
|
4554
|
-
\t\t\t\tif (val.startsWith(" ")) val = val.slice(1)
|
|
4793
|
+
\t\tconst dispatch = (): _SSEEvent | undefined => {
|
|
4794
|
+
\t\t\tconst out: _SSEEvent | undefined = hasData ? { data: data.join("\\n") } : undefined
|
|
4795
|
+
\t\t\tif (out) {
|
|
4796
|
+
\t\t\t\tif (event !== undefined && event !== "") out.event = event
|
|
4797
|
+
\t\t\t\tif (lastId !== "") out.id = lastId
|
|
4798
|
+
\t\t\t\tif (retry !== undefined) out.retry = retry
|
|
4799
|
+
\t\t\t\tretry = undefined
|
|
4555
4800
|
\t\t\t}
|
|
4801
|
+
\t\t\tdata = []
|
|
4802
|
+
\t\t\tdataSize = 0
|
|
4803
|
+
\t\t\thasData = false
|
|
4804
|
+
\t\t\tevent = undefined
|
|
4805
|
+
\t\t\treturn out
|
|
4806
|
+
\t\t}
|
|
4807
|
+
\t\tconst processLine = (line: string): void => {
|
|
4808
|
+
\t\t\tif (line.charCodeAt(0) === 58) return
|
|
4809
|
+
\t\t\tconst colon = line.indexOf(":")
|
|
4810
|
+
\t\t\tconst field = colon === -1 ? line : line.slice(0, colon)
|
|
4811
|
+
\t\t\tlet value = colon === -1 ? "" : line.slice(colon + 1)
|
|
4812
|
+
\t\t\tif (value.charCodeAt(0) === 32) value = value.slice(1)
|
|
4556
4813
|
\t\t\tswitch (field) {
|
|
4557
4814
|
\t\t\t\tcase "data":
|
|
4558
|
-
\t\t\t\t\
|
|
4815
|
+
\t\t\t\t\tdataSize += value.length + 1
|
|
4816
|
+
\t\t\t\t\tif (dataSize > maxBuffer) throw new Error(\`SSE event exceeded \${maxBuffer} characters\`)
|
|
4817
|
+
\t\t\t\t\tdata.push(value)
|
|
4818
|
+
\t\t\t\t\thasData = true
|
|
4559
4819
|
\t\t\t\t\tbreak
|
|
4560
4820
|
\t\t\t\tcase "event":
|
|
4561
|
-
\t\t\t\t\tevent =
|
|
4821
|
+
\t\t\t\t\tevent = value
|
|
4562
4822
|
\t\t\t\t\tbreak
|
|
4563
4823
|
\t\t\t\tcase "id":
|
|
4564
|
-
\t\t\t\t\tif (!
|
|
4824
|
+
\t\t\t\t\tif (!value.includes("\\0")) lastId = value
|
|
4565
4825
|
\t\t\t\t\tbreak
|
|
4566
|
-
\t\t\t\tcase "retry":
|
|
4567
|
-
\t\t\t\t\
|
|
4568
|
-
\t\t\t\t\tif (Number.isFinite(nr) && nr >= 0) retry = nr
|
|
4826
|
+
\t\t\t\tcase "retry":
|
|
4827
|
+
\t\t\t\t\tif (/^\\d+$/.test(value)) retry = Number(value)
|
|
4569
4828
|
\t\t\t\t\tbreak
|
|
4829
|
+
\t\t\t}
|
|
4830
|
+
\t\t}
|
|
4831
|
+
\t\ttry {
|
|
4832
|
+
\t\t\twhile (true) {
|
|
4833
|
+
\t\t\t\tconst { done, value } = await reader.read()
|
|
4834
|
+
\t\t\t\tif (done) break
|
|
4835
|
+
\t\t\t\tpending += decoder.decode(value, { stream: true })
|
|
4836
|
+
\t\t\t\tif (first && pending.length > 0) {
|
|
4837
|
+
\t\t\t\t\tif (pending.charCodeAt(0) === 0xfeff) pending = pending.slice(1)
|
|
4838
|
+
\t\t\t\t\tfirst = false
|
|
4839
|
+
\t\t\t\t}
|
|
4840
|
+
\t\t\t\tlet start = 0
|
|
4841
|
+
\t\t\t\twhile (start < pending.length) {
|
|
4842
|
+
\t\t\t\t\tlet end = start
|
|
4843
|
+
\t\t\t\t\twhile (end < pending.length) {
|
|
4844
|
+
\t\t\t\t\t\tconst c = pending.charCodeAt(end)
|
|
4845
|
+
\t\t\t\t\t\tif (c === 10 || c === 13) break
|
|
4846
|
+
\t\t\t\t\t\tend++
|
|
4847
|
+
\t\t\t\t\t}
|
|
4848
|
+
\t\t\t\t\tif (end === pending.length) break
|
|
4849
|
+
\t\t\t\t\t/* a CR as the last character may be the first half of CRLF */
|
|
4850
|
+
\t\t\t\t\tif (pending.charCodeAt(end) === 13 && end === pending.length - 1) break
|
|
4851
|
+
\t\t\t\t\tconst line = pending.slice(start, end)
|
|
4852
|
+
\t\t\t\t\tstart = end + (pending.charCodeAt(end) === 13 && pending.charCodeAt(end + 1) === 10 ? 2 : 1)
|
|
4853
|
+
\t\t\t\t\tif (line === "") {
|
|
4854
|
+
\t\t\t\t\t\tconst out = dispatch()
|
|
4855
|
+
\t\t\t\t\t\tif (out) yield out
|
|
4856
|
+
\t\t\t\t\t} else {
|
|
4857
|
+
\t\t\t\t\t\tprocessLine(line)
|
|
4858
|
+
\t\t\t\t\t}
|
|
4570
4859
|
\t\t\t\t}
|
|
4860
|
+
\t\t\t\tpending = pending.slice(start)
|
|
4861
|
+
\t\t\t\tif (pending.length > maxBuffer) throw new Error(\`SSE buffer exceeded \${maxBuffer} characters\`)
|
|
4862
|
+
\t\t\t}
|
|
4863
|
+
\t\t\t/* a lone CR held back at the end of the stream still terminates its line */
|
|
4864
|
+
\t\t\tif (pending === "\\r") {
|
|
4865
|
+
\t\t\t\tconst out = dispatch()
|
|
4866
|
+
\t\t\t\tif (out) yield out
|
|
4571
4867
|
\t\t\t}
|
|
4868
|
+
\t\t\t/* anything else still open is an incomplete event: discard it */
|
|
4869
|
+
\t\t} finally {
|
|
4870
|
+
\t\t\tawait reader.cancel().catch(() => {})
|
|
4871
|
+
\t\t\treader.releaseLock()
|
|
4572
4872
|
\t\t}
|
|
4573
|
-
\t\tif (isComment || data === undefined) return undefined
|
|
4574
|
-
\t\tconst evt: _SSEEvent = { data }
|
|
4575
|
-
\t\tif (event !== undefined) evt.event = event
|
|
4576
|
-
\t\tif (id !== undefined) evt.id = id
|
|
4577
|
-
\t\tif (retry !== undefined) evt.retry = retry
|
|
4578
|
-
\t\treturn evt
|
|
4579
4873
|
\t}
|
|
4580
4874
|
`
|
|
4581
4875
|
}
|
|
@@ -4596,108 +4890,152 @@ export function isClientError(e: unknown): e is _ClientError {
|
|
|
4596
4890
|
`
|
|
4597
4891
|
}
|
|
4598
4892
|
|
|
4599
|
-
|
|
4893
|
+
/** The IR every TypeScript SDK emitter reads: refs inlined, derived ids, duplicate ids refused. */
|
|
4894
|
+
export function sdkIR(spec: OpenApiSpecInput): IR {
|
|
4895
|
+
return toIR(resolveRefs(spec), { deriveOperationIds: true, duplicateOperationIds: "throw" })
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4898
|
+
/** Flat `resource → action → entry` map of every operation, for SDK result objects. */
|
|
4899
|
+
export function serviceMapOf(ir: IR): Record<string, Record<string, ServiceEntry>> {
|
|
4900
|
+
/* null-prototype: an operationId of "__proto__.x" must not reach Object.prototype */
|
|
4901
|
+
const serviceMap: Record<string, Record<string, ServiceEntry>> = Object.create(null)
|
|
4902
|
+
for (const op of ir.operations) {
|
|
4903
|
+
const segments = op.id.split(".")
|
|
4904
|
+
const resource = segments.length === 1 ? op.id : (segments[0] ?? op.id)
|
|
4905
|
+
const action = segments.length === 1 ? "_call" : segments.slice(1).join(".")
|
|
4906
|
+
if (!Object.hasOwn(serviceMap, resource)) serviceMap[resource] = Object.create(null)
|
|
4907
|
+
serviceMap[resource][action] = buildServiceEntryForOp(op)
|
|
4908
|
+
}
|
|
4909
|
+
return serviceMap
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
export function collectSDKMethods(
|
|
4913
|
+
spec: OpenApiSpecInput,
|
|
4914
|
+
/* toIR validates namespace collisions; an operation without an operationId gets a derived
|
|
4915
|
+
one instead of silently vanishing from the SDK */
|
|
4916
|
+
ir = sdkIR(spec),
|
|
4917
|
+
): {
|
|
4600
4918
|
serviceMap: Record<string, Record<string, ServiceEntry>>
|
|
4601
4919
|
nestedMap: Map<string, NestedServiceNode>
|
|
4602
4920
|
methods: SDKMethod[]
|
|
4603
4921
|
} {
|
|
4604
|
-
|
|
4605
|
-
const ir = toIR(spec)
|
|
4606
|
-
const resolved = resolveRefs(spec)
|
|
4607
|
-
const serviceMap: Record<string, Record<string, ServiceEntry>> = {}
|
|
4922
|
+
const resolve = irResolver(ir.schemas)
|
|
4608
4923
|
const methods: SDKMethod[] = []
|
|
4609
4924
|
|
|
4610
|
-
for (const
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
if (params.length > 0) entry.params = params
|
|
4631
|
-
const sse = isSSEOperation(op)
|
|
4632
|
-
if (sse) entry.sse = true
|
|
4633
|
-
if (op["x-websocket"] === true) entry.ws = true
|
|
4634
|
-
if (op["x-realtime"] === true) entry.realtime = true
|
|
4635
|
-
if (op["x-idempotency-key"] === true) entry.idempotent = true
|
|
4636
|
-
const xinv = op["x-invalidate"]
|
|
4637
|
-
if (Array.isArray(xinv) && xinv.length > 0) entry.invalidate = xinv as string[]
|
|
4638
|
-
|
|
4639
|
-
serviceMap[resource][action] = entry
|
|
4640
|
-
const isWs = op["x-websocket"] === true
|
|
4641
|
-
const isRealtime = op["x-realtime"] === true
|
|
4642
|
-
const inputResult = emitSDKInputType(op, path)
|
|
4643
|
-
methods.push({
|
|
4644
|
-
action,
|
|
4645
|
-
errorsByStatusType: emitSDKErrorsByStatusType(op),
|
|
4646
|
-
inputHasMandatory: inputResult.hasMandatory,
|
|
4647
|
-
inputType: inputResult.type,
|
|
4648
|
-
realtime: isRealtime,
|
|
4649
|
-
resource,
|
|
4650
|
-
responseType: emitSDKResponseType(op),
|
|
4651
|
-
sse,
|
|
4652
|
-
ws: isWs,
|
|
4653
|
-
})
|
|
4654
|
-
}
|
|
4925
|
+
for (const op of ir.operations) {
|
|
4926
|
+
const operationId = op.id
|
|
4927
|
+
const segments = operationId.split(".")
|
|
4928
|
+
const isTopLevel = segments.length === 1
|
|
4929
|
+
const resource = isTopLevel ? operationId : (segments[0] ?? operationId)
|
|
4930
|
+
const action = isTopLevel ? "_call" : segments.slice(1).join(".")
|
|
4931
|
+
|
|
4932
|
+
const inputResult = emitSDKInputType(op)
|
|
4933
|
+
methods.push({
|
|
4934
|
+
action,
|
|
4935
|
+
errorsByStatusType: emitSDKErrorsByStatusType(op, resolve),
|
|
4936
|
+
id: operationId,
|
|
4937
|
+
inputHasMandatory: inputResult.hasMandatory,
|
|
4938
|
+
inputType: inputResult.type,
|
|
4939
|
+
realtime: op.extensions.realtime === true,
|
|
4940
|
+
resource,
|
|
4941
|
+
responseType: emitSDKResponseType(op),
|
|
4942
|
+
sse: op.extensions.sse === true,
|
|
4943
|
+
ws: op.extensions.websocket === true,
|
|
4944
|
+
})
|
|
4655
4945
|
}
|
|
4656
4946
|
|
|
4657
|
-
const nestedMap = buildNestedServiceMap(ir.tree,
|
|
4658
|
-
return { methods, nestedMap, serviceMap }
|
|
4947
|
+
const nestedMap = buildNestedServiceMap(safeMemberTree(ir.tree, true))
|
|
4948
|
+
return { methods, nestedMap, serviceMap: serviceMapOf(ir) }
|
|
4659
4949
|
}
|
|
4660
4950
|
|
|
4661
|
-
function buildServiceEntryForOp(op:
|
|
4662
|
-
const params =
|
|
4663
|
-
const entry: ServiceEntry = { method: method
|
|
4951
|
+
function buildServiceEntryForOp(op: IROperation): ServiceEntry {
|
|
4952
|
+
const params = openApiPathParams(op.path)
|
|
4953
|
+
const entry: ServiceEntry = { method: op.method, path: op.path }
|
|
4664
4954
|
if (params.length > 0) entry.params = params
|
|
4665
|
-
const
|
|
4666
|
-
if (
|
|
4667
|
-
if (op
|
|
4668
|
-
if (op
|
|
4669
|
-
if (op
|
|
4670
|
-
|
|
4671
|
-
|
|
4955
|
+
const wildcard = op.params.path.find((p) => p.wildcard === true)
|
|
4956
|
+
if (wildcard) entry.wildcard = wildcard.name
|
|
4957
|
+
if (op.extensions.sse) entry.sse = true
|
|
4958
|
+
if (op.extensions.websocket) entry.ws = true
|
|
4959
|
+
if (op.extensions.realtime) entry.realtime = true
|
|
4960
|
+
if (op.extensions.idempotencyKey) entry.idempotent = true
|
|
4961
|
+
const inv = op.extensions.invalidates
|
|
4962
|
+
if (inv && inv.length > 0) entry.invalidate = inv
|
|
4672
4963
|
return entry
|
|
4673
4964
|
}
|
|
4674
4965
|
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4966
|
+
/** Members a generated client already has; a resource of the same name would hide them. */
|
|
4967
|
+
const SDK_ROOT_MEMBERS = new Set(["dispose", "state"])
|
|
4968
|
+
|
|
4969
|
+
/**
|
|
4970
|
+
* Names JavaScript machinery reads off any object: a `then` member makes the client a
|
|
4971
|
+
* thenable, `toString` breaks `String(sdk)`, `__proto__` sets the prototype.
|
|
4972
|
+
*/
|
|
4973
|
+
const SDK_OBJECT_MEMBERS = new Set([
|
|
4974
|
+
"__defineGetter__",
|
|
4975
|
+
"__defineSetter__",
|
|
4976
|
+
"__lookupGetter__",
|
|
4977
|
+
"__lookupSetter__",
|
|
4978
|
+
"__proto__",
|
|
4979
|
+
"constructor",
|
|
4980
|
+
"hasOwnProperty",
|
|
4981
|
+
"isPrototypeOf",
|
|
4982
|
+
"propertyIsEnumerable",
|
|
4983
|
+
"then",
|
|
4984
|
+
"toJSON",
|
|
4985
|
+
"toLocaleString",
|
|
4986
|
+
"toString",
|
|
4987
|
+
"valueOf",
|
|
4988
|
+
])
|
|
4989
|
+
|
|
4990
|
+
/**
|
|
4991
|
+
* The resource tree with every member name a client can expose. A name that would shadow a
|
|
4992
|
+
* client member or an `Object.prototype` name gets a trailing `_` (`state` → `state_`), the
|
|
4993
|
+
* same rename in the interface and the service map, so types and runtime agree.
|
|
4994
|
+
*/
|
|
4995
|
+
function safeMemberTree(ns: IRNamespace, root: boolean): IRNamespace {
|
|
4996
|
+
const entries = new Map<string, IRNamespace["entries"] extends Map<string, infer E> ? E : never>()
|
|
4997
|
+
const sorted = [...ns.entries.entries()].sort(([a], [b]) => compareCodeUnits(a, b))
|
|
4998
|
+
for (const [key, entry] of sorted) {
|
|
4999
|
+
let name = key
|
|
5000
|
+
if ((root && SDK_ROOT_MEMBERS.has(key)) || SDK_OBJECT_MEMBERS.has(key)) {
|
|
5001
|
+
name = `${key}_`
|
|
5002
|
+
while (ns.entries.has(name) || entries.has(name)) name += "_"
|
|
5003
|
+
}
|
|
5004
|
+
entries.set(name, entry.kind === "namespace" ? { kind: "namespace", ns: safeMemberTree(entry.ns, false) } : entry)
|
|
5005
|
+
}
|
|
5006
|
+
return { entries }
|
|
5007
|
+
}
|
|
5008
|
+
|
|
5009
|
+
/**
|
|
5010
|
+
* The member path of every operation on a generated TypeScript client, after the renames
|
|
5011
|
+
* `safeMemberTree` applies: `["state_", "get"]` for an operation `state.get`. A namespace whose
|
|
5012
|
+
* only method is `_call` is callable itself, so its path ends at the namespace.
|
|
5013
|
+
*/
|
|
5014
|
+
export function sdkMemberPaths(ir: IR): Map<string, string[]> {
|
|
5015
|
+
const out = new Map<string, string[]>()
|
|
5016
|
+
const walk = (ns: IRNamespace, prefix: string[]): void => {
|
|
5017
|
+
const methods = methodsOf(ns)
|
|
5018
|
+
for (const [key, entry] of ns.entries) {
|
|
5019
|
+
if (entry.kind === "namespace") {
|
|
5020
|
+
walk(entry.ns, [...prefix, key])
|
|
5021
|
+
} else if (key !== "_call") {
|
|
5022
|
+
out.set(entry.op.id, [...prefix, key])
|
|
5023
|
+
} else if (methods.length === 1) {
|
|
5024
|
+
/* the interface promotes a lone `_call` to the namespace itself and drops any other */
|
|
5025
|
+
out.set(entry.op.id, prefix)
|
|
5026
|
+
}
|
|
4687
5027
|
}
|
|
4688
5028
|
}
|
|
5029
|
+
walk(safeMemberTree(ir.tree, true), [])
|
|
5030
|
+
return out
|
|
5031
|
+
}
|
|
4689
5032
|
|
|
5033
|
+
function buildNestedServiceMap(ns: IRNamespace): Map<string, NestedServiceNode> {
|
|
4690
5034
|
function walkNs(namespace: IRNamespace): Map<string, NestedServiceNode> {
|
|
4691
5035
|
const map = new Map<string, NestedServiceNode>()
|
|
4692
|
-
for (const [key, entry] of [...namespace.entries.entries()].sort(([a], [b]) => a
|
|
4693
|
-
if (entry.kind === "method") {
|
|
4694
|
-
|
|
4695
|
-
if (!found) continue
|
|
4696
|
-
const serviceEntry = buildServiceEntryForOp(found.op, found.path, found.method)
|
|
4697
|
-
map.set(key, { entry: serviceEntry, kind: "leaf" })
|
|
4698
|
-
} else {
|
|
4699
|
-
map.set(key, { children: walkNs(entry.ns), kind: "ns" })
|
|
4700
|
-
}
|
|
5036
|
+
for (const [key, entry] of [...namespace.entries.entries()].sort(([a], [b]) => compareCodeUnits(a, b))) {
|
|
5037
|
+
if (entry.kind === "method") map.set(key, { entry: buildServiceEntryForOp(entry.op), kind: "leaf" })
|
|
5038
|
+
else map.set(key, { children: walkNs(entry.ns), kind: "ns" })
|
|
4701
5039
|
}
|
|
4702
5040
|
return map
|
|
4703
5041
|
}
|
|
@@ -4705,19 +5043,29 @@ function buildNestedServiceMap(
|
|
|
4705
5043
|
return walkNs(ns)
|
|
4706
5044
|
}
|
|
4707
5045
|
|
|
5046
|
+
const TS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
5047
|
+
|
|
5048
|
+
/** The SDK class name lands in source, the stem in import specifiers and file names. */
|
|
5049
|
+
function validateSdkNames(name: string, stem: string): void {
|
|
5050
|
+
if (!TS_IDENTIFIER.test(name) || SDK_TS_RESERVED.has(name)) {
|
|
5051
|
+
throw new Error(`generateSDK: name ${JSON.stringify(name)} is not a TypeScript identifier`)
|
|
5052
|
+
}
|
|
5053
|
+
if (!/^[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(stem)) {
|
|
5054
|
+
throw new Error(
|
|
5055
|
+
`generateSDK: stem ${JSON.stringify(stem)} must be a plain file name (letters, digits, ".", "_", "-")`,
|
|
5056
|
+
)
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
|
|
4708
5060
|
export function generateSDK(spec: OpenApiSpecInput, options?: { name?: string; stem?: string }): GeneratedSDK {
|
|
4709
5061
|
const sdkName = options?.name ?? "SDK"
|
|
4710
5062
|
const stem = options?.stem ?? "sdk"
|
|
4711
|
-
|
|
5063
|
+
validateSdkNames(sdkName, stem)
|
|
5064
|
+
const ir = sdkIR(spec)
|
|
5065
|
+
const { methods: sdkMethods, nestedMap, serviceMap } = collectSDKMethods(spec, ir)
|
|
4712
5066
|
const hasRealtime = sdkMethods.some((m) => m.realtime)
|
|
4713
|
-
|
|
4714
|
-
const ir = toIR(spec)
|
|
4715
|
-
/* lookup by full operationId — action is already the rest after first segment, joined with "." */
|
|
4716
5067
|
const fullMethodLookup = new Map<string, SDKMethod>()
|
|
4717
|
-
for (const m of sdkMethods)
|
|
4718
|
-
const opId = m.action === "_call" ? m.resource : `${m.resource}.${m.action}`
|
|
4719
|
-
fullMethodLookup.set(opId, m)
|
|
4720
|
-
}
|
|
5068
|
+
for (const m of sdkMethods) fullMethodLookup.set(m.id, m)
|
|
4721
5069
|
|
|
4722
5070
|
return {
|
|
4723
5071
|
files: {
|
|
@@ -4725,7 +5073,7 @@ export function generateSDK(spec: OpenApiSpecInput, options?: { name?: string; s
|
|
|
4725
5073
|
index: buildSDKIndex(sdkName, stem),
|
|
4726
5074
|
map: buildSDKMap(nestedMap),
|
|
4727
5075
|
runtime: hasRealtime ? buildSDKRuntime() : null,
|
|
4728
|
-
types: buildSDKTypes(sdkName, sdkMethods, ir.tree, fullMethodLookup),
|
|
5076
|
+
types: buildSDKTypes(sdkName, sdkMethods, safeMemberTree(ir.tree, true), fullMethodLookup),
|
|
4729
5077
|
},
|
|
4730
5078
|
serviceMap,
|
|
4731
5079
|
}
|