@boon4681/giri 0.0.3-alpha-11 → 0.0.3-alpha-13

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/validators/zod.ts","../../src/types.ts","../../src/validation.ts"],"sourcesContent":["import { z } from 'zod';\nimport { defineBodySchema, defineInputSchema } from '../validation';\nimport type { BodyContentType, GiriBodySchema, GiriInputSchema } from '../types';\n\n/** Wrap a single Zod schema as a giri input schema (validate via `safeParse`, JSON Schema via Zod 4). */\nfunction wrap<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\n return defineInputSchema<z.infer<Schema>>({\n validate(value) {\n const result = schema.safeParse(value);\n return result.success\n ? { ok: true, value: result.data }\n : { ok: false, issues: result.error };\n },\n toJsonSchema() {\n // `unrepresentable: 'any'` keeps types Zod can't render (e.g. `z.instanceof(File)`\n // for a multipart upload) as `{}` instead of throwing the whole conversion away.\n return z.toJSONSchema(schema, { unrepresentable: 'any' }) as Record<string, unknown>;\n },\n });\n}\n\n/**\n * Zod adapter. Peer-depends `zod`.\n *\n * ```ts\n * import { z } from 'zod';\n * import { zod } from '@boon4681/giri/validators/zod';\n *\n * // JSON body\n * export const body = zod.body({ json: z.object({ name: z.string().min(1) }) });\n * // JSON *or* multipart dispatched on Content-Type at runtime\n * export const body = zod.body({\n * json: z.object({ name: z.string() }),\n * form: z.object({ name: z.string(), avatar: z.instanceof(File) }),\n * });\n * export const query = zod.query(z.object({ page: z.coerce.number() }));\n * ```\n */\nexport const zod = {\n body<Map extends Partial<Record<BodyContentType, z.ZodType>>>(\n map: Map,\n ): GiriBodySchema<{ [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never }> {\n const contents = {} as Record<BodyContentType, GiriInputSchema>;\n for (const [contentType, schema] of Object.entries(map)) {\n if (schema) {\n contents[contentType as BodyContentType] = wrap(schema);\n }\n }\n return defineBodySchema(contents) as unknown as GiriBodySchema<{\n [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never;\n }>;\n },\n query<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\n return wrap(schema);\n },\n};\n","export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';\r\n\r\nexport type StatusCode = number;\r\n\r\nexport type ResponseFormat = 'json' | 'text' | 'html';\r\n\r\nexport const typedResponseBrand: unique symbol = Symbol.for('giri.typed-response') as never;\r\nexport const nativeContextBrand: unique symbol = Symbol.for('giri.native-context') as never;\r\n\r\nexport interface TypedResponse<\r\n T,\r\n S extends StatusCode = StatusCode,\r\n F extends ResponseFormat = ResponseFormat,\r\n> {\r\n readonly [typedResponseBrand]: {\r\n data: T;\r\n status: S;\r\n format: F;\r\n };\r\n readonly data: T;\r\n readonly status: S;\r\n readonly format: F;\r\n readonly headers?: HeadersInit;\r\n}\r\n\r\nexport type HandlerResponse = Response | TypedResponse<unknown, StatusCode, ResponseFormat>;\r\n\r\nexport interface ValidatedInput {\r\n /**\r\n * The validated request body. For a single declared content-type it's that schema's\r\n * output; for several it's a discriminated union `{ type; data }` (see `ValidBody`).\r\n */\r\n body?: unknown;\r\n query?: unknown;\r\n}\r\n\r\n/** Attributes for a `Set-Cookie` header. `path` defaults to `/`. */\r\nexport interface CookieOptions {\r\n domain?: string;\r\n path?: string;\r\n /** Lifetime in seconds. */\r\n maxAge?: number;\r\n expires?: Date;\r\n httpOnly?: boolean;\r\n secure?: boolean;\r\n sameSite?: 'Strict' | 'Lax' | 'None' | 'strict' | 'lax' | 'none';\r\n partitioned?: boolean;\r\n priority?: 'Low' | 'Medium' | 'High' | 'low' | 'medium' | 'high';\r\n}\r\n\r\n/**\r\n * Cookie read/write, implemented per adapter with its runtime's native helpers. giri core\r\n * supplies the {@link CookieSink} (where to read from / write to); the adapter owns encoding.\r\n */\r\nexport interface CookieJar {\r\n get(name: string): string | undefined;\r\n all(): Record<string, string>;\r\n set(name: string, value: string, options?: CookieOptions): void;\r\n delete(name: string, options?: CookieOptions): void;\r\n getSigned(name: string): Promise<string | false | undefined>;\r\n setSigned(name: string, value: string, options?: CookieOptions): Promise<void>;\r\n}\r\n\r\n/** What core hands an adapter's cookie jar: the request to read from, the response sink to write to. */\r\nexport interface CookieSink {\r\n /** The incoming request, for reading the `Cookie` header. */\r\n request: Request;\r\n /** Append one already-serialized `Set-Cookie` header value to the response. */\r\n append(setCookieHeader: string): void;\r\n /** The configured `cookieSecret`, if any (for signed cookies). */\r\n secret?: string;\r\n}\r\n\r\n/** Builds a {@link CookieJar} bound to one request's {@link CookieSink}. Each adapter provides one. */\r\nexport type CookieJarFactory = (sink: CookieSink) => CookieJar;\r\n\r\nexport interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {\r\n raw: Request;\r\n url: URL;\r\n method: string;\r\n header(name: string): string | null;\r\n json<T = unknown>(): Promise<T>;\r\n text(): Promise<string>;\r\n arrayBuffer(): Promise<ArrayBuffer>;\r\n formData(): Promise<FormData>;\r\n valid<K extends keyof Input & ('body' | 'query')>(key: K): Input[K];\r\n /** Read a request cookie by name, or `undefined` if absent. */\r\n cookie(name: string): string | undefined;\r\n /** All request cookies as a name: value map. */\r\n cookies(): Record<string, string>;\r\n /**\r\n * Read and verify a signed cookie. Resolves to the original value, `false` if the\r\n * signature was tampered with, or `undefined` if the cookie is absent. Requires\r\n * `cookieSecret` in `giri.config`.\r\n */\r\n signedCookie(name: string): Promise<string | false | undefined>;\r\n}\r\n\r\ndeclare global {\r\n /**\r\n * Global registration surface for app-wide types. `giri sync` augments\r\n * `Giri.Register[\"app\"]` from `src/main.ts` `init()` return type so `c.app` is\r\n * typed without per-route generics (the registration pattern).\r\n */\r\n namespace Giri {\r\n interface Register {}\r\n }\r\n}\r\n\r\n/**\r\n * The app-wide services container, the type of `c.app`. `giri sync` infers it from\r\n * `src/main.ts`'s `init()` return type (via the global `Giri.Register` augmentation);\r\n * until then it falls back to an open record. Leave `init` unannotated (its return is\r\n * the source of truth) and annotate `teardown`'s parameter with this:\r\n *\r\n * ```ts\r\n * export const init = () => ({ db }); // inferred\r\n * export const teardown = (services: Services) => services.db.close();\r\n * ```\r\n */\r\nexport type Services = Giri.Register extends { app: infer A }\r\n ? A\r\n : Record<string, unknown>;\r\n\r\nexport interface Context<\r\n Params extends Record<string, string> = Record<string, string>,\r\n Input extends ValidatedInput = ValidatedInput,\r\n Vars extends Record<string, unknown> = {},\r\n> {\r\n params: Params;\r\n /** App-wide services from `src/main.ts`'s `init()`, seeded into every request. */\r\n app: Services;\r\n req: GiriRequest<Input>;\r\n // Context vars (`c.set`/`c.get`). Keys declared by middleware (`Vars`) are typed;\r\n // any other key stays open (`unknown`) so untracked keys still work.\r\n set<K extends keyof Vars & string>(key: K, value: Vars[K]): void;\r\n set<K extends string>(key: K, value: unknown): void;\r\n get<K extends keyof Vars & string>(key: K): Vars[K];\r\n get<V = unknown>(key: string): V;\r\n json<T, S extends StatusCode = 200>(\r\n data: T,\r\n status?: S,\r\n headers?: HeadersInit,\r\n ): TypedResponse<T, S, 'json'>;\r\n text<S extends StatusCode = 200>(\r\n text: string,\r\n status?: S,\r\n headers?: HeadersInit,\r\n ): TypedResponse<string, S, 'text'>;\r\n /** An HTML response (`text/html`). Like `text`, the body is a string. */\r\n html<S extends StatusCode = 200>(\r\n html: string,\r\n status?: S,\r\n headers?: HeadersInit,\r\n ): TypedResponse<string, S, 'html'>;\r\n /** A raw-body response - string, stream, buffer, FormData, … (not documented in OpenAPI). */\r\n body(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\r\n /** Alias of `body`, mirroring Hono's `c.newResponse`. */\r\n newResponse(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\r\n /** A redirect (defaults to 302) with the `Location` header set. */\r\n redirect(location: string, status?: StatusCode): Response;\r\n /** A 404 Not Found response. */\r\n notFound(): Response;\r\n /**\r\n * Set a response header applied to whatever this handler returns. Pass `{ append: true }` to add\r\n * another value (e.g. `Set-Cookie`); omit `value` to delete. Mirrors Hono's `c.header`.\r\n */\r\n header(name: string, value?: string, options?: { append?: boolean }): void;\r\n /** Default status for `body`/`redirect`, and for `json`/`text`/`html` when no status arg is given. */\r\n status(code: StatusCode): void;\r\n /**\r\n * Set a response cookie via `Set-Cookie`. Pass `value: null` to delete it (send the\r\n * same `path`/`domain` you set it with). Stacks with other cookies set this request.\r\n */\r\n cookie(name: string, value: string | null, options?: CookieOptions): void;\r\n /** Set an HMAC-signed cookie. Requires `cookieSecret` in `giri.config`. */\r\n signedCookie(name: string, value: string, options?: CookieOptions): Promise<void>;\r\n}\r\n\r\nexport type Handle<\r\n Params extends Record<string, string> = Record<string, string>,\r\n Input extends ValidatedInput = ValidatedInput,\r\n Vars extends Record<string, unknown> = {},\r\n> = (c: Context<Params, Input, Vars>) => HandlerResponse | Promise<HandlerResponse>;\r\n\r\nexport type Next = () => Promise<HandlerResponse | void>;\r\n\r\n/** An OpenAPI security requirement, e.g. `{ bearerAuth: [] }`. */\r\nexport type SecurityRequirement = Record<string, string[]>;\r\n\r\nexport interface MiddlewareOpenApi {\r\n /** Security requirements this middleware enforces */\r\n security?: SecurityRequirement[];\r\n /** Optional scheme definitions, merged into `components.securitySchemes` so the doc is self-contained. */\r\n securitySchemes?: Record<string, unknown>;\r\n [key: string]: unknown;\r\n}\r\n\r\nexport interface MiddlewareOptions {\r\n /** Validate and expose this request body before the middleware chain runs. */\r\n body?: GiriBodySchema;\r\n /** Validate and expose these query parameters before the middleware chain runs. */\r\n query?: GiriInputSchema;\r\n openapi?: MiddlewareOpenApi;\r\n}\r\n\r\ndeclare const middlewareTypesBrand: unique symbol;\r\n\r\nexport interface Middleware<\r\n Params extends Record<string, string> = Record<string, string>,\r\n Input extends ValidatedInput = ValidatedInput,\r\n Vars extends Record<string, unknown> = {},\r\n> {\r\n (c: Context<Params, Input, Vars>, next: Next): HandlerResponse | void | Promise<HandlerResponse | void>;\r\n readonly [middlewareTypesBrand]?: {\r\n params: Params;\r\n input: Input;\r\n vars: Vars;\r\n };\r\n body?: GiriBodySchema;\r\n query?: GiriInputSchema;\r\n openapi?: MiddlewareOpenApi;\r\n}\r\n\r\n/** The context vars a middleware injects (its `Vars` type parameter). */\r\nexport type VarsOf<M> = M extends {\r\n readonly [middlewareTypesBrand]?: { vars: infer V extends Record<string, unknown> };\r\n}\r\n ? V\r\n : {};\r\n\r\n/** Intersect the injected vars of a tuple of middleware (built with `stack(...)`). */\r\nexport type MergeStack<T> = T extends readonly [infer Head, ...infer Rest]\r\n ? VarsOf<Head> & MergeStack<Rest>\r\n : {};\r\n\r\n/**\r\n * Merge the injected vars of a `middleware` export. A `stack(...)` tuple is merged element-wise;\r\n * a single bare middleware (`export const middleware = fromHono(...)`) contributes its own vars; a\r\n * plain `Middleware[]` (not a `stack(...)` tuple) contributes nothing - its element types are lost.\r\n */\r\nexport type InferStackVars<T> = T extends readonly [unknown, ...unknown[]]\r\n ? MergeStack<T>\r\n : T extends Middleware<any, any, any>\r\n ? VarsOf<T>\r\n : {};\r\n\r\n/**\r\n * The vars injected by a module own `middleware` export (a `stack(...)`). Used by the\r\n * generated per-method handle so a verb file's own `export const middleware` types\r\n * `c.get`/`c.set`, on top of the folder's `+shared.ts` chain.\r\n */\r\nexport type MiddlewareVarsOf<M> = M extends { middleware: infer Stack }\r\n ? InferStackVars<Stack>\r\n : {};\r\n\r\n/** A JSON Schema object (JSON Schema 2020-12 / OpenAPI 3.1 dialect). */\r\nexport type JsonSchema = Record<string, unknown>;\r\n\r\nexport const inputSchemaBrand: unique symbol = Symbol.for('giri.input-schema') as never;\r\n\r\nexport type InputValidationResult<Output = unknown> =\r\n | { ok: true; value: Output }\r\n | { ok: false; issues: unknown };\r\n\r\n/**\r\n * A input schema every wrapper form (`body`/`query`) export takes. A vendor\r\n * adapter (`@boon4681/giri/validators/zod`, `@boon4681/giri/validators/valibot`, …) returns one; build a\r\n * custom one with `defineInputSchema`. giri core depends only on this interface, never\r\n * on a validator library. `validate` is the runtime check; `toJsonSchema` feeds OpenAPI.\r\n */\r\nexport interface GiriInputSchema<Output = unknown> {\r\n readonly [inputSchemaBrand]: true;\r\n validate(value: unknown): InputValidationResult<Output> | Promise<InputValidationResult<Output>>;\r\n toJsonSchema(): JsonSchema;\r\n}\r\n\r\n/** Extract the validated output type of a giri input schema: `Infer<typeof body>`. */\r\nexport type Infer<T> = T extends GiriInputSchema<infer Output> ? Output : never;\r\n\r\nexport type BodyContentType = 'json' | 'form' | 'urlencoded' | 'text';\r\n\r\nexport const bodySchemaBrand: unique symbol = Symbol.for('giri.body-schema') as never;\r\n\r\n/**\r\n * A request body declared as a set of accepted content-types wrapped form `body`\r\n * takes (`zod.body({ json, form })`). One key means that encoding only; several mean the\r\n * endpoint accepts any of them, dispatched at runtime on the request `Content-Type`.\r\n * Each entry is a plain `GiriInputSchema`, so `validate`/`toJsonSchema` work per content-type.\r\n */\r\nexport interface GiriBodySchema<\r\n Outputs extends Partial<Record<BodyContentType, unknown>> = Partial<Record<BodyContentType, unknown>>,\r\n> {\r\n readonly [bodySchemaBrand]: true;\r\n readonly contents: { [K in keyof Outputs & BodyContentType]: GiriInputSchema<Outputs[K]> };\r\n}\r\n\r\n/** True when `T` is a union of more than one member. */\r\ntype IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\r\n\r\n/**\r\n * The validated body a handler receives. A single declared content-type yields that\r\n * schema's output directly; several yield a discriminated union keyed by content-type.\r\n */\r\nexport type ValidBody<B> = B extends GiriBodySchema<infer Outputs>\r\n ? IsUnion<keyof Outputs> extends true\r\n ? { [K in keyof Outputs]: { type: K; data: Outputs[K] } }[keyof Outputs]\r\n : Outputs[keyof Outputs]\r\n : never;\r\n\r\n/** The validated query a handler receives. */\r\nexport type ValidQuery<Q> = Q extends GiriInputSchema<infer Output> ? Output : never;\r\n\r\n/** Drop keys whose value resolved to `never` (an input the route didn't declare). */\r\ntype PruneNever<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] };\r\n\r\n/** Derive validated input from middleware metadata added by `defineMiddleware({ body, query }, ...)`. */\r\nexport type InputOfMiddleware<M> = PruneNever<{\r\n body: M extends { body: infer B } ? ValidBody<B> : never;\r\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\r\n}>;\r\n\r\n/** Intersect the validated inputs contributed by a tuple built with `stack(...)`. */\r\nexport type MergeStackInput<T> = T extends readonly [infer Head, ...infer Rest]\r\n ? InputOfMiddleware<Head> & MergeStackInput<Rest>\r\n : {};\r\n\r\n/** Derive validated input contributed by a middleware function or stack. */\r\nexport type InferStackInput<T> = T extends readonly [unknown, ...unknown[]]\r\n ? MergeStackInput<T>\r\n : InputOfMiddleware<T>;\r\n\r\n/** The validated input contributed by a module's own `middleware` export. */\r\nexport type MiddlewareInputOf<M> = M extends { middleware: infer Stack }\r\n ? InferStackInput<Stack>\r\n : {};\r\n\r\n/** Derive the callback input type from `defineMiddleware` options. */\r\nexport type MiddlewareOptionsInput<O> = PruneNever<{\r\n body: O extends { body: infer B } ? ValidBody<B> : never;\r\n query: O extends { query: infer Q } ? ValidQuery<Q> : never;\r\n}>;\r\n\r\n/**\r\n * Derive a route's `ValidatedInput` from a module's `body`/`query` exports. The generated\r\n * per-method `$types` handle (`POST`, `GET`, …) uses this so handlers infer `c.req.valid`\r\n * with no manual generic.\r\n */\r\nexport type RouteInputOf<M> = PruneNever<{\r\n body: M extends { body: infer B } ? ValidBody<B> : never;\r\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\r\n}>;\r\n\r\nexport interface RouteInput {\r\n // Each owner (route export + every applied middleware) contributes a validator; the\r\n // results are merged, matching the type layer which intersects them. Order is middleware\r\n // first, then the route, so a route's fields win on key collisions.\r\n body?: GiriBodySchema[];\r\n query?: GiriInputSchema[];\r\n}\r\n\r\nexport interface RouteOpenApi {\r\n /** Omit this route from the generated `openapi.json` (it still serves normally). */\r\n hidden?: boolean;\r\n /**\r\n * OpenAPI tags - the grouping in doc viewers. On a `+shared.ts` they apply to every route in the\r\n * folder; the chain is merged and de-duplicated, so a route's tags add to\r\n * its folders'.\r\n */\r\n tags?: string[];\r\n /** Short operation summary. Cascades down the chain (a verb file overrides its folders). */\r\n summary?: string;\r\n /** Longer operation description. Cascades down the chain (a verb file overrides its folders). */\r\n description?: string;\r\n /** Marks the operation(s) deprecated. On a `+shared.ts` it deprecates the whole folder. */\r\n deprecated?: boolean;\r\n /** Unique operationId. Verb-file only - it is never inherited from a `+shared.ts`. */\r\n operationId?: string;\r\n}\r\n\r\nexport type RouteOpenApiConfig = RouteOpenApi | boolean;\r\n\r\nexport interface GiriRouteRegistration {\r\n method: HttpMethod;\r\n path: string;\r\n handle: Handle;\r\n middleware: Middleware[];\r\n input?: RouteInput;\r\n /** App-wide services to seed onto `c.app` (same instance for every route). */\r\n services?: Services;\r\n /** Secret for signing/verifying cookies (`c.signedCookie`), from `config.cookieSecret`. */\r\n cookieSecret?: string;\r\n}\r\n\r\nexport type GiriFetchHandler = (req: Request) => Response | Promise<Response>;\r\n\r\nexport interface GiriServeOptions {\r\n port: number;\r\n hostname?: string;\r\n}\r\n\r\nexport interface GiriServerInfo {\r\n address: string;\r\n port: number;\r\n}\r\n\r\nexport interface GiriServer {\r\n close(): void | Promise<void>;\r\n}\r\n\r\nexport interface GiriAdapter<App> {\r\n name?: string;\r\n createApp(): App;\r\n register(app: App, route: GiriRouteRegistration): void;\r\n fetch(app: App, req: Request): Promise<Response>;\r\n /**\r\n * Bind the configured backend's runtime to a port and start serving.\r\n * giri core stays runtime-agnostic: it hands the adapter a request handler\r\n * (so hot-reload keeps working) and the adapter owns the actual server.\r\n */\r\n serve(\r\n handler: GiriFetchHandler,\r\n options: GiriServeOptions,\r\n onListen?: (info: GiriServerInfo) => void,\r\n ): GiriServer;\r\n}\r\n\r\nexport interface GiriConfig<App = unknown> {\r\n adapter: GiriAdapter<App>;\r\n alias?: Record<string, string | string[]>;\r\n outDir?: string;\r\n server?: {\r\n port?: number;\r\n hostname?: string;\r\n };\r\n errorSchema?: unknown;\r\n /** Secret used to sign/verify cookies via `c.signedCookie` / `c.req.signedCookie`. */\r\n cookieSecret?: string;\r\n}\r\n\r\nexport interface GiriPaths {\r\n cwd: string;\r\n routesDir: string;\r\n outDir: string;\r\n}\r\n","import {\n type BodyContentType,\n type GiriBodySchema,\n type GiriInputSchema,\n type RouteInput,\n type TypedResponse,\n type ValidatedInput,\n bodySchemaBrand,\n inputSchemaBrand,\n} from './types';\nimport { createTypedResponse } from './context';\n\ninterface PreparedInput {\n ok: true;\n validated: ValidatedInput;\n}\n\ninterface FailedInput {\n ok: false;\n response: TypedResponse<{ message: string; issues: unknown }, 400 | 415, 'json'>;\n}\n\nexport type PreparedRequestInput = PreparedInput | FailedInput;\n\nexport interface RouteInputSource {\n label: string;\n body?: unknown;\n query?: unknown;\n}\n\n/**\n * Build a giri input schema from a `validate` + `toJsonSchema` pair. Vendor adapters use\n * this; you can call it directly to make a custom validator. The brand is a global symbol,\n * so a hand-rolled `{ [Symbol.for(\"giri.input-schema\")]: true, validate, toJsonSchema }` works too.\n */\nexport function defineInputSchema<Output>(\n schema: Omit<GiriInputSchema<Output>, typeof inputSchemaBrand>,\n): GiriInputSchema<Output> {\n return { [inputSchemaBrand]: true, ...schema };\n}\n\nexport function isGiriInputSchema(value: unknown): value is GiriInputSchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[inputSchemaBrand] === true,\n );\n}\n\n/**\n * Build a giri body schema from per-content-type input schemas. Validator adapters use this `zod.body({ json, form })`\n */\nexport function defineBodySchema<Outputs extends Partial<Record<BodyContentType, unknown>>>(\n contents: GiriBodySchema<Outputs>['contents'],\n): GiriBodySchema<Outputs> {\n return { [bodySchemaBrand]: true, contents };\n}\n\nexport function isGiriBodySchema(value: unknown): value is GiriBodySchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[bodySchemaBrand] === true,\n );\n}\n\n/**\n * A route/middleware declared a target (`body`/`query`) without wrapping it in a validator.\n * Branded so callers (e.g. the generator) can tell an actionable config error apart from a\n * route that merely failed to load.\n */\nexport class RouteInputError extends Error {\n override readonly name = 'RouteInputError';\n}\n\n/**\n * Collect every owner's validator for each target (`body`/`query`). A route export and any\n * applied middleware can each contribute one; their validated outputs are merged at request time\n * (see {@link prepareRequestInput}), matching the type layer which intersects them. Owners are\n * kept in source order (middleware first, then the route) so a route's fields win on collision.\n */\nexport function resolveRouteInput(sources: readonly RouteInputSource[]): RouteInput | undefined {\n const body: GiriBodySchema[] = [];\n const query: GiriInputSchema[] = [];\n\n for (const source of sources) {\n if (source.body !== undefined) {\n if (!isGiriBodySchema(source.body)) {\n throw new RouteInputError(\n `${source.label}: \"body\" must be wrapped with a validator, e.g. \\`zod.body({ json: ... })\\` from @boon4681/giri/validators/zod.`,\n );\n }\n body.push(source.body);\n }\n\n if (source.query !== undefined) {\n if (!isGiriInputSchema(source.query)) {\n throw new RouteInputError(\n `${source.label}: \"query\" must be wrapped with a validator, e.g. \\`zod.query(...)\\` from @boon4681/giri/validators/zod.`,\n );\n }\n query.push(source.query);\n }\n }\n\n const input: RouteInput = {};\n if (body.length > 0) {\n input.body = body;\n }\n if (query.length > 0) {\n input.query = query;\n }\n return input.body || input.query ? input : undefined;\n}\n\nconst MIME_TO_CONTENT_TYPE: Record<string, BodyContentType> = {\n 'application/json': 'json',\n 'multipart/form-data': 'form',\n 'application/x-www-form-urlencoded': 'urlencoded',\n 'text/plain': 'text',\n};\n\nfunction contentTypeFromHeader(header: string | null): BodyContentType | undefined {\n if (!header) {\n return undefined;\n }\n const mime = header.split(';', 1)[0].trim().toLowerCase();\n return MIME_TO_CONTENT_TYPE[mime];\n}\n\n/** Flatten a `FormData` into a plain object, collapsing repeated fields into arrays. */\nfunction formDataObject(form: FormData): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n form.forEach((value, key) => {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n });\n return result;\n}\n\nasync function readRawBody(request: Request, contentType: BodyContentType): Promise<unknown> {\n const cloned = request.clone();\n if (contentType === 'json') {\n return cloned.json();\n }\n if (contentType === 'text') {\n return cloned.text();\n }\n return formDataObject(await cloned.formData());\n}\n\nfunction queryObject(url: URL): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const [key, value] of url.searchParams) {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n }\n return result;\n}\n\n/** Shallow-merge validated outputs from several owners; a single owner keeps its exact value. */\nfunction mergeValidated(values: unknown[]): unknown {\n if (values.length === 1) {\n return values[0];\n }\n return Object.assign({}, ...values);\n}\n\nexport async function prepareRequestInput(request: Request, input?: RouteInput): Promise<PreparedRequestInput> {\n const validated: ValidatedInput = {};\n\n if (input?.query && input.query.length > 0) {\n const query = queryObject(new URL(request.url));\n const values: unknown[] = [];\n for (const schema of input.query) {\n const result = await schema.validate(query);\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid query parameters.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n values.push(result.value);\n }\n validated.query = mergeValidated(values);\n }\n\n if (input?.body && input.body.length > 0) {\n // The set of acceptable content-types is the union across every body owner.\n const declared = [\n ...new Set(input.body.flatMap((schema) => Object.keys(schema.contents) as BodyContentType[])),\n ];\n const requested = contentTypeFromHeader(request.headers.get('content-type'));\n // Pick the type matching the request's content-type; fall back to JSON when the header is\n // missing/unrecognized but JSON is on offer (so header-less posts still work).\n const chosen: BodyContentType | undefined =\n requested && declared.includes(requested) ? requested : declared.includes('json') ? 'json' : undefined;\n\n if (!chosen) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Unsupported media type.', issues: { accepted: declared } },\n 415,\n 'json',\n ),\n };\n }\n\n let rawBody: unknown;\n try {\n rawBody = await readRawBody(request, chosen);\n } catch (error) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: error },\n 400,\n 'json',\n ),\n };\n }\n\n const values: unknown[] = [];\n for (const schema of input.body) {\n const contents = schema.contents as Partial<Record<BodyContentType, GiriInputSchema>>;\n const contentSchema = contents[chosen];\n // An owner that doesn't declare the chosen content-type simply contributes nothing.\n if (!contentSchema) {\n continue;\n }\n const result = await contentSchema.validate(rawBody);\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n values.push(result.value);\n }\n\n const data = mergeValidated(values);\n validated.body = declared.length > 1 ? { type: chosen, data } : data;\n }\n\n return { ok: true, validated };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAkB;;;ACmQX,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;ACvPpE,SAAS,kBACZ,QACuB;AACvB,SAAO,EAAE,CAAC,gBAAgB,GAAG,MAAM,GAAG,OAAO;AACjD;AAaO,SAAS,iBACZ,UACuB;AACvB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,SAAS;AAC/C;;;AFnDA,SAAS,KAA+B,QAAkD;AACtF,SAAO,kBAAmC;AAAA,IACtC,SAAS,OAAO;AACZ,YAAM,SAAS,OAAO,UAAU,KAAK;AACrC,aAAO,OAAO,UACR,EAAE,IAAI,MAAM,OAAO,OAAO,KAAK,IAC/B,EAAE,IAAI,OAAO,QAAQ,OAAO,MAAM;AAAA,IAC5C;AAAA,IACA,eAAe;AAGX,aAAO,aAAE,aAAa,QAAQ,EAAE,iBAAiB,MAAM,CAAC;AAAA,IAC5D;AAAA,EACJ,CAAC;AACL;AAmBO,IAAM,MAAM;AAAA,EACf,KACI,KACwF;AACxF,UAAM,WAAW,CAAC;AAClB,eAAW,CAAC,aAAa,MAAM,KAAK,OAAO,QAAQ,GAAG,GAAG;AACrD,UAAI,QAAQ;AACR,iBAAS,WAA8B,IAAI,KAAK,MAAM;AAAA,MAC1D;AAAA,IACJ;AACA,WAAO,iBAAiB,QAAQ;AAAA,EAGpC;AAAA,EACA,MAAgC,QAAkD;AAC9E,WAAO,KAAK,MAAM;AAAA,EACtB;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/validators/zod.ts","../../src/types.ts","../../src/validation.ts"],"sourcesContent":["import { z } from 'zod';\r\nimport { defineBodySchema, defineInputSchema } from '../validation';\r\nimport type { BodyContentType, GiriBodySchema, GiriInputSchema } from '../types';\r\n\r\n/** Wrap a single Zod schema as a giri input schema (validate via `safeParse`, JSON Schema via Zod 4). */\r\nfunction wrap<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\r\n return defineInputSchema<z.infer<Schema>>({\r\n validate(value) {\r\n const result = schema.safeParse(value);\r\n return result.success\r\n ? { ok: true, value: result.data }\r\n : { ok: false, issues: result.error };\r\n },\r\n toJsonSchema() {\r\n // `unrepresentable: 'any'` keeps types Zod can't render (e.g. `z.instanceof(File)`\r\n // for a multipart upload) as `{}` instead of throwing the whole conversion away.\r\n return z.toJSONSchema(schema, { unrepresentable: 'any' }) as Record<string, unknown>;\r\n },\r\n });\r\n}\r\n\r\n/**\r\n * Zod adapter. Peer-depends `zod`.\r\n *\r\n * ```ts\r\n * import { z } from 'zod';\r\n * import { zod } from '@boon4681/giri/validators/zod';\r\n *\r\n * // JSON body\r\n * export const body = zod.body({ json: z.object({ name: z.string().min(1) }) });\r\n * // JSON *or* multipart dispatched on Content-Type at runtime\r\n * export const body = zod.body({\r\n * json: z.object({ name: z.string() }),\r\n * form: z.object({ name: z.string(), avatar: z.instanceof(File) }),\r\n * });\r\n * export const query = zod.query(z.object({ page: z.coerce.number() }));\r\n * ```\r\n */\r\nexport const zod = {\r\n body<Map extends Partial<Record<BodyContentType, z.ZodType>>>(\r\n map: Map,\r\n ): GiriBodySchema<{ [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never }> {\r\n const contents = {} as Record<BodyContentType, GiriInputSchema>;\r\n for (const [contentType, schema] of Object.entries(map)) {\r\n if (schema) {\r\n contents[contentType as BodyContentType] = wrap(schema);\r\n }\r\n }\r\n return defineBodySchema(contents) as unknown as GiriBodySchema<{\r\n [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never;\r\n }>;\r\n },\r\n query<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\r\n return wrap(schema);\r\n },\r\n};\r\n","export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';\r\n\r\nexport type StatusCode = number;\r\n\r\nexport type ResponseFormat = 'json' | 'text' | 'html';\r\n\r\nexport const typedResponseBrand: unique symbol = Symbol.for('giri.typed-response') as never;\r\nexport const nativeContextBrand: unique symbol = Symbol.for('giri.native-context') as never;\r\n\r\nexport interface TypedResponse<\r\n T,\r\n S extends StatusCode = StatusCode,\r\n F extends ResponseFormat = ResponseFormat,\r\n> {\r\n readonly [typedResponseBrand]: {\r\n data: T;\r\n status: S;\r\n format: F;\r\n };\r\n readonly data: T;\r\n readonly status: S;\r\n readonly format: F;\r\n readonly headers?: HeadersInit;\r\n}\r\n\r\nexport type HandlerResponse = Response | TypedResponse<unknown, StatusCode, ResponseFormat>;\r\n\r\nexport interface ValidatedInput {\r\n /**\r\n * The validated request body. For a single declared content-type it's that schema's\r\n * output; for several it's a discriminated union `{ type; data }` (see `ValidBody`).\r\n */\r\n body?: unknown;\r\n query?: unknown;\r\n}\r\n\r\n/** Attributes for a `Set-Cookie` header. `path` defaults to `/`. */\r\nexport interface CookieOptions {\r\n domain?: string;\r\n path?: string;\r\n /** Lifetime in seconds. */\r\n maxAge?: number;\r\n expires?: Date;\r\n httpOnly?: boolean;\r\n secure?: boolean;\r\n sameSite?: 'Strict' | 'Lax' | 'None' | 'strict' | 'lax' | 'none';\r\n partitioned?: boolean;\r\n priority?: 'Low' | 'Medium' | 'High' | 'low' | 'medium' | 'high';\r\n}\r\n\r\n/**\r\n * Cookie read/write, implemented per adapter with its runtime's native helpers. giri core\r\n * supplies the {@link CookieSink} (where to read from / write to); the adapter owns encoding.\r\n */\r\nexport interface CookieJar {\r\n get(name: string): string | undefined;\r\n all(): Record<string, string>;\r\n set(name: string, value: string, options?: CookieOptions): void;\r\n delete(name: string, options?: CookieOptions): void;\r\n getSigned(name: string): Promise<string | false | undefined>;\r\n setSigned(name: string, value: string, options?: CookieOptions): Promise<void>;\r\n}\r\n\r\n/** What core hands an adapter's cookie jar: the request to read from, the response sink to write to. */\r\nexport interface CookieSink {\r\n /** The incoming request, for reading the `Cookie` header. */\r\n request: Request;\r\n /** Append one already-serialized `Set-Cookie` header value to the response. */\r\n append(setCookieHeader: string): void;\r\n /** The configured `cookieSecret`, if any (for signed cookies). */\r\n secret?: string;\r\n}\r\n\r\n/** Builds a {@link CookieJar} bound to one request's {@link CookieSink}. Each adapter provides one. */\r\nexport type CookieJarFactory = (sink: CookieSink) => CookieJar;\r\n\r\nexport interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {\r\n raw: Request;\r\n url: URL;\r\n method: string;\r\n header(name: string): string | null;\r\n json<T = unknown>(): Promise<T>;\r\n text(): Promise<string>;\r\n arrayBuffer(): Promise<ArrayBuffer>;\r\n formData(): Promise<FormData>;\r\n valid<K extends keyof Input & ('body' | 'query')>(key: K): Input[K];\r\n /** Read a request cookie by name, or `undefined` if absent. */\r\n cookie(name: string): string | undefined;\r\n /** All request cookies as a name: value map. */\r\n cookies(): Record<string, string>;\r\n /**\r\n * Read and verify a signed cookie. Resolves to the original value, `false` if the\r\n * signature was tampered with, or `undefined` if the cookie is absent. Requires\r\n * `cookieSecret` in `giri.config`.\r\n */\r\n signedCookie(name: string): Promise<string | false | undefined>;\r\n}\r\n\r\ndeclare global {\r\n /**\r\n * Global registration surface for app-wide types. `giri sync` augments\r\n * `Giri.Register[\"app\"]` from `src/main.ts` `init()` return type so `c.app` is\r\n * typed without per-route generics (the registration pattern).\r\n */\r\n namespace Giri {\r\n interface Register {}\r\n }\r\n}\r\n\r\n/**\r\n * The app-wide services container, the type of `c.app`. `giri sync` infers it from\r\n * `src/main.ts`'s `init()` return type (via the global `Giri.Register` augmentation);\r\n * until then it falls back to an open record. Leave `init` unannotated (its return is\r\n * the source of truth) and annotate `teardown`'s parameter with this:\r\n *\r\n * ```ts\r\n * export const init = () => ({ db }); // inferred\r\n * export const teardown = (services: Services) => services.db.close();\r\n * ```\r\n */\r\nexport type Services = Giri.Register extends { app: infer A }\r\n ? A\r\n : Record<string, unknown>;\r\n\r\nexport interface Context<\r\n Params extends Record<string, string> = Record<string, string>,\r\n Input extends ValidatedInput = ValidatedInput,\r\n Vars extends Record<string, unknown> = {},\r\n> {\r\n params: Params;\r\n /** App-wide services from `src/main.ts`'s `init()`, seeded into every request. */\r\n app: Services;\r\n req: GiriRequest<Input>;\r\n // Context vars (`c.set`/`c.get`). Keys declared by middleware (`Vars`) are typed;\r\n // any other key stays open (`unknown`) so untracked keys still work.\r\n set<K extends keyof Vars & string>(key: K, value: Vars[K]): void;\r\n set<K extends string>(key: K, value: unknown): void;\r\n get<K extends keyof Vars & string>(key: K): Vars[K];\r\n get<V = unknown>(key: string): V;\r\n json<T, S extends StatusCode = 200>(\r\n data: T,\r\n status?: S,\r\n headers?: HeadersInit,\r\n ): TypedResponse<T, S, 'json'>;\r\n text<S extends StatusCode = 200>(\r\n text: string,\r\n status?: S,\r\n headers?: HeadersInit,\r\n ): TypedResponse<string, S, 'text'>;\r\n /** An HTML response (`text/html`). Like `text`, the body is a string. */\r\n html<S extends StatusCode = 200>(\r\n html: string,\r\n status?: S,\r\n headers?: HeadersInit,\r\n ): TypedResponse<string, S, 'html'>;\r\n /** A raw-body response - string, stream, buffer, FormData, … (not documented in OpenAPI). */\r\n body(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\r\n /** Alias of `body`, mirroring Hono's `c.newResponse`. */\r\n newResponse(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\r\n /** A redirect (defaults to 302) with the `Location` header set. */\r\n redirect(location: string, status?: StatusCode): Response;\r\n /** A 404 Not Found response. */\r\n notFound(): Response;\r\n /**\r\n * Set a response header applied to whatever this handler returns. Pass `{ append: true }` to add\r\n * another value (e.g. `Set-Cookie`); omit `value` to delete. Mirrors Hono's `c.header`.\r\n */\r\n header(name: string, value?: string, options?: { append?: boolean }): void;\r\n /** Default status for `body`/`redirect`, and for `json`/`text`/`html` when no status arg is given. */\r\n status(code: StatusCode): void;\r\n /**\r\n * Set a response cookie via `Set-Cookie`. Pass `value: null` to delete it (send the\r\n * same `path`/`domain` you set it with). Stacks with other cookies set this request.\r\n */\r\n cookie(name: string, value: string | null, options?: CookieOptions): void;\r\n /** Set an HMAC-signed cookie. Requires `cookieSecret` in `giri.config`. */\r\n signedCookie(name: string, value: string, options?: CookieOptions): Promise<void>;\r\n}\r\n\r\nexport type Handle<\r\n Params extends Record<string, string> = Record<string, string>,\r\n Input extends ValidatedInput = ValidatedInput,\r\n Vars extends Record<string, unknown> = {},\r\n> = (c: Context<Params, Input, Vars>) => HandlerResponse | Promise<HandlerResponse>;\r\n\r\nexport type Next = () => Promise<HandlerResponse | void>;\r\n\r\n/** An OpenAPI security requirement, e.g. `{ bearerAuth: [] }`. */\r\nexport type SecurityRequirement = Record<string, string[]>;\r\n\r\nexport interface MiddlewareOpenApi {\r\n /** Security requirements this middleware enforces */\r\n security?: SecurityRequirement[];\r\n /** Optional scheme definitions, merged into `components.securitySchemes` so the doc is self-contained. */\r\n securitySchemes?: Record<string, unknown>;\r\n [key: string]: unknown;\r\n}\r\n\r\nexport interface MiddlewareOptions {\r\n /** Validate and expose this request body before the middleware chain runs. */\r\n body?: GiriBodySchema;\r\n /** Validate and expose these query parameters before the middleware chain runs. */\r\n query?: GiriInputSchema;\r\n openapi?: MiddlewareOpenApi;\r\n}\r\n\r\ndeclare const middlewareTypesBrand: unique symbol;\r\n\r\nexport interface Middleware<\r\n Params extends Record<string, string> = Record<string, string>,\r\n Input extends ValidatedInput = ValidatedInput,\r\n Vars extends Record<string, unknown> = {},\r\n> {\r\n (c: Context<Params, Input, Vars>, next: Next): HandlerResponse | void | Promise<HandlerResponse | void>;\r\n readonly [middlewareTypesBrand]?: {\r\n params: Params;\r\n input: Input;\r\n vars: Vars;\r\n };\r\n body?: GiriBodySchema;\r\n query?: GiriInputSchema;\r\n openapi?: MiddlewareOpenApi;\r\n}\r\n\r\n/** The context vars a middleware injects (its `Vars` type parameter). */\r\nexport type VarsOf<M> = M extends {\r\n readonly [middlewareTypesBrand]?: { vars: infer V extends Record<string, unknown> };\r\n}\r\n ? V\r\n : {};\r\n\r\n/** Intersect the injected vars of a tuple of middleware (built with `stack(...)`). */\r\nexport type MergeStack<T> = T extends readonly [infer Head, ...infer Rest]\r\n ? VarsOf<Head> & MergeStack<Rest>\r\n : {};\r\n\r\n/**\r\n * Merge the injected vars of a `middleware` export. A `stack(...)` tuple is merged element-wise;\r\n * a single bare middleware (`export const middleware = fromHono(...)`) contributes its own vars; a\r\n * plain `Middleware[]` (not a `stack(...)` tuple) contributes nothing - its element types are lost.\r\n */\r\nexport type InferStackVars<T> = T extends readonly [unknown, ...unknown[]]\r\n ? MergeStack<T>\r\n : T extends Middleware<any, any, any>\r\n ? VarsOf<T>\r\n : {};\r\n\r\n/**\r\n * The vars injected by a module own `middleware` export (a `stack(...)`). Used by the\r\n * generated per-method handle so a verb file's own `export const middleware` types\r\n * `c.get`/`c.set`, on top of the folder's `+shared.ts` chain.\r\n */\r\nexport type MiddlewareVarsOf<M> = M extends { middleware: infer Stack }\r\n ? InferStackVars<Stack>\r\n : {};\r\n\r\n/** A JSON Schema object (JSON Schema 2020-12 / OpenAPI 3.1 dialect). */\r\nexport type JsonSchema = Record<string, unknown>;\r\n\r\nexport const inputSchemaBrand: unique symbol = Symbol.for('giri.input-schema') as never;\r\n\r\nexport type InputValidationResult<Output = unknown> =\r\n | { ok: true; value: Output }\r\n | { ok: false; issues: unknown };\r\n\r\n/**\r\n * A input schema every wrapper form (`body`/`query`) export takes. A vendor\r\n * adapter (`@boon4681/giri/validators/zod`, `@boon4681/giri/validators/valibot`, …) returns one; build a\r\n * custom one with `defineInputSchema`. giri core depends only on this interface, never\r\n * on a validator library. `validate` is the runtime check; `toJsonSchema` feeds OpenAPI.\r\n */\r\nexport interface GiriInputSchema<Output = unknown> {\r\n readonly [inputSchemaBrand]: true;\r\n validate(value: unknown): InputValidationResult<Output> | Promise<InputValidationResult<Output>>;\r\n toJsonSchema(): JsonSchema;\r\n}\r\n\r\n/** Extract the validated output type of a giri input schema: `Infer<typeof body>`. */\r\nexport type Infer<T> = T extends GiriInputSchema<infer Output> ? Output : never;\r\n\r\nexport type BodyContentType = 'json' | 'form' | 'urlencoded' | 'text';\r\n\r\nexport const bodySchemaBrand: unique symbol = Symbol.for('giri.body-schema') as never;\r\n\r\n/**\r\n * A request body declared as a set of accepted content-types wrapped form `body`\r\n * takes (`zod.body({ json, form })`). One key means that encoding only; several mean the\r\n * endpoint accepts any of them, dispatched at runtime on the request `Content-Type`.\r\n * Each entry is a plain `GiriInputSchema`, so `validate`/`toJsonSchema` work per content-type.\r\n */\r\nexport interface GiriBodySchema<\r\n Outputs extends Partial<Record<BodyContentType, unknown>> = Partial<Record<BodyContentType, unknown>>,\r\n> {\r\n readonly [bodySchemaBrand]: true;\r\n readonly contents: { [K in keyof Outputs & BodyContentType]: GiriInputSchema<Outputs[K]> };\r\n}\r\n\r\n/** True when `T` is a union of more than one member. */\r\ntype IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\r\n\r\n/**\r\n * The validated body a handler receives. A single declared content-type yields that\r\n * schema's output directly; several yield a discriminated union keyed by content-type.\r\n */\r\nexport type ValidBody<B> = B extends GiriBodySchema<infer Outputs>\r\n ? IsUnion<keyof Outputs> extends true\r\n ? { [K in keyof Outputs]: { type: K; data: Outputs[K] } }[keyof Outputs]\r\n : Outputs[keyof Outputs]\r\n : never;\r\n\r\n/** The validated query a handler receives. */\r\nexport type ValidQuery<Q> = Q extends GiriInputSchema<infer Output> ? Output : never;\r\n\r\n/** Drop keys whose value resolved to `never` (an input the route didn't declare). */\r\ntype PruneNever<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] };\r\n\r\n/** Derive validated input from middleware metadata added by `defineMiddleware({ body, query }, ...)`. */\r\nexport type InputOfMiddleware<M> = PruneNever<{\r\n body: M extends { body: infer B } ? ValidBody<B> : never;\r\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\r\n}>;\r\n\r\n/** Intersect the validated inputs contributed by a tuple built with `stack(...)`. */\r\nexport type MergeStackInput<T> = T extends readonly [infer Head, ...infer Rest]\r\n ? InputOfMiddleware<Head> & MergeStackInput<Rest>\r\n : {};\r\n\r\n/** Derive validated input contributed by a middleware function or stack. */\r\nexport type InferStackInput<T> = T extends readonly [unknown, ...unknown[]]\r\n ? MergeStackInput<T>\r\n : InputOfMiddleware<T>;\r\n\r\n/** The validated input contributed by a module's own `middleware` export. */\r\nexport type MiddlewareInputOf<M> = M extends { middleware: infer Stack }\r\n ? InferStackInput<Stack>\r\n : {};\r\n\r\n/** Derive the callback input type from `defineMiddleware` options. */\r\nexport type MiddlewareOptionsInput<O> = PruneNever<{\r\n body: O extends { body: infer B } ? ValidBody<B> : never;\r\n query: O extends { query: infer Q } ? ValidQuery<Q> : never;\r\n}>;\r\n\r\n/**\r\n * Derive a route's `ValidatedInput` from a module's `body`/`query` exports. The generated\r\n * per-method `$types` handle (`POST`, `GET`, …) uses this so handlers infer `c.req.valid`\r\n * with no manual generic.\r\n */\r\nexport type RouteInputOf<M> = PruneNever<{\r\n body: M extends { body: infer B } ? ValidBody<B> : never;\r\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\r\n}>;\r\n\r\nexport interface RouteInput {\r\n // Each owner (route export + every applied middleware) contributes a validator; the\r\n // results are merged, matching the type layer which intersects them. Order is middleware\r\n // first, then the route, so a route's fields win on key collisions.\r\n body?: GiriBodySchema[];\r\n query?: GiriInputSchema[];\r\n}\r\n\r\nexport interface RouteOpenApi {\r\n /** Omit this route from the generated `openapi.json` (it still serves normally). */\r\n hidden?: boolean;\r\n /**\r\n * OpenAPI tags - the grouping in doc viewers. On a `+shared.ts` they apply to every route in the\r\n * folder; the chain is merged and de-duplicated, so a route's tags add to\r\n * its folders'.\r\n */\r\n tags?: string[];\r\n /** Short operation summary. Cascades down the chain (a verb file overrides its folders). */\r\n summary?: string;\r\n /** Longer operation description. Cascades down the chain (a verb file overrides its folders). */\r\n description?: string;\r\n /** Marks the operation(s) deprecated. On a `+shared.ts` it deprecates the whole folder. */\r\n deprecated?: boolean;\r\n /** Unique operationId. Verb-file only - it is never inherited from a `+shared.ts`. */\r\n operationId?: string;\r\n}\r\n\r\nexport type RouteOpenApiConfig = RouteOpenApi | boolean;\r\n\r\nexport interface GiriRouteRegistration {\r\n method: HttpMethod;\r\n path: string;\r\n handle: Handle;\r\n middleware: Middleware[];\r\n input?: RouteInput;\r\n /** App-wide services to seed onto `c.app` (same instance for every route). */\r\n services?: Services;\r\n /** Secret for signing/verifying cookies (`c.signedCookie`), from `config.cookieSecret`. */\r\n cookieSecret?: string;\r\n}\r\n\r\nexport type GiriFetchHandler = (req: Request) => Response | Promise<Response>;\r\n\r\nexport interface GiriServeOptions {\r\n port: number;\r\n hostname?: string;\r\n}\r\n\r\nexport interface GiriServerInfo {\r\n address: string;\r\n port: number;\r\n}\r\n\r\nexport interface GiriServer {\r\n close(): void | Promise<void>;\r\n}\r\n\r\nexport interface GiriAdapter<App> {\r\n name?: string;\r\n createApp(): App;\r\n register(app: App, route: GiriRouteRegistration): void;\r\n fetch(app: App, req: Request): Promise<Response>;\r\n /**\r\n * Bind the configured backend's runtime to a port and start serving.\r\n * giri core stays runtime-agnostic: it hands the adapter a request handler\r\n * (so hot-reload keeps working) and the adapter owns the actual server.\r\n */\r\n serve(\r\n handler: GiriFetchHandler,\r\n options: GiriServeOptions,\r\n onListen?: (info: GiriServerInfo) => void,\r\n ): GiriServer;\r\n}\r\n\r\nexport interface GiriConfig<App = unknown> {\r\n adapter: GiriAdapter<App>;\r\n alias?: Record<string, string | string[]>;\r\n outDir?: string;\r\n server?: {\r\n port?: number;\r\n hostname?: string;\r\n };\r\n errorSchema?: unknown;\r\n /** Secret used to sign/verify cookies via `c.signedCookie` / `c.req.signedCookie`. */\r\n cookieSecret?: string;\r\n}\r\n\r\nexport interface GiriPaths {\r\n cwd: string;\r\n routesDir: string;\r\n outDir: string;\r\n}\r\n","import {\n type BodyContentType,\n type GiriBodySchema,\n type GiriInputSchema,\n type RouteInput,\n type TypedResponse,\n type ValidatedInput,\n bodySchemaBrand,\n inputSchemaBrand,\n} from './types';\nimport { createTypedResponse } from './context';\n\ninterface PreparedInput {\n ok: true;\n validated: ValidatedInput;\n}\n\ninterface FailedInput {\n ok: false;\n response: TypedResponse<{ message: string; issues: unknown }, 400 | 415, 'json'>;\n}\n\nexport type PreparedRequestInput = PreparedInput | FailedInput;\n\nexport interface RouteInputSource {\n label: string;\n body?: unknown;\n query?: unknown;\n}\n\n/**\n * Build a giri input schema from a `validate` + `toJsonSchema` pair. Vendor adapters use\n * this; you can call it directly to make a custom validator. The brand is a global symbol,\n * so a hand-rolled `{ [Symbol.for(\"giri.input-schema\")]: true, validate, toJsonSchema }` works too.\n */\nexport function defineInputSchema<Output>(\n schema: Omit<GiriInputSchema<Output>, typeof inputSchemaBrand>,\n): GiriInputSchema<Output> {\n return { [inputSchemaBrand]: true, ...schema };\n}\n\nexport function isGiriInputSchema(value: unknown): value is GiriInputSchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[inputSchemaBrand] === true,\n );\n}\n\n/**\n * Build a giri body schema from per-content-type input schemas. Validator adapters use this `zod.body({ json, form })`\n */\nexport function defineBodySchema<Outputs extends Partial<Record<BodyContentType, unknown>>>(\n contents: GiriBodySchema<Outputs>['contents'],\n): GiriBodySchema<Outputs> {\n return { [bodySchemaBrand]: true, contents };\n}\n\nexport function isGiriBodySchema(value: unknown): value is GiriBodySchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[bodySchemaBrand] === true,\n );\n}\n\n/**\n * A route/middleware declared a target (`body`/`query`) without wrapping it in a validator.\n * Branded so callers (e.g. the generator) can tell an actionable config error apart from a\n * route that merely failed to load.\n */\nexport class RouteInputError extends Error {\n override readonly name = 'RouteInputError';\n}\n\n/**\n * Collect every owner's validator for each target (`body`/`query`). A route export and any\n * applied middleware can each contribute one; their validated outputs are merged at request time\n * (see {@link prepareRequestInput}), matching the type layer which intersects them. Owners are\n * kept in source order (middleware first, then the route) so a route's fields win on collision.\n */\nexport function resolveRouteInput(sources: readonly RouteInputSource[]): RouteInput | undefined {\n const body: GiriBodySchema[] = [];\n const query: GiriInputSchema[] = [];\n\n for (const source of sources) {\n if (source.body !== undefined) {\n if (!isGiriBodySchema(source.body)) {\n throw new RouteInputError(\n `${source.label}: \"body\" must be wrapped with a validator, e.g. \\`zod.body({ json: ... })\\` from @boon4681/giri/validators/zod.`,\n );\n }\n body.push(source.body);\n }\n\n if (source.query !== undefined) {\n if (!isGiriInputSchema(source.query)) {\n throw new RouteInputError(\n `${source.label}: \"query\" must be wrapped with a validator, e.g. \\`zod.query(...)\\` from @boon4681/giri/validators/zod.`,\n );\n }\n query.push(source.query);\n }\n }\n\n const input: RouteInput = {};\n if (body.length > 0) {\n input.body = body;\n }\n if (query.length > 0) {\n input.query = query;\n }\n return input.body || input.query ? input : undefined;\n}\n\nconst MIME_TO_CONTENT_TYPE: Record<string, BodyContentType> = {\n 'application/json': 'json',\n 'multipart/form-data': 'form',\n 'application/x-www-form-urlencoded': 'urlencoded',\n 'text/plain': 'text',\n};\n\nfunction contentTypeFromHeader(header: string | null): BodyContentType | undefined {\n if (!header) {\n return undefined;\n }\n const mime = header.split(';', 1)[0].trim().toLowerCase();\n return MIME_TO_CONTENT_TYPE[mime];\n}\n\n/** Flatten a `FormData` into a plain object, collapsing repeated fields into arrays. */\nfunction formDataObject(form: FormData): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n form.forEach((value, key) => {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n });\n return result;\n}\n\nasync function readRawBody(request: Request, contentType: BodyContentType): Promise<unknown> {\n const cloned = request.clone();\n if (contentType === 'json') {\n return cloned.json();\n }\n if (contentType === 'text') {\n return cloned.text();\n }\n return formDataObject(await cloned.formData());\n}\n\nfunction queryObject(url: URL): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const [key, value] of url.searchParams) {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n }\n return result;\n}\n\n/** Shallow-merge validated outputs from several owners; a single owner keeps its exact value. */\nfunction mergeValidated(values: unknown[]): unknown {\n if (values.length === 1) {\n return values[0];\n }\n return Object.assign({}, ...values);\n}\n\nexport async function prepareRequestInput(request: Request, input?: RouteInput): Promise<PreparedRequestInput> {\n const validated: ValidatedInput = {};\n\n if (input?.query && input.query.length > 0) {\n const query = queryObject(new URL(request.url));\n const values: unknown[] = [];\n for (const schema of input.query) {\n const result = await schema.validate(query);\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid query parameters.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n values.push(result.value);\n }\n validated.query = mergeValidated(values);\n }\n\n if (input?.body && input.body.length > 0) {\n // The set of acceptable content-types is the union across every body owner.\n const declared = [\n ...new Set(input.body.flatMap((schema) => Object.keys(schema.contents) as BodyContentType[])),\n ];\n const requested = contentTypeFromHeader(request.headers.get('content-type'));\n // Pick the type matching the request's content-type; fall back to JSON when the header is\n // missing/unrecognized but JSON is on offer (so header-less posts still work).\n const chosen: BodyContentType | undefined =\n requested && declared.includes(requested) ? requested : declared.includes('json') ? 'json' : undefined;\n\n if (!chosen) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Unsupported media type.', issues: { accepted: declared } },\n 415,\n 'json',\n ),\n };\n }\n\n let rawBody: unknown;\n try {\n rawBody = await readRawBody(request, chosen);\n } catch (error) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: error },\n 400,\n 'json',\n ),\n };\n }\n\n const values: unknown[] = [];\n for (const schema of input.body) {\n const contents = schema.contents as Partial<Record<BodyContentType, GiriInputSchema>>;\n const contentSchema = contents[chosen];\n // An owner that doesn't declare the chosen content-type simply contributes nothing.\n if (!contentSchema) {\n continue;\n }\n const result = await contentSchema.validate(rawBody);\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n values.push(result.value);\n }\n\n const data = mergeValidated(values);\n validated.body = declared.length > 1 ? { type: chosen, data } : data;\n }\n\n return { ok: true, validated };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAkB;;;ACmQX,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;ACvPpE,SAAS,kBACZ,QACuB;AACvB,SAAO,EAAE,CAAC,gBAAgB,GAAG,MAAM,GAAG,OAAO;AACjD;AAaO,SAAS,iBACZ,UACuB;AACvB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,SAAS;AAC/C;;;AFnDA,SAAS,KAA+B,QAAkD;AACtF,SAAO,kBAAmC;AAAA,IACtC,SAAS,OAAO;AACZ,YAAM,SAAS,OAAO,UAAU,KAAK;AACrC,aAAO,OAAO,UACR,EAAE,IAAI,MAAM,OAAO,OAAO,KAAK,IAC/B,EAAE,IAAI,OAAO,QAAQ,OAAO,MAAM;AAAA,IAC5C;AAAA,IACA,eAAe;AAGX,aAAO,aAAE,aAAa,QAAQ,EAAE,iBAAiB,MAAM,CAAC;AAAA,IAC5D;AAAA,EACJ,CAAC;AACL;AAmBO,IAAM,MAAM;AAAA,EACf,KACI,KACwF;AACxF,UAAM,WAAW,CAAC;AAClB,eAAW,CAAC,aAAa,MAAM,KAAK,OAAO,QAAQ,GAAG,GAAG;AACrD,UAAI,QAAQ;AACR,iBAAS,WAA8B,IAAI,KAAK,MAAM;AAAA,MAC1D;AAAA,IACJ;AACA,WAAO,iBAAiB,QAAQ;AAAA,EAGpC;AAAA,EACA,MAAgC,QAAkD;AAC9E,WAAO,KAAK,MAAM;AAAA,EACtB;AACJ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boon4681/giri",
3
- "version": "0.0.3-alpha-11",
3
+ "version": "0.0.3-alpha-13",
4
4
  "license": "MIT",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",