-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add sanity api command for direct HTTP API access
#1561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+2,917
−0
Merged
Changes from 12 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
ba507e1
feat: add `sanity api` command for direct HTTP API access
claude 3cb98bb
chore: update auto-generated changeset for PR #1561
squiggler-app[bot] 03962be
feat: regenerate api routing manifest on every build
claude ad82644
fix: preserve binary api request bodies and strip query markers from …
claude 6c809f8
docs: sync generated README with command help on main
claude fa25a25
fix: stabilize equal-score route tie-breaks and document raw body con…
claude 44544c4
fix: require https for full URL endpoints and reject bare-version paths
claude b4f6f04
fix: build field and query containers without object prototypes
claude 8f33b2b
fix: require a literal segment match in route patterns
claude c4db5ba
fix: keep matched spec version when forcing a host
claude 43551fc
fix: align sanity api with gh api semantics and address review feedback
claude cba4ac8
docs: regenerate CLI README from current command help
claude 6f86b7b
fix: require request paths to fully consume route patterns
claude 6efc7ad
fix: resolve datasets/{name} placeholders and case-insensitive header…
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| <!-- auto-generated --> | ||
| --- | ||
| '@sanity/cli': minor | ||
| --- | ||
|
|
||
| feat: add `sanity api` command for direct HTTP API access |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| /** | ||
| * API Routing Manifest Generator | ||
| * | ||
| * Fetches the published OpenAPI specification index (the same source as | ||
| * `sanity openapi list|get`) and distills it into the routing manifest used | ||
| * by `sanity api` to decide which host serves a request path and which API | ||
| * version to default to. | ||
| * | ||
| * The manifest is generated - not hand-maintained - so the set of APIs | ||
| * reachable through `sanity api` follows the published specs. It runs on | ||
| * every build (prebuild hook) and fails the build when the specs can't be | ||
| * fetched or the distilled manifest would be empty, so a release can never | ||
| * ship with a missing or empty endpoint list. | ||
| * | ||
| * Regenerate: tsx scripts/generate-api-routes.ts | ||
| * Verify freshness: tsx scripts/generate-api-routes.ts --check | ||
| */ | ||
|
|
||
| /* eslint-disable no-console */ | ||
| import {readFileSync, writeFileSync} from 'node:fs' | ||
| import {join} from 'node:path' | ||
| import {setTimeout as sleep} from 'node:timers/promises' | ||
| import {parseArgs} from 'node:util' | ||
|
|
||
| import pMap from 'p-map' | ||
|
|
||
| import {OPENAPI_SPEC_INDEX_URL} from '../src/actions/api/constants.ts' | ||
| import {distillApiRoutes, type SpecSource} from '../src/actions/api/distillApiRoutes.ts' | ||
| import {type ApiRouteEntry, type OpenApiDocument} from '../src/actions/api/types.ts' | ||
|
|
||
| const OUTPUT_PATH = join(import.meta.dirname, '..', 'src', 'generated', 'apiRoutes.ts') | ||
|
|
||
| const FETCH_TIMEOUT_MS = 30_000 | ||
| const FETCH_ATTEMPTS = 3 | ||
| const FETCH_CONCURRENCY = 6 | ||
|
|
||
| interface SpecIndexEntry { | ||
| slug: string | ||
| title: string | ||
| } | ||
|
|
||
| async function fetchJson<T>(url: string): Promise<T> { | ||
| let lastError: unknown | ||
| for (let attempt = 1; attempt <= FETCH_ATTEMPTS; attempt++) { | ||
| try { | ||
| const response = await fetch(url, {signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)}) | ||
| if (!response.ok) { | ||
| throw new Error(`GET ${url} responded with HTTP ${response.status}`) | ||
| } | ||
| return (await response.json()) as T | ||
| } catch (error) { | ||
| lastError = error | ||
| if (attempt < FETCH_ATTEMPTS) { | ||
| const delayMs = 1000 * 2 ** (attempt - 1) | ||
| console.warn( | ||
| `Fetch failed (attempt ${attempt}/${FETCH_ATTEMPTS}), retrying in ${delayMs}ms: ${url}`, | ||
| ) | ||
| await sleep(delayMs) | ||
| } | ||
| } | ||
| } | ||
| throw lastError | ||
| } | ||
|
|
||
| async function fetchSpecSources(): Promise<SpecSource[]> { | ||
| const index = await fetchJson<{specs?: SpecIndexEntry[]}>(OPENAPI_SPEC_INDEX_URL) | ||
| const specs = index.specs ?? [] | ||
| if (specs.length === 0) { | ||
| throw new Error(`No OpenAPI specs found at ${OPENAPI_SPEC_INDEX_URL}`) | ||
| } | ||
|
|
||
| return pMap( | ||
| specs, | ||
| async ({slug, title}): Promise<SpecSource> => { | ||
| console.log(`Fetching spec: ${slug}`) | ||
| const document = await fetchJson<OpenApiDocument>( | ||
| `${OPENAPI_SPEC_INDEX_URL}/${slug}?format=json`, | ||
| ) | ||
| return {document, slug, title} | ||
| }, | ||
| {concurrency: FETCH_CONCURRENCY}, | ||
| ) | ||
| } | ||
|
|
||
| function renderManifest(routes: ApiRouteEntry[]): string { | ||
| const serialized = JSON.stringify(routes, null, 2) | ||
| // Match the repo code style (oxfmt): single-quoted strings. | ||
| .replaceAll(/"((?:[^"\\]|\\.)*)":/g, (_, key: string) => `${key}:`) | ||
| .replaceAll( | ||
| /"((?:[^"\\]|\\.)*)"/g, | ||
| (_, value: string) => `'${value.replaceAll("'", String.raw`\'`)}'`, | ||
| ) | ||
|
|
||
| return `/** | ||
| * GENERATED FILE - DO NOT EDIT | ||
| * | ||
| * Routing manifest for \`sanity api\`, distilled from the published OpenAPI | ||
| * specifications at ${OPENAPI_SPEC_INDEX_URL} | ||
| * | ||
| * Regenerate with: pnpm generate:api-routes | ||
| */ | ||
| import {type ApiRouteEntry} from '../actions/api/types.js' | ||
|
|
||
| export const apiRoutes: ApiRouteEntry[] = ${serialized} | ||
| ` | ||
| } | ||
|
|
||
| const {values: args} = parseArgs({options: {check: {default: false, type: 'boolean'}}}) | ||
|
|
||
| const sources = await fetchSpecSources() | ||
| const routes = distillApiRoutes(sources) | ||
| if (routes.length === 0) { | ||
| throw new Error( | ||
| `Distilled API routing manifest is empty (${sources.length} specs fetched) - refusing to write an empty endpoint list`, | ||
| ) | ||
| } | ||
| const rendered = renderManifest(routes) | ||
|
|
||
| if (args.check) { | ||
| let existing = '' | ||
| try { | ||
| existing = readFileSync(OUTPUT_PATH, 'utf8') | ||
| } catch { | ||
| // Missing file is stale by definition | ||
| } | ||
| if (existing === rendered) { | ||
| console.log('API routing manifest is up to date.') | ||
| } else { | ||
| console.error( | ||
| `API routing manifest is stale. Run "pnpm generate:api-routes" and commit the result.`, | ||
| ) | ||
| process.exitCode = 1 | ||
| } | ||
| } else { | ||
| writeFileSync(OUTPUT_PATH, rendered) | ||
| console.log(`Wrote ${routes.length} route entries to ${OUTPUT_PATH}`) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.