@ultimat3/cli 29.4.0 → 30.1.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/CLAUDE.md +64 -40
- package/README.md +61 -11
- package/package.json +42 -28
- package/src/affected-tests.ts +97 -0
- package/src/affected-unowned.ts +43 -0
- package/src/affected.ts +25 -6
- package/src/app-boundaries.ts +29 -1
- package/src/app-evals.ts +12 -4
- package/src/app-load-unresolved.ts +62 -0
- package/src/app-load.ts +30 -37
- package/src/app-mcp.ts +14 -2
- package/src/app-module-graph.ts +232 -0
- package/src/app-reload-graph.ts +2 -2
- package/src/app-root.ts +37 -1
- package/src/bin.ts +1 -0
- package/src/boundary-findings.ts +6 -0
- package/src/budgets-freshness.ts +32 -0
- package/src/budgets.ts +20 -1
- package/src/build-capture-env.ts +9 -0
- package/src/cdp-shot-a11y.ts +2 -39
- package/src/ci-tested.ts +243 -0
- package/src/client-emit-customer-runtime.ts +139 -0
- package/src/client-emit-customer.ts +108 -0
- package/src/client-emit-drift.ts +72 -0
- package/src/client-emit-header.ts +67 -0
- package/src/client-emit-schemas.ts +175 -0
- package/src/client-emit-types.ts +121 -0
- package/src/client-emit-vendor.ts +146 -0
- package/src/client-errors-routes.ts +229 -0
- package/src/client-errors-script.ts +74 -0
- package/src/client-vitals-route.ts +197 -0
- package/src/client-vitals-script.ts +56 -0
- package/src/cmd-affected.ts +23 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +35 -9
- package/src/cmd-ci-spec.ts +42 -6
- package/src/cmd-ci.ts +99 -2
- package/src/cmd-client-customer.ts +134 -0
- package/src/cmd-client-source.ts +81 -0
- package/src/cmd-client-spec.ts +48 -0
- package/src/cmd-client-write.ts +41 -0
- package/src/cmd-client.ts +111 -0
- package/src/cmd-db-recount.ts +120 -0
- package/src/cmd-db-spec.ts +36 -6
- package/src/cmd-db.ts +11 -3
- package/src/cmd-deploy-compose.ts +61 -3
- package/src/cmd-deploy-drain.ts +10 -2
- package/src/cmd-deploy-helm.ts +11 -0
- package/src/cmd-deploy-preflight.ts +118 -0
- package/src/cmd-deploy-spec.ts +58 -6
- package/src/cmd-deploy-verify.ts +380 -0
- package/src/cmd-deploy.ts +42 -9
- package/src/cmd-docs.ts +34 -4
- package/src/cmd-doctor.ts +66 -9
- package/src/cmd-drill-spec.ts +25 -0
- package/src/cmd-drill.ts +176 -0
- package/src/cmd-env-init.ts +102 -0
- package/src/cmd-env-spec.ts +10 -3
- package/src/cmd-env.ts +15 -10
- package/src/cmd-errors-recent.ts +60 -0
- package/src/cmd-errors-spec.ts +9 -1
- package/src/cmd-errors.ts +4 -0
- package/src/cmd-exercise-spec.ts +19 -0
- package/src/cmd-exercise.ts +129 -0
- package/src/cmd-generate-spec.ts +9 -1
- package/src/cmd-generate.ts +32 -27
- package/src/cmd-graph-load.ts +97 -0
- package/src/cmd-graph-spec.ts +16 -0
- package/src/cmd-graph.ts +73 -0
- package/src/cmd-i18n.ts +4 -1
- package/src/cmd-impact-spec.ts +13 -0
- package/src/cmd-impact.ts +64 -0
- package/src/cmd-jobs-resolve.ts +76 -0
- package/src/cmd-jobs-spec.ts +21 -2
- package/src/cmd-jobs.ts +10 -6
- package/src/cmd-mail-spec.ts +28 -0
- package/src/cmd-mail.ts +70 -0
- package/src/cmd-mcp-lock.ts +69 -0
- package/src/cmd-mcp-spec.ts +23 -4
- package/src/cmd-mcp.ts +17 -3
- package/src/cmd-new-spec.ts +8 -1
- package/src/cmd-new.ts +61 -9
- package/src/cmd-planned.ts +0 -6
- package/src/cmd-repro-spec.ts +31 -0
- package/src/cmd-repro.ts +174 -0
- package/src/cmd-stop-spec.ts +21 -0
- package/src/cmd-stop.ts +166 -0
- package/src/cmd-test-spec.ts +17 -4
- package/src/cmd-test.ts +70 -14
- package/src/cmd-upgrade-spec.ts +26 -0
- package/src/cmd-upgrade.ts +274 -0
- package/src/cmd-verify-spec.ts +3 -3
- package/src/db-backfill.ts +6 -0
- package/src/db-diagnostics.ts +106 -0
- package/src/db-generate.ts +9 -0
- package/src/db-reset-template.ts +1 -1
- package/src/db-rolling.ts +45 -0
- package/src/db-seed-generate.ts +188 -0
- package/src/deploy-verify-findings.ts +98 -0
- package/src/deploy-verify-flags.ts +109 -0
- package/src/deploy-verify-grade.ts +185 -0
- package/src/deploy-verify-queues.ts +53 -0
- package/src/dev-boot.ts +35 -15
- package/src/dev-child-watch.ts +16 -4
- package/src/dev-dashboard-guard.ts +8 -1
- package/src/dev-dashboard.ts +7 -0
- package/src/dev-lock-identity.ts +61 -0
- package/src/dev-lock-port-holder.ts +80 -0
- package/src/dev-lock-steal.ts +124 -0
- package/src/dev-lock.ts +92 -120
- package/src/dev-mail-templates.ts +51 -0
- package/src/dev-rebuild.ts +26 -2
- package/src/dev-route-table.ts +17 -0
- package/src/dev-supervisor.ts +5 -2
- package/src/dev-watch-tree.ts +11 -10
- package/src/dev-watch.ts +1 -1
- package/src/dispatch.ts +29 -6
- package/src/docs-section.ts +69 -0
- package/src/docs-sources.ts +100 -0
- package/src/doctor-auth.ts +11 -2
- package/src/doctor-chart-alerts.ts +55 -0
- package/src/doctor-pooler.ts +107 -0
- package/src/doctor-rolling.ts +35 -0
- package/src/doctor-row-security.ts +40 -0
- package/src/document-options.ts +13 -0
- package/src/drill-sync-lane.ts +25 -0
- package/src/drill-targets.ts +39 -0
- package/src/duplicate-packages.ts +1 -1
- package/src/env-production.ts +93 -0
- package/src/error-codes-shot.ts +59 -0
- package/src/error-codes-tooling.ts +29 -0
- package/src/error-codes.ts +100 -54
- package/src/error-contract.ts +2 -2
- package/src/errors.ts +22 -3
- package/src/exercise-database.ts +99 -0
- package/src/exercise-findings.ts +159 -0
- package/src/exercise-overrides.ts +42 -0
- package/src/exercise-registration.ts +58 -0
- package/src/exercise-static.ts +88 -0
- package/src/exercise-subject.ts +69 -0
- package/src/failure-capture-boot.ts +30 -0
- package/src/framework-env.ts +7 -1
- package/src/framework-schema.ts +72 -5
- package/src/generate-catalog-locales.ts +19 -0
- package/src/generate-chart-alerts.ts +156 -0
- package/src/generate-feature-required.ts +65 -0
- package/src/generate-feature.ts +83 -6
- package/src/generate-files.ts +24 -0
- package/src/generate-format-written.ts +32 -0
- package/src/generate-kinds.ts +6 -0
- package/src/generate-sign-in-config.ts +100 -0
- package/src/generate-sign-in-manifest.ts +28 -0
- package/src/generate-sign-in-mode.ts +101 -0
- package/src/generate-sign-in.ts +250 -0
- package/src/generate-write.ts +44 -7
- package/src/hand-rolled-socket.ts +72 -0
- package/src/i18n-app-key-shapes.ts +29 -0
- package/src/i18n-audit.ts +10 -3
- package/src/i18n-registration.ts +18 -3
- package/src/index.ts +2 -0
- package/src/island-bundle.ts +4 -3
- package/src/island-disposer.ts +116 -0
- package/src/island-realtime-state.ts +34 -6
- package/src/island-realtime.ts +6 -2
- package/src/island-routes.ts +3 -2
- package/src/isr-query-advice.ts +6 -11
- package/src/isr-query-guard.ts +4 -3
- package/src/job-budget.ts +95 -0
- package/src/jobs-json.ts +11 -0
- package/src/lint-findings.ts +104 -0
- package/src/local-cli.ts +34 -4
- package/src/loopback-host.ts +2 -2
- package/src/mail-preview-findings.ts +24 -0
- package/src/mcp-errors.ts +82 -2
- package/src/mcp-host.ts +11 -1
- package/src/mcp-lock-findings.ts +57 -0
- package/src/measure-paths.ts +12 -4
- package/src/measure-scope.ts +27 -13
- package/src/messages-client.ts +23 -0
- package/src/messages-db.ts +56 -0
- package/src/messages-gate.ts +107 -0
- package/src/messages-repro.ts +11 -0
- package/src/messages-shot.ts +51 -0
- package/src/messages-upgrade.ts +45 -0
- package/src/messages.ts +53 -178
- package/src/metrics-endpoint.ts +23 -5
- package/src/new-shapes-generate.ts +71 -0
- package/src/new-shapes.ts +63 -0
- package/src/openapi-compile-ir.ts +68 -0
- package/src/openapi-compile-schema.ts +284 -0
- package/src/openapi-compile.ts +273 -0
- package/src/openapi-refuse.ts +45 -0
- package/src/otlp-export.ts +2 -1
- package/src/output-findings.ts +123 -0
- package/src/output.ts +32 -8
- package/src/package-boundaries.ts +110 -0
- package/src/page-pins.ts +23 -0
- package/src/plain-routes.ts +13 -0
- package/src/prerender-entries.ts +59 -0
- package/src/prerender-heaviest.ts +27 -0
- package/src/prerender-preload.ts +6 -0
- package/src/prerender-report.ts +12 -0
- package/src/prerender-seo-reads.ts +38 -0
- package/src/prerender-served.ts +22 -7
- package/src/prerender-vitals.ts +42 -0
- package/src/prerender.ts +61 -32
- package/src/pwa-artifacts.ts +4 -3
- package/src/pwa-manifest-locales.ts +1 -1
- package/src/registry.ts +18 -0
- package/src/repro-bundle.ts +65 -0
- package/src/repro-errors.ts +47 -0
- package/src/repro-job.ts +72 -0
- package/src/repro-literal.ts +41 -0
- package/src/repro-request.ts +48 -0
- package/src/repro-write.ts +100 -0
- package/src/retain-sweep.ts +27 -0
- package/src/retained-assets-boot.ts +113 -0
- package/src/retained-assets-layout.ts +110 -0
- package/src/retained-assets-read.ts +142 -0
- package/src/retained-assets-sweep.ts +70 -0
- package/src/retained-assets.ts +171 -0
- package/src/revalidated-response.ts +8 -5
- package/src/role-replicator.ts +7 -1
- package/src/role-start-devices.ts +119 -0
- package/src/role-start-types.ts +6 -0
- package/src/role-start.ts +22 -5
- package/src/role-sync.ts +7 -1
- package/src/route-islands.ts +42 -0
- package/src/route-table.ts +2 -1
- package/src/row-security-boot.ts +58 -0
- package/src/row-security-wanted.ts +32 -0
- package/src/runtime-admin.ts +0 -1
- package/src/runtime-assets-site-cache.ts +55 -0
- package/src/runtime-assets-site.ts +136 -0
- package/src/runtime-assets.ts +99 -55
- package/src/runtime-bindings.ts +19 -3
- package/src/runtime-cache.ts +23 -14
- package/src/runtime-isr-outcome.ts +114 -60
- package/src/runtime-purge.ts +14 -2
- package/src/runtime-queue.ts +31 -4
- package/src/runtime-render-document.ts +204 -0
- package/src/runtime-render.ts +48 -198
- package/src/runtime-replica-position.ts +62 -0
- package/src/runtime-replica.ts +18 -3
- package/src/runtime-s3-path-style.ts +17 -0
- package/src/runtime-services.ts +19 -6
- package/src/runtime-statement-bound.ts +22 -0
- package/src/runtime-storage-upload.ts +9 -0
- package/src/runtime-storage.ts +41 -81
- package/src/schema-diff-views.ts +54 -0
- package/src/schema-diff.ts +21 -3
- package/src/schema-drift.ts +28 -9
- package/src/schema-dump-drift.ts +91 -7
- package/src/script-csp.ts +1 -1
- package/src/seo-cards.ts +26 -0
- package/src/seo-routes.ts +22 -8
- package/src/serve-boot.ts +54 -21
- package/src/serve-drain.ts +1 -0
- package/src/serve-env.ts +26 -8
- package/src/serve-retire.ts +12 -6
- package/src/serve-web.ts +42 -4
- package/src/serve.ts +9 -7
- package/src/site-asset-routes.ts +7 -1
- package/src/site-config.ts +4 -0
- package/src/site-llms-pages.ts +106 -0
- package/src/site-llms.ts +64 -0
- package/src/site-seo-write.ts +4 -2
- package/src/site-seo.ts +55 -6
- package/src/source-mtime.ts +50 -0
- package/src/static-capture-errors.ts +41 -0
- package/src/static-capture.ts +56 -0
- package/src/static-cards-serve.ts +39 -0
- package/src/static-cards.ts +93 -0
- package/src/static-document.ts +14 -13
- package/src/static-images.ts +115 -0
- package/src/stream-hole-scope.ts +55 -0
- package/src/style-bundle.ts +1 -1
- package/src/style-routes.ts +3 -2
- package/src/sw-precache-plan.ts +2 -2
- package/src/sw-routes.ts +2 -2
- package/src/templates/action-tests.ts +306 -0
- package/src/templates/action.ts +81 -269
- package/src/templates/backfill.ts +1 -1
- package/src/templates/entity.ts +21 -5
- package/src/templates/github/ci.yml.ts +8 -9
- package/src/templates/guard-bare-error.ts +2 -2
- package/src/templates/guard-error-declared.ts +263 -0
- package/src/templates/guard-native-confirm.ts +132 -0
- package/src/templates/guard-raw-fetch.ts +151 -0
- package/src/templates/guard-raw-interval.ts +121 -0
- package/src/templates/guard-repo-raw-sql.ts +71 -2
- package/src/templates/job.ts +1 -1
- package/src/templates/naming.ts +5 -0
- package/src/templates/pinned/docker-compose.yml +6 -0
- package/src/templates/pinned-images.ts +8 -0
- package/src/templates/policy.ts +85 -2
- package/src/templates/query.ts +70 -13
- package/src/templates/resource-create.ts +3 -1
- package/src/templates/resource-form-island.ts +15 -4
- package/src/templates/resource-page-db.ts +83 -0
- package/src/templates/resource-page.ts +161 -52
- package/src/templates/route.ts +4 -6
- package/src/templates/scaffold-app.ts +3 -0
- package/src/templates/scaffold-auth.ts +2 -1
- package/src/templates/scaffold-bin.ts +5 -52
- package/src/templates/scaffold-claude-agents.ts +19 -19
- package/src/templates/scaffold-claude-commands.ts +28 -31
- package/src/templates/scaffold-claude.ts +26 -23
- package/src/templates/scaffold-container-compose.ts +6 -1
- package/src/templates/scaffold-container.ts +24 -4
- package/src/templates/scaffold-dashboard-bare.ts +3 -3
- package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
- package/src/templates/scaffold-dashboard-example-view.ts +156 -0
- package/src/templates/scaffold-dashboard-example.ts +43 -131
- package/src/templates/scaffold-dashboard-shared.ts +3 -7
- package/src/templates/scaffold-db-client.ts +20 -6
- package/src/templates/scaffold-db-package.ts +5 -14
- package/src/templates/scaffold-docs.ts +73 -57
- package/src/templates/scaffold-domain-package.ts +18 -14
- package/src/templates/scaffold-entries.ts +7 -2
- package/src/templates/scaffold-exercise.ts +37 -0
- package/src/templates/scaffold-guards.ts +13 -1
- package/src/templates/scaffold-helm-alerts.ts +234 -0
- package/src/templates/scaffold-helm-templates.ts +7 -5
- package/src/templates/scaffold-helm.ts +46 -12
- package/src/templates/scaffold-i18n.ts +6 -2
- package/src/templates/scaffold-mcp-endpoint.ts +182 -0
- package/src/templates/scaffold-mcp-json.ts +31 -0
- package/src/templates/scaffold-mcp-package.ts +59 -56
- package/src/templates/scaffold-repo.ts +7 -2
- package/src/templates/scaffold-shell.ts +45 -79
- package/src/templates/scaffold-test-services.ts +13 -5
- package/src/templates/shapes/api-keys.ts +170 -0
- package/src/templates/shapes/api.ts +101 -0
- package/src/templates/shapes/blog-site.ts +340 -0
- package/src/templates/shapes/blog-slice.ts +265 -0
- package/src/templates/shapes/blog.ts +114 -0
- package/src/templates/shapes/dashboard-detail.ts +242 -0
- package/src/templates/shapes/dashboard.ts +53 -0
- package/src/templates/shapes/index.ts +17 -0
- package/src/templates/shapes/overlay.ts +99 -0
- package/src/templates/shapes/shared-edits.ts +46 -0
- package/src/templates/sign-in-action-tests.ts +237 -0
- package/src/templates/sign-in-authenticator.ts +112 -0
- package/src/templates/sign-in-oidc.ts +189 -0
- package/src/templates/sign-in-pages.ts +148 -0
- package/src/templates/sign-in-password.ts +216 -0
- package/src/templates/sign-in-shared.ts +268 -0
- package/src/templates/slice-foundation.ts +70 -15
- package/src/test-batches.ts +1 -1
- package/src/test-database.ts +1 -1
- package/src/test-shards.ts +7 -1
- package/src/test-workers.ts +18 -12
- package/src/tool-gate-names.ts +34 -0
- package/src/upgrade-agents-link.ts +49 -0
- package/src/upgrade-bun-floor.ts +225 -0
- package/src/upgrade-gate.ts +228 -0
- package/src/upgrade-pins.ts +63 -0
- package/src/upgrade-plan.ts +214 -0
- package/src/upgrade-registry.ts +53 -0
- package/src/upgrade-report.ts +139 -0
- package/src/upgrade-root.ts +74 -0
- package/src/verify-budgets-build.ts +118 -0
- package/src/verify-checks.ts +67 -24
- package/src/verify-coverage-run.ts +1 -1
- package/src/verify-e2e-lane.ts +41 -0
- package/src/verify-exercise.ts +64 -0
- package/src/verify-floor.ts +41 -0
- package/src/verify-merge.ts +19 -4
- package/src/verify-role-load.ts +2 -0
- package/src/verify-run.ts +28 -8
- package/src/verify-shard.ts +12 -2
- package/src/verify-skip-ceiling.ts +87 -0
- package/src/verify-step.ts +5 -0
- package/src/verify-tests.ts +25 -5
- package/src/view-drift.ts +55 -0
- package/src/worker-bundle.ts +3 -2
- package/src/workspace-graph.ts +38 -19
- package/src/write-generated.ts +43 -0
- package/src/zone-boot.ts +20 -0
- package/wiki/Actions.md +342 -0
- package/wiki/Admin-Dashboard.md +432 -0
- package/wiki/Agents.md +958 -0
- package/wiki/Auth.md +195 -0
- package/wiki/Bare-VM.md +111 -0
- package/wiki/Batching-And-Preloading.md +136 -0
- package/wiki/Building-Your-Own-Base.md +203 -0
- package/wiki/CI-Parallel-Gate.md +220 -0
- package/wiki/CLI-Reference.md +1894 -0
- package/wiki/Caching-And-Invalidation.md +255 -0
- package/wiki/Client-Data.md +194 -0
- package/wiki/Client-Navigation.md +513 -0
- package/wiki/Compared.md +93 -0
- package/wiki/Configuration.md +868 -0
- package/wiki/Contributing.md +12 -0
- package/wiki/Deploy-Recipes.md +122 -0
- package/wiki/Deployment.md +508 -0
- package/wiki/Entities-And-Migrations.md +1116 -0
- package/wiki/FAQ.md +175 -0
- package/wiki/Feature-Flags.md +65 -0
- package/wiki/Getting-Started.md +221 -0
- package/wiki/Home.md +160 -0
- package/wiki/I18n.md +239 -0
- package/wiki/Installation.md +206 -0
- package/wiki/Interface-Rules.md +339 -0
- package/wiki/Jobs-And-Workflows.md +783 -0
- package/wiki/Known-Gaps.md +185 -0
- package/wiki/MCP-And-AI.md +753 -0
- package/wiki/Mail.md +138 -0
- package/wiki/Migrating-An-Existing-App.md +594 -0
- package/wiki/Migrating-From-Django.md +54 -0
- package/wiki/Migrating-From-Laravel.md +53 -0
- package/wiki/Migrating-From-Node-Drizzle.md +53 -0
- package/wiki/Migrating-From-Rails.md +53 -0
- package/wiki/Migrations-And-Backfills.md +552 -0
- package/wiki/Money.md +338 -0
- package/wiki/N-Plus-One-Detection.md +197 -0
- package/wiki/Notify.md +208 -0
- package/wiki/Observability.md +245 -0
- package/wiki/Outbound-HTTP.md +109 -0
- package/wiki/PWA-And-Offline.md +343 -0
- package/wiki/Policies-And-Authz.md +251 -0
- package/wiki/Project-Layout.md +150 -0
- package/wiki/Queries-And-Live-Queries.md +429 -0
- package/wiki/Realtime.md +734 -0
- package/wiki/Resource-Management.md +121 -0
- package/wiki/Routes-And-Render-Modes.md +521 -0
- package/wiki/SEO.md +288 -0
- package/wiki/Scheduled-Tasks.md +175 -0
- package/wiki/Scraping.md +493 -0
- package/wiki/Shapes.md +63 -0
- package/wiki/Stability.md +97 -0
- package/wiki/Static-Assets.md +166 -0
- package/wiki/Storage-And-Uploads.md +404 -0
- package/wiki/Testing.md +1314 -0
- package/wiki/The-Eight-Primitives.md +247 -0
- package/wiki/Theming.md +362 -0
- package/wiki/Timezones-And-Dates.md +194 -0
- package/wiki/Troubleshooting.md +163 -0
- package/wiki/Tutorial-01-First-App.md +289 -0
- package/wiki/Tutorial-02-First-Feature.md +339 -0
- package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
- package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
- package/wiki/Tutorial-05-Deploy-Free.md +231 -0
- package/wiki/Tutorial-06-Growing-Up.md +158 -0
- package/wiki/UI-Components.md +156 -0
- package/wiki/Upgrading.md +3667 -0
- package/src/stored-object-headers.ts +0 -43
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// One JSON Schema (OpenAPI 3.0 or 3.1) to a `ClientType`: `$ref`, objects, arrays, enums, `const`,
|
|
2
|
+
// `oneOf`/`anyOf`, `allOf`, nullable. A keyword that would change a value's shape and is not one of
|
|
3
|
+
// those is refused with its pointer; a keyword that only narrows a value is not a shape and is read
|
|
4
|
+
// past. Named components are compiled on first reach, so a recursive `$ref` is a name, not a loop.
|
|
5
|
+
|
|
6
|
+
import type { ClientField, ClientObject, ClientType } from './openapi-compile-ir';
|
|
7
|
+
import { objectAt, own, pointerJoin, unsupported } from './openapi-refuse';
|
|
8
|
+
|
|
9
|
+
const SHAPE_KEYS = new Set([
|
|
10
|
+
'type',
|
|
11
|
+
'properties',
|
|
12
|
+
'required',
|
|
13
|
+
'additionalProperties',
|
|
14
|
+
'items',
|
|
15
|
+
'enum',
|
|
16
|
+
'const',
|
|
17
|
+
'oneOf',
|
|
18
|
+
'anyOf',
|
|
19
|
+
'allOf',
|
|
20
|
+
'$ref',
|
|
21
|
+
'nullable',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/** Annotations and value narrowings: true of a value, never a different shape for it. */
|
|
25
|
+
const NARROWING_KEYS = new Set([
|
|
26
|
+
'$schema',
|
|
27
|
+
'$id',
|
|
28
|
+
'$comment',
|
|
29
|
+
'$defs',
|
|
30
|
+
'title',
|
|
31
|
+
'description',
|
|
32
|
+
'default',
|
|
33
|
+
'example',
|
|
34
|
+
'examples',
|
|
35
|
+
'deprecated',
|
|
36
|
+
'readOnly',
|
|
37
|
+
'writeOnly',
|
|
38
|
+
'format',
|
|
39
|
+
'minLength',
|
|
40
|
+
'maxLength',
|
|
41
|
+
'pattern',
|
|
42
|
+
'minimum',
|
|
43
|
+
'maximum',
|
|
44
|
+
'exclusiveMinimum',
|
|
45
|
+
'exclusiveMaximum',
|
|
46
|
+
'multipleOf',
|
|
47
|
+
'minItems',
|
|
48
|
+
'maxItems',
|
|
49
|
+
'uniqueItems',
|
|
50
|
+
'minProperties',
|
|
51
|
+
'maxProperties',
|
|
52
|
+
'propertyNames',
|
|
53
|
+
'discriminator',
|
|
54
|
+
'externalDocs',
|
|
55
|
+
'xml',
|
|
56
|
+
'contentMediaType',
|
|
57
|
+
'contentEncoding',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const COMPONENT_REF = '#/components/schemas/';
|
|
61
|
+
const UNKNOWN: ClientType = { kind: 'unknown' };
|
|
62
|
+
const NULL: ClientType = { kind: 'null' };
|
|
63
|
+
|
|
64
|
+
/** The component table the compiler fills as operations reach it. */
|
|
65
|
+
export interface SchemaScope {
|
|
66
|
+
readonly components: Readonly<Record<string, unknown>>;
|
|
67
|
+
/** Component name → its compiled type; `null` while that component is being compiled. */
|
|
68
|
+
readonly compiled: Map<string, ClientType | null>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `review-draft.input` → `ReviewDraftInput`: a TypeScript identifier, case kept inside a word. */
|
|
72
|
+
export function typeName(name: string): string {
|
|
73
|
+
const words = name.split(/[^A-Za-z0-9]+/).filter((word) => word.length > 0);
|
|
74
|
+
const joined = words.map((word) => `${word[0]?.toUpperCase() ?? ''}${word.slice(1)}`).join('');
|
|
75
|
+
return /^[0-9]/.test(joined) || joined === '' ? `T${joined}` : joined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A union with nested unions flattened and duplicates dropped; one member is that member. */
|
|
79
|
+
export function unionOf(members: readonly ClientType[]): ClientType {
|
|
80
|
+
const seen = new Map<string, ClientType>();
|
|
81
|
+
for (const member of members.flatMap((m) => (m.kind === 'union' ? m.members : [m]))) {
|
|
82
|
+
seen.set(JSON.stringify(member), member);
|
|
83
|
+
}
|
|
84
|
+
const unique = [...seen.values()];
|
|
85
|
+
if (unique.some((member) => member.kind === 'unknown')) return UNKNOWN;
|
|
86
|
+
return unique.length === 1 ? (unique[0] as ClientType) : { kind: 'union', members: unique };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function reachComponent(scope: SchemaScope, ref: string, pointer: string): ClientType {
|
|
90
|
+
if (!ref.startsWith(COMPONENT_REF)) return refuse(pointer, `the $ref ${ref}`);
|
|
91
|
+
const name = ref.slice(COMPONENT_REF.length).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
92
|
+
const target = own(scope.components, name);
|
|
93
|
+
if (target === undefined) return refuse(pointer, `a $ref to a missing component (${ref})`);
|
|
94
|
+
if (!scope.compiled.has(name)) {
|
|
95
|
+
scope.compiled.set(name, null);
|
|
96
|
+
scope.compiled.set(name, compileSchema(scope, target, `${COMPONENT_REF}${name}`));
|
|
97
|
+
}
|
|
98
|
+
return { kind: 'ref', name };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function refuse(pointer: string, construct: string): never {
|
|
102
|
+
throw unsupported(pointer, construct);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A type as an object to merge into, a `$ref` followed; anything else is refused. */
|
|
106
|
+
function asObject(scope: SchemaScope, type: ClientType, pointer: string): ClientObject {
|
|
107
|
+
if (type.kind === 'object') return type;
|
|
108
|
+
if (type.kind === 'ref') {
|
|
109
|
+
const target = scope.compiled.get(type.name);
|
|
110
|
+
if (target === null) return refuse(pointer, 'an allOf through a recursive $ref');
|
|
111
|
+
if (target !== undefined) return asObject(scope, target, pointer);
|
|
112
|
+
}
|
|
113
|
+
return refuse(pointer, `an allOf or oneOf member that is not an object (${type.kind})`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Field by field, the later declaration winning: an `allOf` narrows what it extends. */
|
|
117
|
+
function mergeObjects(objects: readonly ClientObject[]): ClientObject {
|
|
118
|
+
const fields = new Map<string, ClientField>();
|
|
119
|
+
for (const object of objects) {
|
|
120
|
+
for (const field of object.fields) {
|
|
121
|
+
const before = fields.get(field.name);
|
|
122
|
+
fields.set(field.name, { ...field, required: field.required || before?.required === true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { kind: 'object', fields: [...fields.values()] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function objectOf(
|
|
129
|
+
scope: SchemaScope,
|
|
130
|
+
schema: Readonly<Record<string, unknown>>,
|
|
131
|
+
pointer: string,
|
|
132
|
+
): ClientType {
|
|
133
|
+
const properties = objectAt(schema['properties']) ?? {};
|
|
134
|
+
const required = new Set(Array.isArray(schema['required']) ? schema['required'].map(String) : []);
|
|
135
|
+
const names = [...new Set([...Object.keys(properties), ...required])];
|
|
136
|
+
const fields = names.map((name) => ({
|
|
137
|
+
name,
|
|
138
|
+
required: required.has(name),
|
|
139
|
+
type: Object.hasOwn(properties, name)
|
|
140
|
+
? compileSchema(
|
|
141
|
+
scope,
|
|
142
|
+
properties[name],
|
|
143
|
+
pointerJoin(pointerJoin(pointer, 'properties'), name),
|
|
144
|
+
)
|
|
145
|
+
: UNKNOWN,
|
|
146
|
+
}));
|
|
147
|
+
const extra = schema['additionalProperties'];
|
|
148
|
+
if (extra !== undefined && typeof extra !== 'boolean') {
|
|
149
|
+
if (fields.length > 0) return refuse(pointer, 'properties beside a typed additionalProperties');
|
|
150
|
+
return {
|
|
151
|
+
kind: 'record',
|
|
152
|
+
values: compileSchema(scope, extra, pointerJoin(pointer, 'additionalProperties')),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (fields.length === 0 && extra !== false) return { kind: 'record', values: UNKNOWN };
|
|
156
|
+
return { kind: 'object', fields };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function typed(
|
|
160
|
+
scope: SchemaScope,
|
|
161
|
+
schema: Readonly<Record<string, unknown>>,
|
|
162
|
+
type: unknown,
|
|
163
|
+
pointer: string,
|
|
164
|
+
): ClientType {
|
|
165
|
+
switch (type) {
|
|
166
|
+
case 'string':
|
|
167
|
+
return { kind: 'string' };
|
|
168
|
+
case 'integer':
|
|
169
|
+
case 'number':
|
|
170
|
+
return { kind: 'number', integer: type === 'integer' };
|
|
171
|
+
case 'boolean':
|
|
172
|
+
return { kind: 'boolean' };
|
|
173
|
+
case 'null':
|
|
174
|
+
return NULL;
|
|
175
|
+
case 'array':
|
|
176
|
+
return {
|
|
177
|
+
kind: 'array',
|
|
178
|
+
items:
|
|
179
|
+
schema['items'] === undefined
|
|
180
|
+
? UNKNOWN
|
|
181
|
+
: compileSchema(scope, schema['items'], pointerJoin(pointer, 'items')),
|
|
182
|
+
};
|
|
183
|
+
case 'object':
|
|
184
|
+
return objectOf(scope, schema, pointer);
|
|
185
|
+
default:
|
|
186
|
+
return refuse(pointerJoin(pointer, 'type'), `the type ${JSON.stringify(type)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function literalOf(value: unknown, pointer: string): ClientType {
|
|
191
|
+
if (value === null) return NULL;
|
|
192
|
+
if (typeof value === 'string' || typeof value === 'boolean') return { kind: 'literal', value };
|
|
193
|
+
if (typeof value === 'number' && Number.isFinite(value)) return { kind: 'literal', value };
|
|
194
|
+
return refuse(pointer, 'an enum or const value that is not a string, number, boolean or null');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** What a schema with no combinator says: its `type` (or the type its keywords imply). */
|
|
198
|
+
function shapeOf(
|
|
199
|
+
scope: SchemaScope,
|
|
200
|
+
schema: Readonly<Record<string, unknown>>,
|
|
201
|
+
pointer: string,
|
|
202
|
+
): ClientType {
|
|
203
|
+
if (Array.isArray(schema['enum'])) {
|
|
204
|
+
return unionOf(schema['enum'].map((value, i) => literalOf(value, pointerJoin(pointer, i))));
|
|
205
|
+
}
|
|
206
|
+
if (Object.hasOwn(schema, 'const'))
|
|
207
|
+
return literalOf(schema['const'], pointerJoin(pointer, 'const'));
|
|
208
|
+
if (Array.isArray(schema['type'])) {
|
|
209
|
+
return unionOf(schema['type'].map((type) => typed(scope, schema, type, pointer)));
|
|
210
|
+
}
|
|
211
|
+
if (schema['type'] !== undefined) return typed(scope, schema, schema['type'], pointer);
|
|
212
|
+
if (schema['properties'] !== undefined || schema['additionalProperties'] !== undefined) {
|
|
213
|
+
return objectOf(scope, schema, pointer);
|
|
214
|
+
}
|
|
215
|
+
if (schema['required'] !== undefined) return objectOf(scope, schema, pointer);
|
|
216
|
+
if (schema['items'] !== undefined) return typed(scope, schema, 'array', pointer);
|
|
217
|
+
return UNKNOWN;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const hasShape = (schema: Readonly<Record<string, unknown>>): boolean =>
|
|
221
|
+
['type', 'properties', 'required', 'additionalProperties', 'items', 'enum', 'const'].some((key) =>
|
|
222
|
+
Object.hasOwn(schema, key),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const without = (
|
|
226
|
+
schema: Readonly<Record<string, unknown>>,
|
|
227
|
+
keys: readonly string[],
|
|
228
|
+
): Readonly<Record<string, unknown>> =>
|
|
229
|
+
Object.fromEntries(Object.entries(schema).filter(([key]) => !keys.includes(key)));
|
|
230
|
+
|
|
231
|
+
function combined(
|
|
232
|
+
scope: SchemaScope,
|
|
233
|
+
schema: Readonly<Record<string, unknown>>,
|
|
234
|
+
pointer: string,
|
|
235
|
+
): ClientType {
|
|
236
|
+
if (typeof schema['$ref'] === 'string') {
|
|
237
|
+
const target = reachComponent(scope, schema['$ref'], pointerJoin(pointer, '$ref'));
|
|
238
|
+
const rest = without(schema, ['$ref']);
|
|
239
|
+
if (!hasShape(rest) && rest['allOf'] === undefined) return target;
|
|
240
|
+
const merged = mergeObjects([
|
|
241
|
+
asObject(scope, target, pointer),
|
|
242
|
+
asObject(scope, compileSchema(scope, rest, pointer), pointer),
|
|
243
|
+
]);
|
|
244
|
+
return merged;
|
|
245
|
+
}
|
|
246
|
+
if (Array.isArray(schema['allOf'])) {
|
|
247
|
+
const members = schema['allOf'].map((member, i) =>
|
|
248
|
+
compileSchema(scope, member, pointerJoin(pointerJoin(pointer, 'allOf'), i)),
|
|
249
|
+
);
|
|
250
|
+
const rest = without(schema, ['allOf']);
|
|
251
|
+
const all = hasShape(rest) ? [...members, compileSchema(scope, rest, pointer)] : members;
|
|
252
|
+
if (all.length === 1) return all[0] as ClientType;
|
|
253
|
+
return mergeObjects(all.map((member) => asObject(scope, member, pointer)));
|
|
254
|
+
}
|
|
255
|
+
const key = Array.isArray(schema['oneOf'])
|
|
256
|
+
? 'oneOf'
|
|
257
|
+
: Array.isArray(schema['anyOf'])
|
|
258
|
+
? 'anyOf'
|
|
259
|
+
: '';
|
|
260
|
+
if (key !== '') {
|
|
261
|
+
const list = schema[key] as readonly unknown[];
|
|
262
|
+
const members = list.map((member, i) =>
|
|
263
|
+
compileSchema(scope, member, pointerJoin(pointerJoin(pointer, key), i)),
|
|
264
|
+
);
|
|
265
|
+
const rest = without(schema, ['oneOf', 'anyOf']);
|
|
266
|
+
if (!hasShape(rest)) return unionOf(members);
|
|
267
|
+
const base = asObject(scope, shapeOf(scope, rest, pointer), pointer);
|
|
268
|
+
return unionOf(members.map((member) => mergeObjects([base, asObject(scope, member, pointer)])));
|
|
269
|
+
}
|
|
270
|
+
return shapeOf(scope, schema, pointer);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The one entry point: a schema at `pointer` to its `ClientType`, or a refusal naming where. */
|
|
274
|
+
export function compileSchema(scope: SchemaScope, schema: unknown, pointer: string): ClientType {
|
|
275
|
+
if (schema === true) return UNKNOWN;
|
|
276
|
+
const node = objectAt(schema);
|
|
277
|
+
if (node === undefined) return refuse(pointer, 'a schema that is not an object');
|
|
278
|
+
for (const key of Object.keys(node)) {
|
|
279
|
+
if (SHAPE_KEYS.has(key) || NARROWING_KEYS.has(key) || key.startsWith('x-')) continue;
|
|
280
|
+
refuse(pointerJoin(pointer, key), `the keyword ${key}`);
|
|
281
|
+
}
|
|
282
|
+
const type = combined(scope, node, pointer);
|
|
283
|
+
return node['nullable'] === true ? unionOf([type, NULL]) : type;
|
|
284
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// An OpenAPI 3.0/3.1 document to the neutral operation list both client emitters read — the ONLY
|
|
2
|
+
// reader of OpenAPI in the client path. Operations are compiled first and components only as an
|
|
3
|
+
// operation reaches them, so `--only` around a construct the compiler refuses really leaves it out.
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ClientBody,
|
|
7
|
+
ClientMethod,
|
|
8
|
+
ClientOperation,
|
|
9
|
+
ClientParameter,
|
|
10
|
+
ClientType,
|
|
11
|
+
CompiledDocument,
|
|
12
|
+
} from './openapi-compile-ir';
|
|
13
|
+
import type { SchemaScope } from './openapi-compile-schema';
|
|
14
|
+
import { compileSchema, typeName } from './openapi-compile-schema';
|
|
15
|
+
import { ClientSpecInvalidError, objectAt, own, pointerJoin, unsupported } from './openapi-refuse';
|
|
16
|
+
|
|
17
|
+
type Json = Readonly<Record<string, unknown>>;
|
|
18
|
+
|
|
19
|
+
const METHODS: ReadonlyMap<string, ClientMethod> = new Map([
|
|
20
|
+
['get', 'GET'],
|
|
21
|
+
['put', 'PUT'],
|
|
22
|
+
['post', 'POST'],
|
|
23
|
+
['delete', 'DELETE'],
|
|
24
|
+
['options', 'OPTIONS'],
|
|
25
|
+
['head', 'HEAD'],
|
|
26
|
+
['patch', 'PATCH'],
|
|
27
|
+
]);
|
|
28
|
+
const PATH_ITEM_KEYS = new Set(['summary', 'description', 'servers', 'parameters']);
|
|
29
|
+
const JSON_MEDIA = /^application\/(?:[\w.-]+\+)?json$/;
|
|
30
|
+
|
|
31
|
+
export interface CompileOptions {
|
|
32
|
+
/** Where the document came from, for a refusal's cause. */
|
|
33
|
+
readonly source: string;
|
|
34
|
+
/** Keep only operations carrying one of these tags. */
|
|
35
|
+
readonly only?: readonly string[] | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `invoices.get` → `invoicesGet`: a TypeScript identifier, first letter lower. */
|
|
39
|
+
export function operationName(id: string): string {
|
|
40
|
+
const pascal = typeName(id);
|
|
41
|
+
return /^T[0-9]/.test(pascal) ? pascal : `${pascal[0]?.toLowerCase() ?? ''}${pascal.slice(1)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** No `operationId`: method then path, `{id}` read as `ById` — still a pure function of the doc. */
|
|
45
|
+
const derivedName = (method: string, path: string): string =>
|
|
46
|
+
operationName(
|
|
47
|
+
[method, ...path.split('/').map((s) => s.replace(/^\{(.+)\}$/, 'by-$1'))].join('-'),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/** A `$ref` into `#/components/<section>/` followed once; anything else is the value itself. */
|
|
51
|
+
function deref(document: Json, value: unknown, pointer: string): { node: Json; at: string } {
|
|
52
|
+
const node = objectAt(value);
|
|
53
|
+
if (node === undefined) throw unsupported(pointer, 'a value that is not an object');
|
|
54
|
+
if (typeof node['$ref'] !== 'string') return { node, at: pointer };
|
|
55
|
+
const segments = node['$ref'].startsWith('#/components/') ? node['$ref'].slice(2).split('/') : [];
|
|
56
|
+
let target: unknown = document;
|
|
57
|
+
for (const raw of segments) {
|
|
58
|
+
const record = objectAt(target);
|
|
59
|
+
target =
|
|
60
|
+
record === undefined ? undefined : own(record, raw.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
61
|
+
}
|
|
62
|
+
const resolved = objectAt(target);
|
|
63
|
+
if (segments.length !== 3 || resolved === undefined) {
|
|
64
|
+
throw unsupported(pointerJoin(pointer, '$ref'), `the $ref ${node['$ref']}`);
|
|
65
|
+
}
|
|
66
|
+
return { node: resolved, at: node['$ref'] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The JSON media entry of a `content` map; `undefined` when there is none at all. */
|
|
70
|
+
function jsonSchemaOf(
|
|
71
|
+
content: unknown,
|
|
72
|
+
pointer: string,
|
|
73
|
+
): { schema: unknown; at: string } | undefined {
|
|
74
|
+
const media = objectAt(content);
|
|
75
|
+
if (media === undefined || Object.keys(media).length === 0) return undefined;
|
|
76
|
+
const key = Object.keys(media).find((type) => JSON_MEDIA.test(type));
|
|
77
|
+
if (key === undefined) {
|
|
78
|
+
throw unsupported(pointer, `a body that is not JSON (${Object.keys(media).join(', ')})`);
|
|
79
|
+
}
|
|
80
|
+
const entry = objectAt(media[key]) ?? {};
|
|
81
|
+
return { schema: entry['schema'] ?? true, at: pointerJoin(pointerJoin(pointer, key), 'schema') };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parameterOf(
|
|
85
|
+
document: Json,
|
|
86
|
+
scope: SchemaScope,
|
|
87
|
+
raw: unknown,
|
|
88
|
+
pointer: string,
|
|
89
|
+
): ClientParameter {
|
|
90
|
+
const { node, at } = deref(document, raw, pointer);
|
|
91
|
+
const where = node['in'];
|
|
92
|
+
if (where !== 'path' && where !== 'query' && where !== 'header') {
|
|
93
|
+
throw unsupported(pointerJoin(at, 'in'), `a parameter in ${String(where)}`);
|
|
94
|
+
}
|
|
95
|
+
if (node['schema'] === undefined)
|
|
96
|
+
throw unsupported(at, 'a parameter with content instead of schema');
|
|
97
|
+
const type = compileSchema(scope, node['schema'], pointerJoin(at, 'schema'));
|
|
98
|
+
if (where === 'query' && (type.kind === 'object' || type.kind === 'record')) {
|
|
99
|
+
throw unsupported(pointerJoin(at, 'schema'), 'an object-valued query parameter');
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
name: String(node['name']),
|
|
103
|
+
in: where,
|
|
104
|
+
required: node['required'] === true || where === 'path',
|
|
105
|
+
type,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isObjectBody = (scope: SchemaScope, type: ClientType): boolean => {
|
|
110
|
+
if (type.kind === 'object') return true;
|
|
111
|
+
const target = type.kind === 'ref' ? scope.compiled.get(type.name) : undefined;
|
|
112
|
+
return target !== undefined && target !== null && target.kind === 'object';
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function bodyOf(
|
|
116
|
+
document: Json,
|
|
117
|
+
scope: SchemaScope,
|
|
118
|
+
raw: unknown,
|
|
119
|
+
pointer: string,
|
|
120
|
+
): ClientBody | undefined {
|
|
121
|
+
if (raw === undefined) return undefined;
|
|
122
|
+
const { node, at } = deref(document, raw, pointer);
|
|
123
|
+
const found = jsonSchemaOf(node['content'], pointerJoin(at, 'content'));
|
|
124
|
+
if (found === undefined) return undefined;
|
|
125
|
+
const type = compileSchema(scope, found.schema, found.at);
|
|
126
|
+
const required = node['required'] === true;
|
|
127
|
+
return isObjectBody(scope, type)
|
|
128
|
+
? { mode: 'fields', type, required }
|
|
129
|
+
: { mode: 'value', type, required };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function responseOf(
|
|
133
|
+
document: Json,
|
|
134
|
+
scope: SchemaScope,
|
|
135
|
+
responses: Json,
|
|
136
|
+
pointer: string,
|
|
137
|
+
): ClientType | undefined {
|
|
138
|
+
const success = Object.keys(responses)
|
|
139
|
+
.filter((status) => /^2(?:\d\d|XX)$/.test(status))
|
|
140
|
+
.sort()[0];
|
|
141
|
+
if (success === undefined) return undefined;
|
|
142
|
+
const { node, at } = deref(document, responses[success], pointerJoin(pointer, success));
|
|
143
|
+
const found = jsonSchemaOf(node['content'], pointerJoin(at, 'content'));
|
|
144
|
+
return found === undefined ? undefined : compileSchema(scope, found.schema, found.at);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function refusalsOf(responses: Json): readonly string[] {
|
|
148
|
+
const codes = new Set<string>();
|
|
149
|
+
for (const [status, value] of Object.entries(responses)) {
|
|
150
|
+
const declared = /^2/.test(status) ? undefined : objectAt(value)?.['x-ultimate-errors'];
|
|
151
|
+
if (Array.isArray(declared)) for (const code of declared) codes.add(String(code));
|
|
152
|
+
}
|
|
153
|
+
return [...codes].sort();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function operationOf(
|
|
157
|
+
document: Json,
|
|
158
|
+
scope: SchemaScope,
|
|
159
|
+
input: {
|
|
160
|
+
path: string;
|
|
161
|
+
method: string;
|
|
162
|
+
op: Json;
|
|
163
|
+
shared: readonly unknown[];
|
|
164
|
+
itemPointer: string;
|
|
165
|
+
pointer: string;
|
|
166
|
+
},
|
|
167
|
+
): ClientOperation {
|
|
168
|
+
const { op, pointer } = input;
|
|
169
|
+
const byKey = new Map<string, ClientParameter>();
|
|
170
|
+
// Path-item parameters first, so an operation's own parameter of the same name and place wins.
|
|
171
|
+
const lists: readonly (readonly [readonly unknown[], string])[] = [
|
|
172
|
+
[input.shared, pointerJoin(input.itemPointer, 'parameters')],
|
|
173
|
+
[Array.isArray(op['parameters']) ? op['parameters'] : [], pointerJoin(pointer, 'parameters')],
|
|
174
|
+
];
|
|
175
|
+
for (const [list, at] of lists) {
|
|
176
|
+
list.forEach((raw, i) => {
|
|
177
|
+
const parameter = parameterOf(document, scope, raw, pointerJoin(at, i));
|
|
178
|
+
byKey.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const parameters = [...byKey.values()];
|
|
182
|
+
const body = bodyOf(document, scope, op['requestBody'], pointerJoin(pointer, 'requestBody'));
|
|
183
|
+
if (body?.mode === 'value' && parameters.some((p) => p.name === 'body')) {
|
|
184
|
+
throw unsupported(
|
|
185
|
+
pointerJoin(pointer, 'parameters'),
|
|
186
|
+
'a parameter named body beside a non-object body',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const responses = objectAt(op['responses']) ?? {};
|
|
190
|
+
return {
|
|
191
|
+
name:
|
|
192
|
+
typeof op['operationId'] === 'string'
|
|
193
|
+
? operationName(op['operationId'])
|
|
194
|
+
: derivedName(input.method, input.path),
|
|
195
|
+
method: METHODS.get(input.method) as ClientMethod,
|
|
196
|
+
path: input.path,
|
|
197
|
+
tags: Array.isArray(op['tags']) ? op['tags'].map(String) : [],
|
|
198
|
+
summary: typeof op['summary'] === 'string' ? op['summary'] : undefined,
|
|
199
|
+
parameters,
|
|
200
|
+
body,
|
|
201
|
+
response: responseOf(document, scope, responses, pointerJoin(pointer, 'responses')),
|
|
202
|
+
refusals: refusalsOf(responses),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function checkedDocument(document: unknown, source: string): Json {
|
|
207
|
+
const root = objectAt(document);
|
|
208
|
+
if (root === undefined)
|
|
209
|
+
throw new ClientSpecInvalidError({ source, reason: 'it is not a JSON object' });
|
|
210
|
+
const version = root['openapi'];
|
|
211
|
+
if (typeof version !== 'string' || !/^3\.[01]\.\d+/.test(version)) {
|
|
212
|
+
throw new ClientSpecInvalidError({ source, reason: 'its openapi field is not 3.0.x or 3.1.x' });
|
|
213
|
+
}
|
|
214
|
+
if (objectAt(root['paths']) === undefined) {
|
|
215
|
+
throw new ClientSpecInvalidError({ source, reason: 'it has no paths object' });
|
|
216
|
+
}
|
|
217
|
+
return root;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The whole document, in a deterministic order: operations by name, types by component name. */
|
|
221
|
+
export function compileOpenApi(document: unknown, options: CompileOptions): CompiledDocument {
|
|
222
|
+
const root = checkedDocument(document, options.source);
|
|
223
|
+
const scope: SchemaScope = {
|
|
224
|
+
components: objectAt(objectAt(root['components'])?.['schemas']) ?? {},
|
|
225
|
+
compiled: new Map(),
|
|
226
|
+
};
|
|
227
|
+
const only = options.only === undefined ? undefined : new Set(options.only);
|
|
228
|
+
const operations: ClientOperation[] = [];
|
|
229
|
+
const paths = objectAt(root['paths']) ?? {};
|
|
230
|
+
for (const path of Object.keys(paths).sort()) {
|
|
231
|
+
const itemPointer = pointerJoin('#/paths', path);
|
|
232
|
+
const item = objectAt(paths[path]) ?? {};
|
|
233
|
+
const shared = Array.isArray(item['parameters']) ? item['parameters'] : [];
|
|
234
|
+
for (const [key, value] of Object.entries(item)) {
|
|
235
|
+
if (PATH_ITEM_KEYS.has(key) || key.startsWith('x-')) continue;
|
|
236
|
+
if (!METHODS.has(key))
|
|
237
|
+
throw unsupported(pointerJoin(itemPointer, key), `the path item key ${key}`);
|
|
238
|
+
const op = objectAt(value) ?? {};
|
|
239
|
+
const tags = Array.isArray(op['tags']) ? op['tags'].map(String) : [];
|
|
240
|
+
if (only !== undefined && !tags.some((tag) => only.has(tag))) continue;
|
|
241
|
+
const pointer = pointerJoin(itemPointer, key);
|
|
242
|
+
const compiled = operationOf(root, scope, {
|
|
243
|
+
path,
|
|
244
|
+
method: key,
|
|
245
|
+
op,
|
|
246
|
+
shared,
|
|
247
|
+
itemPointer,
|
|
248
|
+
pointer,
|
|
249
|
+
});
|
|
250
|
+
if (operations.some((other) => other.name === compiled.name)) {
|
|
251
|
+
throw unsupported(pointer, `a second operation named ${compiled.name}`);
|
|
252
|
+
}
|
|
253
|
+
operations.push(compiled);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const types = new Map<string, ClientType>();
|
|
257
|
+
const spelled = new Map<string, string>();
|
|
258
|
+
for (const name of [...scope.compiled.keys()].sort()) {
|
|
259
|
+
const clash = spelled.get(typeName(name));
|
|
260
|
+
if (clash !== undefined) {
|
|
261
|
+
throw unsupported(`#/components/schemas/${name}`, `a component spelled like ${clash}`);
|
|
262
|
+
}
|
|
263
|
+
spelled.set(typeName(name), name);
|
|
264
|
+
types.set(name, scope.compiled.get(name) as ClientType);
|
|
265
|
+
}
|
|
266
|
+
const info = objectAt(root['info']) ?? {};
|
|
267
|
+
return {
|
|
268
|
+
title: typeof info['title'] === 'string' ? info['title'] : options.source,
|
|
269
|
+
version: typeof info['version'] === 'string' ? info['version'] : '0.0.0',
|
|
270
|
+
types,
|
|
271
|
+
operations: operations.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The OpenAPI compiler's refusals: a document that is not OpenAPI 3.0/3.1, and a construct it will
|
|
2
|
+
// not read, each naming the JSON pointer it stopped at. A construct is refused, never typed
|
|
3
|
+
// `unknown` in silence. Codes and titles are registered in `error-codes.ts`.
|
|
4
|
+
import { renderCauseValue, UltimateError } from '@ultimat3/core';
|
|
5
|
+
|
|
6
|
+
/** RFC 6901: `~` and `/` escaped, so a path key such as `/invoices/{id}` stays one segment. */
|
|
7
|
+
export const pointerJoin = (pointer: string, key: string | number): string =>
|
|
8
|
+
`${pointer}/${String(key).replace(/~/g, '~0').replace(/\//g, '~1')}`;
|
|
9
|
+
|
|
10
|
+
/** `#/paths/~1invoices/get` for a reader: the pointer as written in the document. */
|
|
11
|
+
export class ClientSpecUnsupportedError extends UltimateError {
|
|
12
|
+
constructor(input: { readonly pointer: string; readonly construct: string }) {
|
|
13
|
+
super({
|
|
14
|
+
code: 'X_CLIENT_SPEC_UNSUPPORTED',
|
|
15
|
+
cause: `the document uses ${input.construct} at ${renderCauseValue(input.pointer)}, which the client compiler does not read`,
|
|
16
|
+
fix: 'x help client --json # then rerun with --only naming tags that leave that operation out, or pin a corrected source.json',
|
|
17
|
+
meta: { pointer: input.pointer, construct: input.construct },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ClientSpecInvalidError extends UltimateError {
|
|
23
|
+
constructor(input: { readonly source: string; readonly reason: string }) {
|
|
24
|
+
super({
|
|
25
|
+
code: 'X_CLIENT_SPEC_INVALID',
|
|
26
|
+
cause: `${renderCauseValue(input.source)} is not an OpenAPI 3.0 or 3.1 document: ${input.reason}`,
|
|
27
|
+
fix: 'x help client --json # then point --from at the OpenAPI 3.0 or 3.1 document itself',
|
|
28
|
+
meta: { source: input.source },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const unsupported = (pointer: string, construct: string): ClientSpecUnsupportedError =>
|
|
34
|
+
new ClientSpecUnsupportedError({ pointer, construct });
|
|
35
|
+
|
|
36
|
+
/** A plain JSON object, or `undefined` — the one narrowing every reader of the document uses. */
|
|
37
|
+
export function objectAt(value: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
38
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
39
|
+
? (value as Readonly<Record<string, unknown>>)
|
|
40
|
+
: undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `value[key]` for an OWN key only: a component named `constructor` is a name, not a prototype. */
|
|
44
|
+
export const own = (value: Readonly<Record<string, unknown>>, key: string): unknown =>
|
|
45
|
+
Object.hasOwn(value, key) ? value[key] : undefined;
|
package/src/otlp-export.ts
CHANGED
|
@@ -30,7 +30,8 @@ export const METRIC_EXPORT_INTERVAL_MS = 60_000;
|
|
|
30
30
|
*
|
|
31
31
|
* Both are registered with `onShutdown(..., { phase: 'close' })`: the last spans of a drain are
|
|
32
32
|
* the ones that explain the drain, and a process that exits with a full queue loses exactly the
|
|
33
|
-
* window an operator went looking for.
|
|
33
|
+
* window an operator went looking for. That phase runs last-registered first, so the boot calls this
|
|
34
|
+
* BEFORE anything whose teardown should be seen: everything registered after it flushes into it.
|
|
34
35
|
*
|
|
35
36
|
* The release UNINSTALLS what it installed, per signal. `configureTelemetry`/`configureMetrics`
|
|
36
37
|
* merge into process-global state, so stopping the timer and dropping the drain hooks left the
|