@richie-rpc/core 1.2.0 → 1.2.2

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 CHANGED
@@ -47,6 +47,7 @@ Each endpoint can have:
47
47
  - `query` (optional): Zod schema for query parameters
48
48
  - `headers` (optional): Zod schema for request headers
49
49
  - `body` (optional): Zod schema for request body
50
+ - `contentType` (optional): Request content type (`'application/json'` or `'multipart/form-data'`)
50
51
  - `responses` (required): Object mapping status codes to Zod schemas
51
52
 
52
53
  ## Features
@@ -58,6 +59,8 @@ Each endpoint can have:
58
59
  - ✅ Multiple response types per endpoint
59
60
  - ✅ Full TypeScript inference
60
61
  - ✅ Status code constants for cleaner code
62
+ - ✅ File uploads with `multipart/form-data` support
63
+ - ✅ Nested file structures in request bodies
61
64
 
62
65
  ## Utilities
63
66
 
@@ -86,6 +89,75 @@ import { buildUrl } from '@richie-rpc/core';
86
89
 
87
90
  buildUrl('http://api.example.com', '/users', { limit: '10', offset: '0' });
88
91
  // => 'http://api.example.com/users?limit=10&offset=0'
92
+
93
+ // With basePath in baseUrl
94
+ buildUrl('http://api.example.com/api', '/users');
95
+ // => 'http://api.example.com/api/users'
96
+ ```
97
+
98
+ The `buildUrl` function properly concatenates the baseUrl with the path, supporting basePath prefixes in the baseUrl.
99
+
100
+ ### File Upload / FormData Utilities
101
+
102
+ The core package provides utilities for handling `multipart/form-data` requests with nested file structures.
103
+
104
+ #### Defining File Upload Endpoints
105
+
106
+ Use `contentType: 'multipart/form-data'` and `z.instanceof(File)` in your body schema:
107
+
108
+ ```typescript
109
+ import { defineContract, Status } from '@richie-rpc/core';
110
+ import { z } from 'zod';
111
+
112
+ const contract = defineContract({
113
+ uploadDocuments: {
114
+ method: 'POST',
115
+ path: '/upload',
116
+ contentType: 'multipart/form-data',
117
+ body: z.object({
118
+ documents: z.array(z.object({
119
+ file: z.instanceof(File),
120
+ name: z.string(),
121
+ tags: z.array(z.string()).optional(),
122
+ })),
123
+ category: z.string(),
124
+ }),
125
+ responses: {
126
+ [Status.Created]: z.object({
127
+ uploadedCount: z.number(),
128
+ filenames: z.array(z.string()),
129
+ }),
130
+ },
131
+ },
132
+ });
133
+ ```
134
+
135
+ #### How It Works
136
+
137
+ FormData is inherently flat, but Richie RPC supports nested structures using a hybrid JSON + Files approach:
138
+
139
+ 1. **Client-side**: Files are extracted from the object and replaced with `{ __fileRef__: "path" }` placeholders. The JSON structure is sent as `__json__` and files are sent as separate FormData entries.
140
+
141
+ 2. **Server-side**: The JSON is parsed, and `__fileRef__` placeholders are replaced with actual File objects from the FormData.
142
+
143
+ #### Utility Functions
144
+
145
+ ```typescript
146
+ import { objectToFormData, formDataToObject } from '@richie-rpc/core';
147
+
148
+ // Client-side: Convert object with Files to FormData
149
+ const formData = objectToFormData({
150
+ documents: [
151
+ { file: file1, name: 'doc1.pdf' },
152
+ { file: file2, name: 'doc2.pdf' },
153
+ ],
154
+ category: 'reports',
155
+ });
156
+ // Result: FormData with __json__ + files at "documents.0.file", "documents.1.file"
157
+
158
+ // Server-side: Convert FormData back to object with Files
159
+ const obj = formDataToObject(formData);
160
+ // Result: { documents: [{ file: File, name: 'doc1.pdf' }, ...], category: 'reports' }
89
161
  ```
90
162
 
91
163
  ## Status Codes
@@ -32,8 +32,10 @@ var exports_core = {};
32
32
  __export(exports_core, {
33
33
  parseQuery: () => parseQuery,
34
34
  parsePathParams: () => parsePathParams,
35
+ objectToFormData: () => objectToFormData,
35
36
  matchPath: () => matchPath,
36
37
  interpolatePath: () => interpolatePath,
38
+ formDataToObject: () => formDataToObject,
37
39
  defineContract: () => defineContract,
38
40
  buildUrl: () => buildUrl,
39
41
  Status: () => Status
@@ -126,6 +128,58 @@ function parseQuery(searchParams) {
126
128
  function defineContract(contract) {
127
129
  return contract;
128
130
  }
131
+ function objectToFormData(obj) {
132
+ const formData = new FormData;
133
+ const files = [];
134
+ function traverse(value, path) {
135
+ if (value instanceof File) {
136
+ files.push({ path, file: value });
137
+ return { __fileRef__: path };
138
+ }
139
+ if (Array.isArray(value)) {
140
+ return value.map((item, i) => traverse(item, `${path}.${i}`));
141
+ }
142
+ if (value && typeof value === "object") {
143
+ const result = {};
144
+ for (const [k, v] of Object.entries(value)) {
145
+ result[k] = traverse(v, path ? `${path}.${k}` : k);
146
+ }
147
+ return result;
148
+ }
149
+ return value;
150
+ }
151
+ const jsonWithRefs = traverse(obj, "");
152
+ formData.append("__json__", JSON.stringify(jsonWithRefs));
153
+ for (const { path, file } of files) {
154
+ formData.append(path, file);
155
+ }
156
+ return formData;
157
+ }
158
+ function formDataToObject(formData) {
159
+ const jsonStr = formData.get("__json__");
160
+ if (typeof jsonStr !== "string") {
161
+ return Object.fromEntries(formData.entries());
162
+ }
163
+ const obj = JSON.parse(jsonStr);
164
+ function replaceRefs(value) {
165
+ if (value && typeof value === "object" && "__fileRef__" in value) {
166
+ const path = value.__fileRef__;
167
+ return formData.get(path);
168
+ }
169
+ if (Array.isArray(value)) {
170
+ return value.map(replaceRefs);
171
+ }
172
+ if (value && typeof value === "object") {
173
+ const result = {};
174
+ for (const [k, v] of Object.entries(value)) {
175
+ result[k] = replaceRefs(v);
176
+ }
177
+ return result;
178
+ }
179
+ return value;
180
+ }
181
+ return replaceRefs(obj);
182
+ }
129
183
  })
130
184
 
131
- //# debugId=ACC9A49236395C2D64756E2164756E21
185
+ //# debugId=56D7471C2C96572E64756E2164756E21
@@ -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// 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// Endpoint definition structure\nexport interface EndpointDefinition {\n method: HttpMethod;\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n body?: z.ZodTypeAny;\n responses: Record<number, z.ZodTypeAny>;\n}\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, EndpointDefinition>;\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\nexport type ExtractBody<T extends EndpointDefinition> = T['body'] extends z.ZodTypeAny\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes\nexport type ExtractResponses<T extends EndpointDefinition> = {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n};\n\n// Extract a specific response type by status code\nexport type ExtractResponse<\n T extends EndpointDefinition,\n Status extends number,\n> = Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never;\n\n// Path parameter extraction utilities\nexport type ExtractPathParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractPathParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n */\nexport function parsePathParams(path: string): string[] {\n const matches = path.match(/:([^/]+)/g);\n if (!matches) return [];\n return matches.map((match) => match.slice(1));\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 */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const paramNames = parsePathParams(pattern);\n\n // Convert pattern to regex\n const regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\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 paramNames.forEach((name, index) => {\n params[name] = match[index + 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 */\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 result = result.replace(`:${key}`, String(value));\n }\n return result;\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"
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// Endpoint definition structure\nexport interface EndpointDefinition {\n method: HttpMethod;\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n responses: Record<number, z.ZodTypeAny>;\n}\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, EndpointDefinition>;\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\nexport type ExtractBody<T extends EndpointDefinition> = T['body'] extends z.ZodTypeAny\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes\nexport type ExtractResponses<T extends EndpointDefinition> = {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n};\n\n// Extract a specific response type by status code\nexport type ExtractResponse<\n T extends EndpointDefinition,\n Status extends number,\n> = Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never;\n\n// Path parameter extraction utilities\nexport type ExtractPathParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractPathParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n */\nexport function parsePathParams(path: string): string[] {\n const matches = path.match(/:([^/]+)/g);\n if (!matches) return [];\n return matches.map((match) => match.slice(1));\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 */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const paramNames = parsePathParams(pattern);\n\n // Convert pattern to regex\n const regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\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 paramNames.forEach((name, index) => {\n params[name] = match[index + 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 */\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 result = result.replace(`:${key}`, String(value));\n }\n return result;\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"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "ACC9A49236395C2D64756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "56D7471C2C96572E64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@richie-rpc/core",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "type": "commonjs"
5
5
  }
@@ -87,14 +87,68 @@ function parseQuery(searchParams) {
87
87
  function defineContract(contract) {
88
88
  return contract;
89
89
  }
90
+ function objectToFormData(obj) {
91
+ const formData = new FormData;
92
+ const files = [];
93
+ function traverse(value, path) {
94
+ if (value instanceof File) {
95
+ files.push({ path, file: value });
96
+ return { __fileRef__: path };
97
+ }
98
+ if (Array.isArray(value)) {
99
+ return value.map((item, i) => traverse(item, `${path}.${i}`));
100
+ }
101
+ if (value && typeof value === "object") {
102
+ const result = {};
103
+ for (const [k, v] of Object.entries(value)) {
104
+ result[k] = traverse(v, path ? `${path}.${k}` : k);
105
+ }
106
+ return result;
107
+ }
108
+ return value;
109
+ }
110
+ const jsonWithRefs = traverse(obj, "");
111
+ formData.append("__json__", JSON.stringify(jsonWithRefs));
112
+ for (const { path, file } of files) {
113
+ formData.append(path, file);
114
+ }
115
+ return formData;
116
+ }
117
+ function formDataToObject(formData) {
118
+ const jsonStr = formData.get("__json__");
119
+ if (typeof jsonStr !== "string") {
120
+ return Object.fromEntries(formData.entries());
121
+ }
122
+ const obj = JSON.parse(jsonStr);
123
+ function replaceRefs(value) {
124
+ if (value && typeof value === "object" && "__fileRef__" in value) {
125
+ const path = value.__fileRef__;
126
+ return formData.get(path);
127
+ }
128
+ if (Array.isArray(value)) {
129
+ return value.map(replaceRefs);
130
+ }
131
+ if (value && typeof value === "object") {
132
+ const result = {};
133
+ for (const [k, v] of Object.entries(value)) {
134
+ result[k] = replaceRefs(v);
135
+ }
136
+ return result;
137
+ }
138
+ return value;
139
+ }
140
+ return replaceRefs(obj);
141
+ }
90
142
  export {
91
143
  parseQuery,
92
144
  parsePathParams,
145
+ objectToFormData,
93
146
  matchPath,
94
147
  interpolatePath,
148
+ formDataToObject,
95
149
  defineContract,
96
150
  buildUrl,
97
151
  Status
98
152
  };
99
153
 
100
- //# debugId=6A3EE2DEC11404CC64756E2164756E21
154
+ //# debugId=4DAE69D50D8DB01A64756E2164756E21
@@ -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// 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// Endpoint definition structure\nexport interface EndpointDefinition {\n method: HttpMethod;\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n body?: z.ZodTypeAny;\n responses: Record<number, z.ZodTypeAny>;\n}\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, EndpointDefinition>;\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\nexport type ExtractBody<T extends EndpointDefinition> = T['body'] extends z.ZodTypeAny\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes\nexport type ExtractResponses<T extends EndpointDefinition> = {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n};\n\n// Extract a specific response type by status code\nexport type ExtractResponse<\n T extends EndpointDefinition,\n Status extends number,\n> = Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never;\n\n// Path parameter extraction utilities\nexport type ExtractPathParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractPathParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n */\nexport function parsePathParams(path: string): string[] {\n const matches = path.match(/:([^/]+)/g);\n if (!matches) return [];\n return matches.map((match) => match.slice(1));\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 */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const paramNames = parsePathParams(pattern);\n\n // Convert pattern to regex\n const regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\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 paramNames.forEach((name, index) => {\n params[name] = match[index + 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 */\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 result = result.replace(`:${key}`, String(value));\n }\n return result;\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"
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// Endpoint definition structure\nexport interface EndpointDefinition {\n method: HttpMethod;\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n responses: Record<number, z.ZodTypeAny>;\n}\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, EndpointDefinition>;\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\nexport type ExtractBody<T extends EndpointDefinition> = T['body'] extends z.ZodTypeAny\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes\nexport type ExtractResponses<T extends EndpointDefinition> = {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n};\n\n// Extract a specific response type by status code\nexport type ExtractResponse<\n T extends EndpointDefinition,\n Status extends number,\n> = Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never;\n\n// Path parameter extraction utilities\nexport type ExtractPathParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractPathParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n */\nexport function parsePathParams(path: string): string[] {\n const matches = path.match(/:([^/]+)/g);\n if (!matches) return [];\n return matches.map((match) => match.slice(1));\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 */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const paramNames = parsePathParams(pattern);\n\n // Convert pattern to regex\n const regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\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 paramNames.forEach((name, index) => {\n params[name] = match[index + 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 */\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 result = result.replace(`:${key}`, String(value));\n }\n return result;\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"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "6A3EE2DEC11404CC64756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "4DAE69D50D8DB01A64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@richie-rpc/core",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "type": "module"
5
5
  }
@@ -1,5 +1,6 @@
1
1
  import type { z } from 'zod';
2
2
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
3
+ export type ContentType = 'application/json' | 'multipart/form-data';
3
4
  export declare const Status: {
4
5
  readonly OK: 200;
5
6
  readonly Created: 201;
@@ -29,6 +30,7 @@ export interface EndpointDefinition {
29
30
  query?: z.ZodTypeAny;
30
31
  headers?: z.ZodTypeAny;
31
32
  body?: z.ZodTypeAny;
33
+ contentType?: ContentType;
32
34
  responses: Record<number, z.ZodTypeAny>;
33
35
  }
34
36
  export type Contract = Record<string, EndpointDefinition>;
@@ -69,3 +71,16 @@ export declare function buildUrl(baseUrl: string, path: string, query?: Record<s
69
71
  */
70
72
  export declare function parseQuery(searchParams: URLSearchParams): Record<string, string | string[]>;
71
73
  export declare function defineContract<T extends Contract>(contract: T): T;
74
+ /**
75
+ * Convert an object to FormData using the hybrid JSON + Files approach.
76
+ * Files are extracted and replaced with { __fileRef__: "path" } placeholders.
77
+ * The resulting FormData contains __json__ with the serialized structure
78
+ * and individual file entries at their path keys.
79
+ */
80
+ export declare function objectToFormData(obj: Record<string, unknown>): FormData;
81
+ /**
82
+ * Parse FormData back to an object, reconstructing the structure with File objects.
83
+ * Expects FormData created by objectToFormData with __json__ and file entries.
84
+ * Falls back to simple Object.fromEntries for FormData without __json__.
85
+ */
86
+ export declare function formDataToObject(formData: FormData): Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@richie-rpc/core",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "main": "./dist/cjs/index.cjs",
5
5
  "exports": {
6
6
  ".": {