@richie-rpc/core 1.2.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/index.mjs.map +2 -2
- package/dist/mjs/package.json +1 -1
- package/dist/types/index.d.ts +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ const contract = defineContract({
|
|
|
23
23
|
params: z.object({ id: z.string() }),
|
|
24
24
|
responses: {
|
|
25
25
|
[Status.OK]: z.object({ id: z.string(), name: z.string() }),
|
|
26
|
+
},
|
|
27
|
+
errorResponses: {
|
|
26
28
|
[Status.NotFound]: z.object({ error: z.string() }),
|
|
27
29
|
},
|
|
28
30
|
},
|
|
@@ -33,6 +35,9 @@ const contract = defineContract({
|
|
|
33
35
|
responses: {
|
|
34
36
|
[Status.Created]: z.object({ id: z.string(), name: z.string(), email: z.string() }),
|
|
35
37
|
},
|
|
38
|
+
errorResponses: {
|
|
39
|
+
[Status.BadRequest]: z.object({ error: z.string() }),
|
|
40
|
+
},
|
|
36
41
|
},
|
|
37
42
|
});
|
|
38
43
|
```
|
|
@@ -48,7 +53,8 @@ Each endpoint can have:
|
|
|
48
53
|
- `headers` (optional): Zod schema for request headers
|
|
49
54
|
- `body` (optional): Zod schema for request body
|
|
50
55
|
- `contentType` (optional): Request content type (`'application/json'` or `'multipart/form-data'`)
|
|
51
|
-
- `responses` (required): Object mapping status codes to Zod schemas
|
|
56
|
+
- `responses` (required): Object mapping success status codes to Zod schemas
|
|
57
|
+
- `errorResponses` (optional): Object mapping error status codes to Zod schemas. Error responses are thrown as `ErrorResponse` by the client instead of being returned as data, enabling clean `useSuspenseQuery` patterns where `data` is always the success type.
|
|
52
58
|
|
|
53
59
|
### Streaming Endpoint
|
|
54
60
|
|
|
@@ -258,6 +264,8 @@ const contract = defineContract({
|
|
|
258
264
|
params: z.object({ id: z.string() }),
|
|
259
265
|
responses: {
|
|
260
266
|
[Status.OK]: UserSchema,
|
|
267
|
+
},
|
|
268
|
+
errorResponses: {
|
|
261
269
|
[Status.NotFound]: ErrorSchema,
|
|
262
270
|
},
|
|
263
271
|
},
|
|
@@ -311,6 +319,8 @@ The package exports several utility types for extracting types from endpoint def
|
|
|
311
319
|
- `ExtractBody<T>`: Extract request body type
|
|
312
320
|
- `ExtractResponses<T>`: Extract all response types
|
|
313
321
|
- `ExtractResponse<T, Status>`: Extract specific response type by status code
|
|
322
|
+
- `ExtractErrorResponses<T>`: Extract all error response types
|
|
323
|
+
- `ExtractErrorResponse<T, Status>`: Extract specific error response type by status code
|
|
314
324
|
|
|
315
325
|
## Links
|
|
316
326
|
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type { z } from 'zod';\n\n// HTTP methods supported\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n\n// Content types supported for request bodies\nexport type ContentType = 'application/json' | 'multipart/form-data';\n\n// HTTP status codes as const object for type-safe responses without 'as const'\nexport const Status = {\n // Success responses\n OK: 200 as const,\n Created: 201 as const,\n Accepted: 202 as const,\n NoContent: 204 as const,\n\n // Redirection\n MovedPermanently: 301 as const,\n Found: 302 as const,\n NotModified: 304 as const,\n\n // Client errors\n BadRequest: 400 as const,\n Unauthorized: 401 as const,\n Forbidden: 403 as const,\n NotFound: 404 as const,\n MethodNotAllowed: 405 as const,\n Conflict: 409 as const,\n UnprocessableEntity: 422 as const,\n TooManyRequests: 429 as const,\n\n // Server errors\n InternalServerError: 500 as const,\n NotImplemented: 501 as const,\n BadGateway: 502 as const,\n ServiceUnavailable: 503 as const,\n GatewayTimeout: 504 as const,\n} as const;\n\n// Base fields shared by all endpoint types\ninterface BaseEndpointFields {\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n}\n\n// Standard HTTP endpoint\nexport interface StandardEndpointDefinition extends BaseEndpointFields {\n type: 'standard';\n method: HttpMethod;\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n responses: Record<number, z.ZodTypeAny>;\n}\n\n// Streaming response endpoint (NDJSON)\nexport interface StreamingEndpointDefinition extends BaseEndpointFields {\n type: 'streaming';\n method: 'POST';\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n /** Schema for each NDJSON chunk (type inference only, not validated) */\n chunk: z.ZodTypeAny;\n /** Optional final response after stream ends */\n finalResponse?: z.ZodTypeAny;\n /** Error responses for non-streaming failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// SSE endpoint\nexport interface SSEEndpointDefinition extends BaseEndpointFields {\n type: 'sse';\n method: 'GET';\n /** Event types: key = event name, value = data schema (type inference only) */\n events: Record<string, z.ZodTypeAny>;\n /** Error responses for connection failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Download endpoint (binary file response)\nexport interface DownloadEndpointDefinition extends BaseEndpointFields {\n type: 'download';\n method: 'GET'; // Downloads are GET-only\n /** Error responses (non-2xx status codes) */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Union of all endpoint types\nexport type AnyEndpointDefinition =\n | StandardEndpointDefinition\n | StreamingEndpointDefinition\n | SSEEndpointDefinition\n | DownloadEndpointDefinition;\n\n// Alias for backwards compatibility in type utilities\nexport type EndpointDefinition = AnyEndpointDefinition;\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, AnyEndpointDefinition>;\n\n// Extract the Zod type from a schema\nexport type InferZodType<T> = T extends z.ZodTypeAny ? z.infer<T> : never;\n\n// Extract params type from endpoint\nexport type ExtractParams<T extends EndpointDefinition> = T['params'] extends z.ZodTypeAny\n ? InferZodType<T['params']>\n : never;\n\n// Extract query type from endpoint\nexport type ExtractQuery<T extends EndpointDefinition> = T['query'] extends z.ZodTypeAny\n ? InferZodType<T['query']>\n : never;\n\n// Extract headers type from endpoint\nexport type ExtractHeaders<T extends EndpointDefinition> = T['headers'] extends z.ZodTypeAny\n ? InferZodType<T['headers']>\n : never;\n\n// Extract body type from endpoint (only standard and streaming have body)\nexport type ExtractBody<T extends EndpointDefinition> = T extends { body: z.ZodTypeAny }\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes (only standard has responses)\nexport type ExtractResponses<T extends EndpointDefinition> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n }\n : never;\n\n// Extract a specific response type by status code (only standard has responses)\nexport type ExtractResponse<T extends EndpointDefinition, Status extends number> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never\n : never;\n\n// Extract chunk type from streaming endpoint\nexport type ExtractChunk<T extends StreamingEndpointDefinition> = T['chunk'] extends z.ZodTypeAny\n ? InferZodType<T['chunk']>\n : never;\n\n// Extract final response type from streaming endpoint\nexport type ExtractFinalResponse<T extends StreamingEndpointDefinition> =\n T['finalResponse'] extends z.ZodTypeAny ? InferZodType<T['finalResponse']> : undefined;\n\n// Extract SSE event union type\nexport type ExtractSSEEvents<T extends SSEEndpointDefinition> = {\n [K in keyof T['events']]: {\n event: K;\n data: T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n id?: string;\n };\n}[keyof T['events']];\n\n// Extract specific SSE event data type\nexport type ExtractSSEEventData<\n T extends SSEEndpointDefinition,\n K extends keyof T['events'],\n> = T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n\n// Extract error responses for download endpoint\nexport type ExtractDownloadErrorResponse<\n T extends DownloadEndpointDefinition,\n Status extends keyof T['errorResponses'],\n> =\n T['errorResponses'] extends Record<number, z.ZodTypeAny>\n ? Status extends keyof T['errorResponses']\n ? T['errorResponses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['errorResponses'][Status]>\n : never\n : never\n : never;\n\n// Upload progress event\nexport interface UploadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal)\n}\n\n// Download progress event\nexport interface DownloadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal), NaN if total unknown\n}\n\n// Path parameter extraction utilities\n// Extract regular path params (:param)\ntype ExtractRegularParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractRegularParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Extract wildcard params (*param) - must be at end of path\ntype ExtractWildcardParams<T extends string> = T extends `${infer _Start}*${infer Param}`\n ? Param\n : never;\n\n// Combined extraction of all path params\nexport type ExtractPathParams<T extends string> =\n | ExtractRegularParams<T>\n | ExtractWildcardParams<T>;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Detailed path parameter info distinguishing regular vs wildcard params\n */\nexport interface ParsedPathParams {\n regular: string[];\n wildcard: string | null;\n}\n\n/**\n * Parse path parameters with detailed type information\n * e.g., \"/users/:id/*path\" => { regular: [\"id\"], wildcard: \"path\" }\n */\nexport function parsePathParamsDetailed(path: string): ParsedPathParams {\n const regular: string[] = [];\n\n // Extract regular params (:param)\n const regularMatches = path.match(/:([^/]+)/g);\n if (regularMatches) {\n regular.push(...regularMatches.map((match) => match.slice(1)));\n }\n\n // Extract wildcard param (*param) - must be at end\n const wildcardMatch = path.match(/\\*([^/]+)$/);\n\n return {\n regular,\n wildcard: wildcardMatch?.[1] ?? null,\n };\n}\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n * e.g., \"/files/*path\" => [\"path\"]\n */\nexport function parsePathParams(path: string): string[] {\n const { regular, wildcard } = parsePathParamsDetailed(path);\n return wildcard ? [...regular, wildcard] : regular;\n}\n\n/**\n * Match a URL path against a pattern and extract parameters\n * e.g., matchPath(\"/users/:id\", \"/users/123\") => { id: \"123\" }\n * e.g., matchPath(\"/files/*path\", \"/files/a/b/c.txt\") => { path: \"a/b/c.txt\" }\n */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const { regular, wildcard } = parsePathParamsDetailed(pattern);\n\n // Convert pattern to regex:\n // 1. Replace :param with capturing group for single segment\n // 2. Replace *param with capturing group for remaining path (including slashes)\n let regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\n\n // Handle wildcard at the end - replace *paramName with (.+)\n if (wildcard) {\n regexPattern = regexPattern.replace(/\\*[^/]+$/, '(.+)');\n }\n\n const regex = new RegExp(`^${regexPattern}$`);\n const match = path.match(regex);\n\n if (!match) return null;\n\n const params: Record<string, string> = {};\n\n // Extract regular params first\n regular.forEach((name, index) => {\n params[name] = match[index + 1] ?? '';\n });\n\n // Extract wildcard param if present (always last capture group)\n if (wildcard) {\n params[wildcard] = match[regular.length + 1] ?? '';\n }\n\n return params;\n}\n\n/**\n * Interpolate path parameters into a URL pattern\n * e.g., interpolatePath(\"/users/:id\", { id: \"123\" }) => \"/users/123\"\n * e.g., interpolatePath(\"/files/*path\", { path: \"a/b/c.txt\" }) => \"/files/a/b/c.txt\"\n */\nexport function interpolatePath(pattern: string, params: Record<string, string | number>): string {\n let result = pattern;\n for (const [key, value] of Object.entries(params)) {\n // Try replacing :param first, then *param\n if (result.includes(`:${key}`)) {\n result = result.replace(`:${key}`, String(value));\n } else if (result.includes(`*${key}`)) {\n result = result.replace(`*${key}`, String(value));\n }\n }\n return result;\n}\n\n/**\n * Validate a path pattern for correct syntax\n * @throws Error if pattern is invalid\n */\nexport function validatePathPattern(pattern: string): void {\n // Check for wildcard not at the end\n const wildcardIndex = pattern.indexOf('*');\n if (wildcardIndex !== -1) {\n const afterWildcard = pattern.slice(wildcardIndex + 1);\n\n // Wildcard param name should go to end of string (no more slashes)\n if (afterWildcard.includes('/')) {\n throw new Error(\n `Invalid path pattern \"${pattern}\": Wildcard parameter must be the last segment`,\n );\n }\n\n // Ensure wildcard has a valid name (like Express 5 requires)\n if (!/^\\*[a-zA-Z_][a-zA-Z0-9_]*$/.test(pattern.slice(wildcardIndex))) {\n throw new Error(\n `Invalid path pattern \"${pattern}\": Wildcard parameter must have a valid name (e.g., *filePath)`,\n );\n }\n }\n\n // Check for multiple wildcards\n const wildcardCount = (pattern.match(/\\*/g) || []).length;\n if (wildcardCount > 1) {\n throw new Error(`Invalid path pattern \"${pattern}\": Only one wildcard parameter is allowed`);\n }\n}\n\n/**\n * Build a complete URL with query parameters\n */\nexport function buildUrl(\n baseUrl: string,\n path: string,\n query?: Record<string, string | number | boolean | string[]>,\n): string {\n // Normalize baseUrl - remove trailing slash\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n\n // Ensure path starts with /\n const normalizedPath = path.startsWith('/') ? path : `/${path}`;\n\n // Concatenate base and path\n const fullPath = normalizedBase + normalizedPath;\n\n const url = new URL(fullPath);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n for (const v of value) {\n url.searchParams.append(key, String(v));\n }\n } else {\n url.searchParams.append(key, String(value));\n }\n }\n }\n }\n\n return url.toString();\n}\n\n/**\n * Parse query parameters from URLSearchParams\n */\nexport function parseQuery(searchParams: URLSearchParams): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of searchParams.entries()) {\n const existing = result[key];\n if (existing) {\n if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result[key] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n// Type helper to ensure a value is a valid contract\nexport function defineContract<T extends Contract>(contract: T): T {\n return contract;\n}\n\n/**\n * Convert an object to FormData using the hybrid JSON + Files approach.\n * Files are extracted and replaced with { __fileRef__: \"path\" } placeholders.\n * The resulting FormData contains __json__ with the serialized structure\n * and individual file entries at their path keys.\n */\nexport function objectToFormData(obj: Record<string, unknown>): FormData {\n const formData = new FormData();\n const files: Array<{ path: string; file: File }> = [];\n\n function traverse(value: unknown, path: string): unknown {\n if (value instanceof File) {\n files.push({ path, file: value });\n return { __fileRef__: path };\n }\n if (Array.isArray(value)) {\n return value.map((item, i) => traverse(item, `${path}.${i}`));\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = traverse(v, path ? `${path}.${k}` : k);\n }\n return result;\n }\n return value;\n }\n\n const jsonWithRefs = traverse(obj, '');\n formData.append('__json__', JSON.stringify(jsonWithRefs));\n\n for (const { path, file } of files) {\n formData.append(path, file);\n }\n\n return formData;\n}\n\n/**\n * Parse FormData back to an object, reconstructing the structure with File objects.\n * Expects FormData created by objectToFormData with __json__ and file entries.\n * Falls back to simple Object.fromEntries for FormData without __json__.\n */\nexport function formDataToObject(formData: FormData): Record<string, unknown> {\n const jsonStr = formData.get('__json__');\n if (typeof jsonStr !== 'string') {\n return Object.fromEntries(formData.entries());\n }\n\n const obj = JSON.parse(jsonStr);\n\n function replaceRefs(value: unknown): unknown {\n if (value && typeof value === 'object' && '__fileRef__' in value) {\n const path = (value as { __fileRef__: string }).__fileRef__;\n return formData.get(path);\n }\n if (Array.isArray(value)) {\n return value.map(replaceRefs);\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = replaceRefs(v);\n }\n return result;\n }\n return value;\n }\n\n return replaceRefs(obj) as Record<string, unknown>;\n}\n\n// Re-export WebSocket types\nexport * from './websocket.cjs';\n"
|
|
5
|
+
"import type { z } from 'zod';\n\n// HTTP methods supported\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n\n// Content types supported for request bodies\nexport type ContentType = 'application/json' | 'multipart/form-data';\n\n// HTTP status codes as const object for type-safe responses without 'as const'\nexport const Status = {\n // Success responses\n OK: 200 as const,\n Created: 201 as const,\n Accepted: 202 as const,\n NoContent: 204 as const,\n\n // Redirection\n MovedPermanently: 301 as const,\n Found: 302 as const,\n NotModified: 304 as const,\n\n // Client errors\n BadRequest: 400 as const,\n Unauthorized: 401 as const,\n Forbidden: 403 as const,\n NotFound: 404 as const,\n MethodNotAllowed: 405 as const,\n Conflict: 409 as const,\n UnprocessableEntity: 422 as const,\n TooManyRequests: 429 as const,\n\n // Server errors\n InternalServerError: 500 as const,\n NotImplemented: 501 as const,\n BadGateway: 502 as const,\n ServiceUnavailable: 503 as const,\n GatewayTimeout: 504 as const,\n} as const;\n\n// Base fields shared by all endpoint types\ninterface BaseEndpointFields {\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n}\n\n// Standard HTTP endpoint\nexport interface StandardEndpointDefinition extends BaseEndpointFields {\n type: 'standard';\n method: HttpMethod;\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n responses: Record<number, z.ZodTypeAny>;\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Streaming response endpoint (NDJSON)\nexport interface StreamingEndpointDefinition extends BaseEndpointFields {\n type: 'streaming';\n method: 'POST';\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n /** Schema for each NDJSON chunk (type inference only, not validated) */\n chunk: z.ZodTypeAny;\n /** Optional final response after stream ends */\n finalResponse?: z.ZodTypeAny;\n /** Error responses for non-streaming failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// SSE endpoint\nexport interface SSEEndpointDefinition extends BaseEndpointFields {\n type: 'sse';\n method: 'GET';\n /** Event types: key = event name, value = data schema (type inference only) */\n events: Record<string, z.ZodTypeAny>;\n /** Error responses for connection failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Download endpoint (binary file response)\nexport interface DownloadEndpointDefinition extends BaseEndpointFields {\n type: 'download';\n method: 'GET'; // Downloads are GET-only\n /** Error responses (non-2xx status codes) */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Union of all endpoint types\nexport type AnyEndpointDefinition =\n | StandardEndpointDefinition\n | StreamingEndpointDefinition\n | SSEEndpointDefinition\n | DownloadEndpointDefinition;\n\n// Alias for backwards compatibility in type utilities\nexport type EndpointDefinition = AnyEndpointDefinition;\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, AnyEndpointDefinition>;\n\n// Extract the Zod type from a schema\nexport type InferZodType<T> = T extends z.ZodTypeAny ? z.infer<T> : never;\n\n// Extract params type from endpoint\nexport type ExtractParams<T extends EndpointDefinition> = T['params'] extends z.ZodTypeAny\n ? InferZodType<T['params']>\n : never;\n\n// Extract query type from endpoint\nexport type ExtractQuery<T extends EndpointDefinition> = T['query'] extends z.ZodTypeAny\n ? InferZodType<T['query']>\n : never;\n\n// Extract headers type from endpoint\nexport type ExtractHeaders<T extends EndpointDefinition> = T['headers'] extends z.ZodTypeAny\n ? InferZodType<T['headers']>\n : never;\n\n// Extract body type from endpoint (only standard and streaming have body)\nexport type ExtractBody<T extends EndpointDefinition> = T extends { body: z.ZodTypeAny }\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes (only standard has responses)\nexport type ExtractResponses<T extends EndpointDefinition> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n }\n : never;\n\n// Extract a specific response type by status code (only standard has responses)\nexport type ExtractResponse<T extends EndpointDefinition, Status extends number> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never\n : never;\n\n// Extract error response types for all status codes\nexport type ExtractErrorResponses<T extends EndpointDefinition> = T extends {\n errorResponses: Record<number, z.ZodTypeAny>;\n} ? {\n [K in keyof T['errorResponses']]: T['errorResponses'][K] extends z.ZodTypeAny\n ? InferZodType<T['errorResponses'][K]>\n : never;\n } : never;\n\n// Extract a specific error response type by status code\nexport type ExtractErrorResponse<T extends EndpointDefinition, Status extends number> = T extends {\n errorResponses: Record<number, z.ZodTypeAny>;\n} ? Status extends keyof T['errorResponses']\n ? T['errorResponses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['errorResponses'][Status]>\n : never\n : never\n : never;\n\n// Extract chunk type from streaming endpoint\nexport type ExtractChunk<T extends StreamingEndpointDefinition> = T['chunk'] extends z.ZodTypeAny\n ? InferZodType<T['chunk']>\n : never;\n\n// Extract final response type from streaming endpoint\nexport type ExtractFinalResponse<T extends StreamingEndpointDefinition> =\n T['finalResponse'] extends z.ZodTypeAny ? InferZodType<T['finalResponse']> : undefined;\n\n// Extract SSE event union type\nexport type ExtractSSEEvents<T extends SSEEndpointDefinition> = {\n [K in keyof T['events']]: {\n event: K;\n data: T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n id?: string;\n };\n}[keyof T['events']];\n\n// Extract specific SSE event data type\nexport type ExtractSSEEventData<\n T extends SSEEndpointDefinition,\n K extends keyof T['events'],\n> = T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n\n// Extract error responses for download endpoint\nexport type ExtractDownloadErrorResponse<\n T extends DownloadEndpointDefinition,\n Status extends keyof T['errorResponses'],\n> =\n T['errorResponses'] extends Record<number, z.ZodTypeAny>\n ? Status extends keyof T['errorResponses']\n ? T['errorResponses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['errorResponses'][Status]>\n : never\n : never\n : never;\n\n// Upload progress event\nexport interface UploadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal)\n}\n\n// Download progress event\nexport interface DownloadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal), NaN if total unknown\n}\n\n// Path parameter extraction utilities\n// Extract regular path params (:param)\ntype ExtractRegularParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractRegularParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Extract wildcard params (*param) - must be at end of path\ntype ExtractWildcardParams<T extends string> = T extends `${infer _Start}*${infer Param}`\n ? Param\n : never;\n\n// Combined extraction of all path params\nexport type ExtractPathParams<T extends string> =\n | ExtractRegularParams<T>\n | ExtractWildcardParams<T>;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Detailed path parameter info distinguishing regular vs wildcard params\n */\nexport interface ParsedPathParams {\n regular: string[];\n wildcard: string | null;\n}\n\n/**\n * Parse path parameters with detailed type information\n * e.g., \"/users/:id/*path\" => { regular: [\"id\"], wildcard: \"path\" }\n */\nexport function parsePathParamsDetailed(path: string): ParsedPathParams {\n const regular: string[] = [];\n\n // Extract regular params (:param)\n const regularMatches = path.match(/:([^/]+)/g);\n if (regularMatches) {\n regular.push(...regularMatches.map((match) => match.slice(1)));\n }\n\n // Extract wildcard param (*param) - must be at end\n const wildcardMatch = path.match(/\\*([^/]+)$/);\n\n return {\n regular,\n wildcard: wildcardMatch?.[1] ?? null,\n };\n}\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n * e.g., \"/files/*path\" => [\"path\"]\n */\nexport function parsePathParams(path: string): string[] {\n const { regular, wildcard } = parsePathParamsDetailed(path);\n return wildcard ? [...regular, wildcard] : regular;\n}\n\n/**\n * Match a URL path against a pattern and extract parameters\n * e.g., matchPath(\"/users/:id\", \"/users/123\") => { id: \"123\" }\n * e.g., matchPath(\"/files/*path\", \"/files/a/b/c.txt\") => { path: \"a/b/c.txt\" }\n */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const { regular, wildcard } = parsePathParamsDetailed(pattern);\n\n // Convert pattern to regex:\n // 1. Replace :param with capturing group for single segment\n // 2. Replace *param with capturing group for remaining path (including slashes)\n let regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\n\n // Handle wildcard at the end - replace *paramName with (.+)\n if (wildcard) {\n regexPattern = regexPattern.replace(/\\*[^/]+$/, '(.+)');\n }\n\n const regex = new RegExp(`^${regexPattern}$`);\n const match = path.match(regex);\n\n if (!match) return null;\n\n const params: Record<string, string> = {};\n\n // Extract regular params first\n regular.forEach((name, index) => {\n params[name] = match[index + 1] ?? '';\n });\n\n // Extract wildcard param if present (always last capture group)\n if (wildcard) {\n params[wildcard] = match[regular.length + 1] ?? '';\n }\n\n return params;\n}\n\n/**\n * Interpolate path parameters into a URL pattern\n * e.g., interpolatePath(\"/users/:id\", { id: \"123\" }) => \"/users/123\"\n * e.g., interpolatePath(\"/files/*path\", { path: \"a/b/c.txt\" }) => \"/files/a/b/c.txt\"\n */\nexport function interpolatePath(pattern: string, params: Record<string, string | number>): string {\n let result = pattern;\n for (const [key, value] of Object.entries(params)) {\n // Try replacing :param first, then *param\n if (result.includes(`:${key}`)) {\n result = result.replace(`:${key}`, String(value));\n } else if (result.includes(`*${key}`)) {\n result = result.replace(`*${key}`, String(value));\n }\n }\n return result;\n}\n\n/**\n * Validate a path pattern for correct syntax\n * @throws Error if pattern is invalid\n */\nexport function validatePathPattern(pattern: string): void {\n // Check for wildcard not at the end\n const wildcardIndex = pattern.indexOf('*');\n if (wildcardIndex !== -1) {\n const afterWildcard = pattern.slice(wildcardIndex + 1);\n\n // Wildcard param name should go to end of string (no more slashes)\n if (afterWildcard.includes('/')) {\n throw new Error(\n `Invalid path pattern \"${pattern}\": Wildcard parameter must be the last segment`,\n );\n }\n\n // Ensure wildcard has a valid name (like Express 5 requires)\n if (!/^\\*[a-zA-Z_][a-zA-Z0-9_]*$/.test(pattern.slice(wildcardIndex))) {\n throw new Error(\n `Invalid path pattern \"${pattern}\": Wildcard parameter must have a valid name (e.g., *filePath)`,\n );\n }\n }\n\n // Check for multiple wildcards\n const wildcardCount = (pattern.match(/\\*/g) || []).length;\n if (wildcardCount > 1) {\n throw new Error(`Invalid path pattern \"${pattern}\": Only one wildcard parameter is allowed`);\n }\n}\n\n/**\n * Build a complete URL with query parameters\n */\nexport function buildUrl(\n baseUrl: string,\n path: string,\n query?: Record<string, string | number | boolean | string[]>,\n): string {\n // Normalize baseUrl - remove trailing slash\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n\n // Ensure path starts with /\n const normalizedPath = path.startsWith('/') ? path : `/${path}`;\n\n // Concatenate base and path\n const fullPath = normalizedBase + normalizedPath;\n\n const url = new URL(fullPath);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n for (const v of value) {\n url.searchParams.append(key, String(v));\n }\n } else {\n url.searchParams.append(key, String(value));\n }\n }\n }\n }\n\n return url.toString();\n}\n\n/**\n * Parse query parameters from URLSearchParams\n */\nexport function parseQuery(searchParams: URLSearchParams): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of searchParams.entries()) {\n const existing = result[key];\n if (existing) {\n if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result[key] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n// Type helper to ensure a value is a valid contract\nexport function defineContract<T extends Contract>(contract: T): T {\n return contract;\n}\n\n/**\n * Convert an object to FormData using the hybrid JSON + Files approach.\n * Files are extracted and replaced with { __fileRef__: \"path\" } placeholders.\n * The resulting FormData contains __json__ with the serialized structure\n * and individual file entries at their path keys.\n */\nexport function objectToFormData(obj: Record<string, unknown>): FormData {\n const formData = new FormData();\n const files: Array<{ path: string; file: File }> = [];\n\n function traverse(value: unknown, path: string): unknown {\n if (value instanceof File) {\n files.push({ path, file: value });\n return { __fileRef__: path };\n }\n if (Array.isArray(value)) {\n return value.map((item, i) => traverse(item, `${path}.${i}`));\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = traverse(v, path ? `${path}.${k}` : k);\n }\n return result;\n }\n return value;\n }\n\n const jsonWithRefs = traverse(obj, '');\n formData.append('__json__', JSON.stringify(jsonWithRefs));\n\n for (const { path, file } of files) {\n formData.append(path, file);\n }\n\n return formData;\n}\n\n/**\n * Parse FormData back to an object, reconstructing the structure with File objects.\n * Expects FormData created by objectToFormData with __json__ and file entries.\n * Falls back to simple Object.fromEntries for FormData without __json__.\n */\nexport function formDataToObject(formData: FormData): Record<string, unknown> {\n const jsonStr = formData.get('__json__');\n if (typeof jsonStr !== 'string') {\n return Object.fromEntries(formData.entries());\n }\n\n const obj = JSON.parse(jsonStr);\n\n function replaceRefs(value: unknown): unknown {\n if (value && typeof value === 'object' && '__fileRef__' in value) {\n const path = (value as { __fileRef__: string }).__fileRef__;\n return formData.get(path);\n }\n if (Array.isArray(value)) {\n return value.map(replaceRefs);\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = replaceRefs(v);\n }\n return result;\n }\n return value;\n }\n\n return replaceRefs(obj) as Record<string, unknown>;\n}\n\n// Re-export WebSocket types\nexport * from './websocket.cjs';\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
7
|
+
"mappings": "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",
|
|
8
8
|
"debugId": "CA0ED6C82DBBBD5564756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/index.mjs.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type { z } from 'zod';\n\n// HTTP methods supported\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n\n// Content types supported for request bodies\nexport type ContentType = 'application/json' | 'multipart/form-data';\n\n// HTTP status codes as const object for type-safe responses without 'as const'\nexport const Status = {\n // Success responses\n OK: 200 as const,\n Created: 201 as const,\n Accepted: 202 as const,\n NoContent: 204 as const,\n\n // Redirection\n MovedPermanently: 301 as const,\n Found: 302 as const,\n NotModified: 304 as const,\n\n // Client errors\n BadRequest: 400 as const,\n Unauthorized: 401 as const,\n Forbidden: 403 as const,\n NotFound: 404 as const,\n MethodNotAllowed: 405 as const,\n Conflict: 409 as const,\n UnprocessableEntity: 422 as const,\n TooManyRequests: 429 as const,\n\n // Server errors\n InternalServerError: 500 as const,\n NotImplemented: 501 as const,\n BadGateway: 502 as const,\n ServiceUnavailable: 503 as const,\n GatewayTimeout: 504 as const,\n} as const;\n\n// Base fields shared by all endpoint types\ninterface BaseEndpointFields {\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n}\n\n// Standard HTTP endpoint\nexport interface StandardEndpointDefinition extends BaseEndpointFields {\n type: 'standard';\n method: HttpMethod;\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n responses: Record<number, z.ZodTypeAny>;\n}\n\n// Streaming response endpoint (NDJSON)\nexport interface StreamingEndpointDefinition extends BaseEndpointFields {\n type: 'streaming';\n method: 'POST';\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n /** Schema for each NDJSON chunk (type inference only, not validated) */\n chunk: z.ZodTypeAny;\n /** Optional final response after stream ends */\n finalResponse?: z.ZodTypeAny;\n /** Error responses for non-streaming failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// SSE endpoint\nexport interface SSEEndpointDefinition extends BaseEndpointFields {\n type: 'sse';\n method: 'GET';\n /** Event types: key = event name, value = data schema (type inference only) */\n events: Record<string, z.ZodTypeAny>;\n /** Error responses for connection failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Download endpoint (binary file response)\nexport interface DownloadEndpointDefinition extends BaseEndpointFields {\n type: 'download';\n method: 'GET'; // Downloads are GET-only\n /** Error responses (non-2xx status codes) */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Union of all endpoint types\nexport type AnyEndpointDefinition =\n | StandardEndpointDefinition\n | StreamingEndpointDefinition\n | SSEEndpointDefinition\n | DownloadEndpointDefinition;\n\n// Alias for backwards compatibility in type utilities\nexport type EndpointDefinition = AnyEndpointDefinition;\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, AnyEndpointDefinition>;\n\n// Extract the Zod type from a schema\nexport type InferZodType<T> = T extends z.ZodTypeAny ? z.infer<T> : never;\n\n// Extract params type from endpoint\nexport type ExtractParams<T extends EndpointDefinition> = T['params'] extends z.ZodTypeAny\n ? InferZodType<T['params']>\n : never;\n\n// Extract query type from endpoint\nexport type ExtractQuery<T extends EndpointDefinition> = T['query'] extends z.ZodTypeAny\n ? InferZodType<T['query']>\n : never;\n\n// Extract headers type from endpoint\nexport type ExtractHeaders<T extends EndpointDefinition> = T['headers'] extends z.ZodTypeAny\n ? InferZodType<T['headers']>\n : never;\n\n// Extract body type from endpoint (only standard and streaming have body)\nexport type ExtractBody<T extends EndpointDefinition> = T extends { body: z.ZodTypeAny }\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes (only standard has responses)\nexport type ExtractResponses<T extends EndpointDefinition> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n }\n : never;\n\n// Extract a specific response type by status code (only standard has responses)\nexport type ExtractResponse<T extends EndpointDefinition, Status extends number> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never\n : never;\n\n// Extract chunk type from streaming endpoint\nexport type ExtractChunk<T extends StreamingEndpointDefinition> = T['chunk'] extends z.ZodTypeAny\n ? InferZodType<T['chunk']>\n : never;\n\n// Extract final response type from streaming endpoint\nexport type ExtractFinalResponse<T extends StreamingEndpointDefinition> =\n T['finalResponse'] extends z.ZodTypeAny ? InferZodType<T['finalResponse']> : undefined;\n\n// Extract SSE event union type\nexport type ExtractSSEEvents<T extends SSEEndpointDefinition> = {\n [K in keyof T['events']]: {\n event: K;\n data: T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n id?: string;\n };\n}[keyof T['events']];\n\n// Extract specific SSE event data type\nexport type ExtractSSEEventData<\n T extends SSEEndpointDefinition,\n K extends keyof T['events'],\n> = T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n\n// Extract error responses for download endpoint\nexport type ExtractDownloadErrorResponse<\n T extends DownloadEndpointDefinition,\n Status extends keyof T['errorResponses'],\n> =\n T['errorResponses'] extends Record<number, z.ZodTypeAny>\n ? Status extends keyof T['errorResponses']\n ? T['errorResponses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['errorResponses'][Status]>\n : never\n : never\n : never;\n\n// Upload progress event\nexport interface UploadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal)\n}\n\n// Download progress event\nexport interface DownloadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal), NaN if total unknown\n}\n\n// Path parameter extraction utilities\n// Extract regular path params (:param)\ntype ExtractRegularParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractRegularParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Extract wildcard params (*param) - must be at end of path\ntype ExtractWildcardParams<T extends string> = T extends `${infer _Start}*${infer Param}`\n ? Param\n : never;\n\n// Combined extraction of all path params\nexport type ExtractPathParams<T extends string> =\n | ExtractRegularParams<T>\n | ExtractWildcardParams<T>;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Detailed path parameter info distinguishing regular vs wildcard params\n */\nexport interface ParsedPathParams {\n regular: string[];\n wildcard: string | null;\n}\n\n/**\n * Parse path parameters with detailed type information\n * e.g., \"/users/:id/*path\" => { regular: [\"id\"], wildcard: \"path\" }\n */\nexport function parsePathParamsDetailed(path: string): ParsedPathParams {\n const regular: string[] = [];\n\n // Extract regular params (:param)\n const regularMatches = path.match(/:([^/]+)/g);\n if (regularMatches) {\n regular.push(...regularMatches.map((match) => match.slice(1)));\n }\n\n // Extract wildcard param (*param) - must be at end\n const wildcardMatch = path.match(/\\*([^/]+)$/);\n\n return {\n regular,\n wildcard: wildcardMatch?.[1] ?? null,\n };\n}\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n * e.g., \"/files/*path\" => [\"path\"]\n */\nexport function parsePathParams(path: string): string[] {\n const { regular, wildcard } = parsePathParamsDetailed(path);\n return wildcard ? [...regular, wildcard] : regular;\n}\n\n/**\n * Match a URL path against a pattern and extract parameters\n * e.g., matchPath(\"/users/:id\", \"/users/123\") => { id: \"123\" }\n * e.g., matchPath(\"/files/*path\", \"/files/a/b/c.txt\") => { path: \"a/b/c.txt\" }\n */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const { regular, wildcard } = parsePathParamsDetailed(pattern);\n\n // Convert pattern to regex:\n // 1. Replace :param with capturing group for single segment\n // 2. Replace *param with capturing group for remaining path (including slashes)\n let regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\n\n // Handle wildcard at the end - replace *paramName with (.+)\n if (wildcard) {\n regexPattern = regexPattern.replace(/\\*[^/]+$/, '(.+)');\n }\n\n const regex = new RegExp(`^${regexPattern}$`);\n const match = path.match(regex);\n\n if (!match) return null;\n\n const params: Record<string, string> = {};\n\n // Extract regular params first\n regular.forEach((name, index) => {\n params[name] = match[index + 1] ?? '';\n });\n\n // Extract wildcard param if present (always last capture group)\n if (wildcard) {\n params[wildcard] = match[regular.length + 1] ?? '';\n }\n\n return params;\n}\n\n/**\n * Interpolate path parameters into a URL pattern\n * e.g., interpolatePath(\"/users/:id\", { id: \"123\" }) => \"/users/123\"\n * e.g., interpolatePath(\"/files/*path\", { path: \"a/b/c.txt\" }) => \"/files/a/b/c.txt\"\n */\nexport function interpolatePath(pattern: string, params: Record<string, string | number>): string {\n let result = pattern;\n for (const [key, value] of Object.entries(params)) {\n // Try replacing :param first, then *param\n if (result.includes(`:${key}`)) {\n result = result.replace(`:${key}`, String(value));\n } else if (result.includes(`*${key}`)) {\n result = result.replace(`*${key}`, String(value));\n }\n }\n return result;\n}\n\n/**\n * Validate a path pattern for correct syntax\n * @throws Error if pattern is invalid\n */\nexport function validatePathPattern(pattern: string): void {\n // Check for wildcard not at the end\n const wildcardIndex = pattern.indexOf('*');\n if (wildcardIndex !== -1) {\n const afterWildcard = pattern.slice(wildcardIndex + 1);\n\n // Wildcard param name should go to end of string (no more slashes)\n if (afterWildcard.includes('/')) {\n throw new Error(\n `Invalid path pattern \"${pattern}\": Wildcard parameter must be the last segment`,\n );\n }\n\n // Ensure wildcard has a valid name (like Express 5 requires)\n if (!/^\\*[a-zA-Z_][a-zA-Z0-9_]*$/.test(pattern.slice(wildcardIndex))) {\n throw new Error(\n `Invalid path pattern \"${pattern}\": Wildcard parameter must have a valid name (e.g., *filePath)`,\n );\n }\n }\n\n // Check for multiple wildcards\n const wildcardCount = (pattern.match(/\\*/g) || []).length;\n if (wildcardCount > 1) {\n throw new Error(`Invalid path pattern \"${pattern}\": Only one wildcard parameter is allowed`);\n }\n}\n\n/**\n * Build a complete URL with query parameters\n */\nexport function buildUrl(\n baseUrl: string,\n path: string,\n query?: Record<string, string | number | boolean | string[]>,\n): string {\n // Normalize baseUrl - remove trailing slash\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n\n // Ensure path starts with /\n const normalizedPath = path.startsWith('/') ? path : `/${path}`;\n\n // Concatenate base and path\n const fullPath = normalizedBase + normalizedPath;\n\n const url = new URL(fullPath);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n for (const v of value) {\n url.searchParams.append(key, String(v));\n }\n } else {\n url.searchParams.append(key, String(value));\n }\n }\n }\n }\n\n return url.toString();\n}\n\n/**\n * Parse query parameters from URLSearchParams\n */\nexport function parseQuery(searchParams: URLSearchParams): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of searchParams.entries()) {\n const existing = result[key];\n if (existing) {\n if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result[key] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n// Type helper to ensure a value is a valid contract\nexport function defineContract<T extends Contract>(contract: T): T {\n return contract;\n}\n\n/**\n * Convert an object to FormData using the hybrid JSON + Files approach.\n * Files are extracted and replaced with { __fileRef__: \"path\" } placeholders.\n * The resulting FormData contains __json__ with the serialized structure\n * and individual file entries at their path keys.\n */\nexport function objectToFormData(obj: Record<string, unknown>): FormData {\n const formData = new FormData();\n const files: Array<{ path: string; file: File }> = [];\n\n function traverse(value: unknown, path: string): unknown {\n if (value instanceof File) {\n files.push({ path, file: value });\n return { __fileRef__: path };\n }\n if (Array.isArray(value)) {\n return value.map((item, i) => traverse(item, `${path}.${i}`));\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = traverse(v, path ? `${path}.${k}` : k);\n }\n return result;\n }\n return value;\n }\n\n const jsonWithRefs = traverse(obj, '');\n formData.append('__json__', JSON.stringify(jsonWithRefs));\n\n for (const { path, file } of files) {\n formData.append(path, file);\n }\n\n return formData;\n}\n\n/**\n * Parse FormData back to an object, reconstructing the structure with File objects.\n * Expects FormData created by objectToFormData with __json__ and file entries.\n * Falls back to simple Object.fromEntries for FormData without __json__.\n */\nexport function formDataToObject(formData: FormData): Record<string, unknown> {\n const jsonStr = formData.get('__json__');\n if (typeof jsonStr !== 'string') {\n return Object.fromEntries(formData.entries());\n }\n\n const obj = JSON.parse(jsonStr);\n\n function replaceRefs(value: unknown): unknown {\n if (value && typeof value === 'object' && '__fileRef__' in value) {\n const path = (value as { __fileRef__: string }).__fileRef__;\n return formData.get(path);\n }\n if (Array.isArray(value)) {\n return value.map(replaceRefs);\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = replaceRefs(v);\n }\n return result;\n }\n return value;\n }\n\n return replaceRefs(obj) as Record<string, unknown>;\n}\n\n// Re-export WebSocket types\nexport * from './websocket.mjs';\n"
|
|
5
|
+
"import type { z } from 'zod';\n\n// HTTP methods supported\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n\n// Content types supported for request bodies\nexport type ContentType = 'application/json' | 'multipart/form-data';\n\n// HTTP status codes as const object for type-safe responses without 'as const'\nexport const Status = {\n // Success responses\n OK: 200 as const,\n Created: 201 as const,\n Accepted: 202 as const,\n NoContent: 204 as const,\n\n // Redirection\n MovedPermanently: 301 as const,\n Found: 302 as const,\n NotModified: 304 as const,\n\n // Client errors\n BadRequest: 400 as const,\n Unauthorized: 401 as const,\n Forbidden: 403 as const,\n NotFound: 404 as const,\n MethodNotAllowed: 405 as const,\n Conflict: 409 as const,\n UnprocessableEntity: 422 as const,\n TooManyRequests: 429 as const,\n\n // Server errors\n InternalServerError: 500 as const,\n NotImplemented: 501 as const,\n BadGateway: 502 as const,\n ServiceUnavailable: 503 as const,\n GatewayTimeout: 504 as const,\n} as const;\n\n// Base fields shared by all endpoint types\ninterface BaseEndpointFields {\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n}\n\n// Standard HTTP endpoint\nexport interface StandardEndpointDefinition extends BaseEndpointFields {\n type: 'standard';\n method: HttpMethod;\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n responses: Record<number, z.ZodTypeAny>;\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Streaming response endpoint (NDJSON)\nexport interface StreamingEndpointDefinition extends BaseEndpointFields {\n type: 'streaming';\n method: 'POST';\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n /** Schema for each NDJSON chunk (type inference only, not validated) */\n chunk: z.ZodTypeAny;\n /** Optional final response after stream ends */\n finalResponse?: z.ZodTypeAny;\n /** Error responses for non-streaming failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// SSE endpoint\nexport interface SSEEndpointDefinition extends BaseEndpointFields {\n type: 'sse';\n method: 'GET';\n /** Event types: key = event name, value = data schema (type inference only) */\n events: Record<string, z.ZodTypeAny>;\n /** Error responses for connection failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Download endpoint (binary file response)\nexport interface DownloadEndpointDefinition extends BaseEndpointFields {\n type: 'download';\n method: 'GET'; // Downloads are GET-only\n /** Error responses (non-2xx status codes) */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Union of all endpoint types\nexport type AnyEndpointDefinition =\n | StandardEndpointDefinition\n | StreamingEndpointDefinition\n | SSEEndpointDefinition\n | DownloadEndpointDefinition;\n\n// Alias for backwards compatibility in type utilities\nexport type EndpointDefinition = AnyEndpointDefinition;\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, AnyEndpointDefinition>;\n\n// Extract the Zod type from a schema\nexport type InferZodType<T> = T extends z.ZodTypeAny ? z.infer<T> : never;\n\n// Extract params type from endpoint\nexport type ExtractParams<T extends EndpointDefinition> = T['params'] extends z.ZodTypeAny\n ? InferZodType<T['params']>\n : never;\n\n// Extract query type from endpoint\nexport type ExtractQuery<T extends EndpointDefinition> = T['query'] extends z.ZodTypeAny\n ? InferZodType<T['query']>\n : never;\n\n// Extract headers type from endpoint\nexport type ExtractHeaders<T extends EndpointDefinition> = T['headers'] extends z.ZodTypeAny\n ? InferZodType<T['headers']>\n : never;\n\n// Extract body type from endpoint (only standard and streaming have body)\nexport type ExtractBody<T extends EndpointDefinition> = T extends { body: z.ZodTypeAny }\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes (only standard has responses)\nexport type ExtractResponses<T extends EndpointDefinition> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n }\n : never;\n\n// Extract a specific response type by status code (only standard has responses)\nexport type ExtractResponse<T extends EndpointDefinition, Status extends number> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never\n : never;\n\n// Extract error response types for all status codes\nexport type ExtractErrorResponses<T extends EndpointDefinition> = T extends {\n errorResponses: Record<number, z.ZodTypeAny>;\n} ? {\n [K in keyof T['errorResponses']]: T['errorResponses'][K] extends z.ZodTypeAny\n ? InferZodType<T['errorResponses'][K]>\n : never;\n } : never;\n\n// Extract a specific error response type by status code\nexport type ExtractErrorResponse<T extends EndpointDefinition, Status extends number> = T extends {\n errorResponses: Record<number, z.ZodTypeAny>;\n} ? Status extends keyof T['errorResponses']\n ? T['errorResponses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['errorResponses'][Status]>\n : never\n : never\n : never;\n\n// Extract chunk type from streaming endpoint\nexport type ExtractChunk<T extends StreamingEndpointDefinition> = T['chunk'] extends z.ZodTypeAny\n ? InferZodType<T['chunk']>\n : never;\n\n// Extract final response type from streaming endpoint\nexport type ExtractFinalResponse<T extends StreamingEndpointDefinition> =\n T['finalResponse'] extends z.ZodTypeAny ? InferZodType<T['finalResponse']> : undefined;\n\n// Extract SSE event union type\nexport type ExtractSSEEvents<T extends SSEEndpointDefinition> = {\n [K in keyof T['events']]: {\n event: K;\n data: T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n id?: string;\n };\n}[keyof T['events']];\n\n// Extract specific SSE event data type\nexport type ExtractSSEEventData<\n T extends SSEEndpointDefinition,\n K extends keyof T['events'],\n> = T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n\n// Extract error responses for download endpoint\nexport type ExtractDownloadErrorResponse<\n T extends DownloadEndpointDefinition,\n Status extends keyof T['errorResponses'],\n> =\n T['errorResponses'] extends Record<number, z.ZodTypeAny>\n ? Status extends keyof T['errorResponses']\n ? T['errorResponses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['errorResponses'][Status]>\n : never\n : never\n : never;\n\n// Upload progress event\nexport interface UploadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal)\n}\n\n// Download progress event\nexport interface DownloadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal), NaN if total unknown\n}\n\n// Path parameter extraction utilities\n// Extract regular path params (:param)\ntype ExtractRegularParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractRegularParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Extract wildcard params (*param) - must be at end of path\ntype ExtractWildcardParams<T extends string> = T extends `${infer _Start}*${infer Param}`\n ? Param\n : never;\n\n// Combined extraction of all path params\nexport type ExtractPathParams<T extends string> =\n | ExtractRegularParams<T>\n | ExtractWildcardParams<T>;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Detailed path parameter info distinguishing regular vs wildcard params\n */\nexport interface ParsedPathParams {\n regular: string[];\n wildcard: string | null;\n}\n\n/**\n * Parse path parameters with detailed type information\n * e.g., \"/users/:id/*path\" => { regular: [\"id\"], wildcard: \"path\" }\n */\nexport function parsePathParamsDetailed(path: string): ParsedPathParams {\n const regular: string[] = [];\n\n // Extract regular params (:param)\n const regularMatches = path.match(/:([^/]+)/g);\n if (regularMatches) {\n regular.push(...regularMatches.map((match) => match.slice(1)));\n }\n\n // Extract wildcard param (*param) - must be at end\n const wildcardMatch = path.match(/\\*([^/]+)$/);\n\n return {\n regular,\n wildcard: wildcardMatch?.[1] ?? null,\n };\n}\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n * e.g., \"/files/*path\" => [\"path\"]\n */\nexport function parsePathParams(path: string): string[] {\n const { regular, wildcard } = parsePathParamsDetailed(path);\n return wildcard ? [...regular, wildcard] : regular;\n}\n\n/**\n * Match a URL path against a pattern and extract parameters\n * e.g., matchPath(\"/users/:id\", \"/users/123\") => { id: \"123\" }\n * e.g., matchPath(\"/files/*path\", \"/files/a/b/c.txt\") => { path: \"a/b/c.txt\" }\n */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const { regular, wildcard } = parsePathParamsDetailed(pattern);\n\n // Convert pattern to regex:\n // 1. Replace :param with capturing group for single segment\n // 2. Replace *param with capturing group for remaining path (including slashes)\n let regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\n\n // Handle wildcard at the end - replace *paramName with (.+)\n if (wildcard) {\n regexPattern = regexPattern.replace(/\\*[^/]+$/, '(.+)');\n }\n\n const regex = new RegExp(`^${regexPattern}$`);\n const match = path.match(regex);\n\n if (!match) return null;\n\n const params: Record<string, string> = {};\n\n // Extract regular params first\n regular.forEach((name, index) => {\n params[name] = match[index + 1] ?? '';\n });\n\n // Extract wildcard param if present (always last capture group)\n if (wildcard) {\n params[wildcard] = match[regular.length + 1] ?? '';\n }\n\n return params;\n}\n\n/**\n * Interpolate path parameters into a URL pattern\n * e.g., interpolatePath(\"/users/:id\", { id: \"123\" }) => \"/users/123\"\n * e.g., interpolatePath(\"/files/*path\", { path: \"a/b/c.txt\" }) => \"/files/a/b/c.txt\"\n */\nexport function interpolatePath(pattern: string, params: Record<string, string | number>): string {\n let result = pattern;\n for (const [key, value] of Object.entries(params)) {\n // Try replacing :param first, then *param\n if (result.includes(`:${key}`)) {\n result = result.replace(`:${key}`, String(value));\n } else if (result.includes(`*${key}`)) {\n result = result.replace(`*${key}`, String(value));\n }\n }\n return result;\n}\n\n/**\n * Validate a path pattern for correct syntax\n * @throws Error if pattern is invalid\n */\nexport function validatePathPattern(pattern: string): void {\n // Check for wildcard not at the end\n const wildcardIndex = pattern.indexOf('*');\n if (wildcardIndex !== -1) {\n const afterWildcard = pattern.slice(wildcardIndex + 1);\n\n // Wildcard param name should go to end of string (no more slashes)\n if (afterWildcard.includes('/')) {\n throw new Error(\n `Invalid path pattern \"${pattern}\": Wildcard parameter must be the last segment`,\n );\n }\n\n // Ensure wildcard has a valid name (like Express 5 requires)\n if (!/^\\*[a-zA-Z_][a-zA-Z0-9_]*$/.test(pattern.slice(wildcardIndex))) {\n throw new Error(\n `Invalid path pattern \"${pattern}\": Wildcard parameter must have a valid name (e.g., *filePath)`,\n );\n }\n }\n\n // Check for multiple wildcards\n const wildcardCount = (pattern.match(/\\*/g) || []).length;\n if (wildcardCount > 1) {\n throw new Error(`Invalid path pattern \"${pattern}\": Only one wildcard parameter is allowed`);\n }\n}\n\n/**\n * Build a complete URL with query parameters\n */\nexport function buildUrl(\n baseUrl: string,\n path: string,\n query?: Record<string, string | number | boolean | string[]>,\n): string {\n // Normalize baseUrl - remove trailing slash\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n\n // Ensure path starts with /\n const normalizedPath = path.startsWith('/') ? path : `/${path}`;\n\n // Concatenate base and path\n const fullPath = normalizedBase + normalizedPath;\n\n const url = new URL(fullPath);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n for (const v of value) {\n url.searchParams.append(key, String(v));\n }\n } else {\n url.searchParams.append(key, String(value));\n }\n }\n }\n }\n\n return url.toString();\n}\n\n/**\n * Parse query parameters from URLSearchParams\n */\nexport function parseQuery(searchParams: URLSearchParams): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of searchParams.entries()) {\n const existing = result[key];\n if (existing) {\n if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result[key] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n// Type helper to ensure a value is a valid contract\nexport function defineContract<T extends Contract>(contract: T): T {\n return contract;\n}\n\n/**\n * Convert an object to FormData using the hybrid JSON + Files approach.\n * Files are extracted and replaced with { __fileRef__: \"path\" } placeholders.\n * The resulting FormData contains __json__ with the serialized structure\n * and individual file entries at their path keys.\n */\nexport function objectToFormData(obj: Record<string, unknown>): FormData {\n const formData = new FormData();\n const files: Array<{ path: string; file: File }> = [];\n\n function traverse(value: unknown, path: string): unknown {\n if (value instanceof File) {\n files.push({ path, file: value });\n return { __fileRef__: path };\n }\n if (Array.isArray(value)) {\n return value.map((item, i) => traverse(item, `${path}.${i}`));\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = traverse(v, path ? `${path}.${k}` : k);\n }\n return result;\n }\n return value;\n }\n\n const jsonWithRefs = traverse(obj, '');\n formData.append('__json__', JSON.stringify(jsonWithRefs));\n\n for (const { path, file } of files) {\n formData.append(path, file);\n }\n\n return formData;\n}\n\n/**\n * Parse FormData back to an object, reconstructing the structure with File objects.\n * Expects FormData created by objectToFormData with __json__ and file entries.\n * Falls back to simple Object.fromEntries for FormData without __json__.\n */\nexport function formDataToObject(formData: FormData): Record<string, unknown> {\n const jsonStr = formData.get('__json__');\n if (typeof jsonStr !== 'string') {\n return Object.fromEntries(formData.entries());\n }\n\n const obj = JSON.parse(jsonStr);\n\n function replaceRefs(value: unknown): unknown {\n if (value && typeof value === 'object' && '__fileRef__' in value) {\n const path = (value as { __fileRef__: string }).__fileRef__;\n return formData.get(path);\n }\n if (Array.isArray(value)) {\n return value.map(replaceRefs);\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = replaceRefs(v);\n }\n return result;\n }\n return value;\n }\n\n return replaceRefs(obj) as Record<string, unknown>;\n}\n\n// Re-export WebSocket types\nexport * from './websocket.mjs';\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;
|
|
7
|
+
"mappings": "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",
|
|
8
8
|
"debugId": "BA32EBFEE62F023664756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/mjs/package.json
CHANGED
package/dist/types/index.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface StandardEndpointDefinition extends BaseEndpointFields {
|
|
|
35
35
|
body?: z.ZodTypeAny;
|
|
36
36
|
contentType?: ContentType;
|
|
37
37
|
responses: Record<number, z.ZodTypeAny>;
|
|
38
|
+
errorResponses?: Record<number, z.ZodTypeAny>;
|
|
38
39
|
}
|
|
39
40
|
export interface StreamingEndpointDefinition extends BaseEndpointFields {
|
|
40
41
|
type: 'streaming';
|
|
@@ -80,6 +81,14 @@ export type ExtractResponses<T extends EndpointDefinition> = T extends {
|
|
|
80
81
|
export type ExtractResponse<T extends EndpointDefinition, Status extends number> = T extends {
|
|
81
82
|
responses: Record<number, z.ZodTypeAny>;
|
|
82
83
|
} ? Status extends keyof T['responses'] ? T['responses'][Status] extends z.ZodTypeAny ? InferZodType<T['responses'][Status]> : never : never : never;
|
|
84
|
+
export type ExtractErrorResponses<T extends EndpointDefinition> = T extends {
|
|
85
|
+
errorResponses: Record<number, z.ZodTypeAny>;
|
|
86
|
+
} ? {
|
|
87
|
+
[K in keyof T['errorResponses']]: T['errorResponses'][K] extends z.ZodTypeAny ? InferZodType<T['errorResponses'][K]> : never;
|
|
88
|
+
} : never;
|
|
89
|
+
export type ExtractErrorResponse<T extends EndpointDefinition, Status extends number> = T extends {
|
|
90
|
+
errorResponses: Record<number, z.ZodTypeAny>;
|
|
91
|
+
} ? Status extends keyof T['errorResponses'] ? T['errorResponses'][Status] extends z.ZodTypeAny ? InferZodType<T['errorResponses'][Status]> : never : never : never;
|
|
83
92
|
export type ExtractChunk<T extends StreamingEndpointDefinition> = T['chunk'] extends z.ZodTypeAny ? InferZodType<T['chunk']> : never;
|
|
84
93
|
export type ExtractFinalResponse<T extends StreamingEndpointDefinition> = T['finalResponse'] extends z.ZodTypeAny ? InferZodType<T['finalResponse']> : undefined;
|
|
85
94
|
export type ExtractSSEEvents<T extends SSEEndpointDefinition> = {
|