Add Zod-based type safety to bundle search route#38
Closed
shwetank-dev wants to merge 8 commits intomainfrom
Closed
Add Zod-based type safety to bundle search route#38shwetank-dev wants to merge 8 commits intomainfrom
shwetank-dev wants to merge 8 commits intomainfrom
Conversation
Replace hand-written JSON Schema with BundleSearchQuerySchema in query.ts, wire it into the /v1/bundles/search route via toJsonSchema(), and make the handler type-safe using Fastify generics instead of manual type casts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add BundleSearchQuerySchema in schemas/query.ts with proper enum constraints for type (node/python/binary) and sort, plus min/max validation for limit and offset - Replace hand-written JSON Schema with toJsonSchema(BundleSearchQuerySchema) - Make handler type-safe via Fastify generics, removing manual type cast - Use PackageSearchFilters instead of Record<string, unknown> for filters - Replace if/else sort chain with explicit sort map lookup - Remove runtime clamping (now enforced at schema validation level) - Update test to verify 400 rejection instead of silent clamping Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
apps/registry/src/schemas/query.ts
Outdated
Contributor
There was a problem hiding this comment.
This needs to be in packages/shcmeas
Collaborator
Author
There was a problem hiding this comment.
Some confusion on how schemas are copy over to generated
mgoldsborough
requested changes
Mar 11, 2026
apps/registry/src/schemas/query.ts
Outdated
| import { z } from 'zod'; | ||
|
|
||
| export const BundleSearchQuerySchema = z.object({ | ||
| q: z.optional(z.string()), |
Contributor
There was a problem hiding this comment.
add limit to the elngth
…parks Seed the local dev database with realistic bundle data matching production. Includes all 9 echo versions (with prereleases) and 6 nationalparks versions, with real manifests, provenance SHAs, download counts, and timestamps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Define BundleSearchParamsSchema in packages/schemas as the single source of truth for bundle search query validation (q max 200 chars, type, sort defaulting to downloads, limit 1-100, offset min 0). Copy to registry generated schemas and update bundles route to import from there instead of the ad-hoc query.ts, which is now deleted. Also fix type mismatches in the search handler: explicitly type tools as PackageTool[], coerce verified to boolean, and convert provenance schema_version from number to string to match the API response schema. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tring Replace inline JSON Schema definitions in the download route with Zod schemas, matching the pattern established for the search route: - Add BundleDownloadParamsSchema to validate os/arch query params against known enum values (darwin/linux/win32, x64/arm64), rejecting invalid platform values at the Fastify validation layer (422) - Add BundleVersionPathParamsSchema for scope/package/version path params - Wire both schemas via Fastify generics for type-safe request access without manual `as` casts - Register the production error handler in tests so validation errors return 422 (matching prod behavior) instead of raw Fastify 400s - Add tests for invalid os/arch rejection and search schema validation (defaults, type filter, enum/boundary validation, pagination) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…'t match When os/arch query params were provided but didn't match any artifact, the download route silently fell back to artifacts[0] instead of returning a 404. This meant requests like ?os=foo&arch=bar would succeed and return an unrelated platform's artifact. Clear the artifact to undefined when neither an exact platform match nor a universal (any/any) fallback is found, so the existing !artifact guard correctly returns 404. Fixes #28 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| const { os: queryOs, arch: queryArch } = request.query; | ||
| const name = `@${scope}/${packageName}`; | ||
|
|
||
| const pkg = await packageRepo.findByName(name); |
Contributor
There was a problem hiding this comment.
TODO: we should refactor this to be .findPackage(name, os, arch)
Then check for pkg and/or artifacts === undefined
9 tasks
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
BundleSearchQuerySchemainschemas/query.tsusing Zod with proper enum constraints (type: node/python/binary,sort: downloads/recent/name) and min/max validation forlimit(1-100) andoffset(>=0)toJsonSchema(BundleSearchQuerySchema)in the/v1/bundles/searchroute<{ Querystring: BundleSearchQuery }>generic — removes the manualas { ... }castPackageSearchFiltersinstead ofRecord<string, unknown>for filtersletinitializerMath.max/Math.min) — validation is now enforced at the schema level, invalid values return 400 instead of being silently correctedDesign decisions & tradeoffs discussed
optional()+default()ordering in Zodz.optional(z.string().default(...))wraps default inside optional → output type includesundefinedz.string().optional().default(...)puts default on the outside → output type is always concretesort: "downloads" | "recent" | "name"(neverundefined), while the JSON Schema still marks the field as not required (user can omit it)Schema validation vs silent clamping
limit=0were silently clamped to1— the client got unexpected results with no indication their input was wrongmin(1).max(100)rejects bad values with a 400, which is more correct API behaviorResponse type safety — current limitations
toJsonSchema(BundleSearchResponseSchema)) does serialization/stripping of extra fields but does not validate the response shape — if the handler returns the wrong structure, it won't throwReplygeneric can typereply.send(), but does not enforce return types fromasynchandlers — so there's no compile-time check that the handler returns the correct shapeWhere should the abstraction between DB fields and API fields live?
searchBundles()that accept query params directly and return API-shaped types (e.g.BundleSearchItem[]). Keeps handlers thin, but couples the repo to the API shapeWhere should query schemas live? (open question)
apps/registry/src/schemas/query.ts— co-located with the registry apppackages/schemas/alongside the shared response schemas, then generate/export from there — this would allow the SDK and CLI to share the same query typesTest plan
pnpm --filter @nimblebrain/mpak-registry test— all 80 tests passcurl 'http://localhost:3200/v1/bundles/search?q=echo'returns results with defaults appliedcurl 'http://localhost:3200/v1/bundles/search?limit=0'returns 400curl 'http://localhost:3200/v1/bundles/search?type=rust'returns 400curl 'http://localhost:3200/v1/bundles/search?sort=popularity'returns 400🤖 Generated with Claude Code