@richie-rpc/client 0.1.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 +193 -0
- package/dist/cjs/index.cjs +172 -0
- package/dist/cjs/index.cjs.map +10 -0
- package/dist/cjs/package.json +5 -0
- package/dist/mjs/index.mjs +141 -0
- package/dist/mjs/index.mjs.map +10 -0
- package/dist/mjs/package.json +5 -0
- package/dist/types/index.d.ts +44 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# @richie-rpc/client
|
|
2
|
+
|
|
3
|
+
Type-safe fetch client for Richie RPC contracts.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @richie-rpc/client @richie-rpc/core zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Creating a Client
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createClient } from '@richie-rpc/client';
|
|
17
|
+
import { contract } from './contract';
|
|
18
|
+
|
|
19
|
+
const client = createClient(contract, {
|
|
20
|
+
baseUrl: 'https://api.example.com',
|
|
21
|
+
headers: {
|
|
22
|
+
'Authorization': 'Bearer token123'
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Making Requests
|
|
28
|
+
|
|
29
|
+
The client provides fully typed methods for each endpoint in your contract:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// GET request with path parameters
|
|
33
|
+
const user = await client.getUser({
|
|
34
|
+
params: { id: '123' }
|
|
35
|
+
});
|
|
36
|
+
// user is typed based on the response schema
|
|
37
|
+
|
|
38
|
+
// POST request with body
|
|
39
|
+
const newUser = await client.createUser({
|
|
40
|
+
body: {
|
|
41
|
+
name: 'John Doe',
|
|
42
|
+
email: 'john@example.com'
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Request with query parameters
|
|
47
|
+
const users = await client.listUsers({
|
|
48
|
+
query: {
|
|
49
|
+
limit: '10',
|
|
50
|
+
offset: '0'
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Request with custom headers
|
|
55
|
+
const data = await client.getData({
|
|
56
|
+
headers: {
|
|
57
|
+
'X-Custom-Header': 'value'
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Features
|
|
63
|
+
|
|
64
|
+
- ✅ Full type safety based on contract
|
|
65
|
+
- ✅ Automatic path parameter interpolation
|
|
66
|
+
- ✅ Query parameter encoding
|
|
67
|
+
- ✅ Request validation before sending
|
|
68
|
+
- ✅ Response validation after receiving
|
|
69
|
+
- ✅ Detailed error information
|
|
70
|
+
- ✅ Support for all HTTP methods
|
|
71
|
+
- ✅ Custom headers per request
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
### ClientConfig Options
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
interface ClientConfig {
|
|
79
|
+
baseUrl: string; // Base URL for all requests
|
|
80
|
+
headers?: Record<string, string>; // Default headers
|
|
81
|
+
validateRequest?: boolean; // Validate before sending (default: true)
|
|
82
|
+
validateResponse?: boolean; // Validate after receiving (default: true)
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Response Format
|
|
87
|
+
|
|
88
|
+
Responses include both the status code and data:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const response = await client.getUser({ params: { id: '123' } });
|
|
92
|
+
|
|
93
|
+
console.log(response.status); // 200, 404, etc.
|
|
94
|
+
console.log(response.data); // Typed response body
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Error Handling
|
|
98
|
+
|
|
99
|
+
The client throws typed errors for different scenarios:
|
|
100
|
+
|
|
101
|
+
### ClientValidationError
|
|
102
|
+
|
|
103
|
+
Thrown when request data fails validation:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
try {
|
|
107
|
+
await client.createUser({
|
|
108
|
+
body: { email: 'invalid-email' }
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof ClientValidationError) {
|
|
112
|
+
console.log(error.field); // 'body'
|
|
113
|
+
console.log(error.issues); // Zod validation issues
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### HTTPError
|
|
119
|
+
|
|
120
|
+
Thrown for unexpected HTTP status codes:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
try {
|
|
124
|
+
await client.getUser({ params: { id: '999' } });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof HTTPError) {
|
|
127
|
+
console.log(error.status); // 404
|
|
128
|
+
console.log(error.statusText); // 'Not Found'
|
|
129
|
+
console.log(error.body); // Response body
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Type Safety
|
|
135
|
+
|
|
136
|
+
All client methods are fully typed based on your contract:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// ✅ Type-safe: required fields
|
|
140
|
+
await client.createUser({
|
|
141
|
+
body: { name: 'John', email: 'john@example.com' }
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ❌ Type error: missing required field
|
|
145
|
+
await client.createUser({
|
|
146
|
+
body: { name: 'John' }
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ✅ Type-safe: response data
|
|
150
|
+
const user = await client.getUser({ params: { id: '123' } });
|
|
151
|
+
console.log(user.data.name); // string
|
|
152
|
+
|
|
153
|
+
// ❌ Type error: invalid property
|
|
154
|
+
console.log(user.data.invalid);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Request Options
|
|
158
|
+
|
|
159
|
+
Each client method accepts an options object with the following fields (based on the endpoint definition):
|
|
160
|
+
|
|
161
|
+
- `params`: Path parameters (if endpoint has params schema)
|
|
162
|
+
- `query`: Query parameters (if endpoint has query schema)
|
|
163
|
+
- `headers`: Custom headers (if endpoint has headers schema)
|
|
164
|
+
- `body`: Request body (if endpoint has body schema)
|
|
165
|
+
|
|
166
|
+
Only the fields defined in the contract are available and typed.
|
|
167
|
+
|
|
168
|
+
## Validation
|
|
169
|
+
|
|
170
|
+
By default, both request and response data are validated:
|
|
171
|
+
|
|
172
|
+
- **Request validation**: Ensures data conforms to schema before sending
|
|
173
|
+
- **Response validation**: Ensures server response matches expected schema
|
|
174
|
+
|
|
175
|
+
You can disable validation:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const client = createClient(contract, {
|
|
179
|
+
baseUrl: 'https://api.example.com',
|
|
180
|
+
validateRequest: false, // Skip request validation
|
|
181
|
+
validateResponse: false // Skip response validation
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Links
|
|
186
|
+
|
|
187
|
+
- **npm:** https://www.npmjs.com/package/@richie-rpc/client
|
|
188
|
+
- **Repository:** https://github.com/ricsam/richie-rpc
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT
|
|
193
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @bun @bun-cjs
|
|
2
|
+
(function(exports, require, module, __filename, __dirname) {var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
7
|
+
var __toCommonJS = (from) => {
|
|
8
|
+
var entry = __moduleCache.get(from), desc;
|
|
9
|
+
if (entry)
|
|
10
|
+
return entry;
|
|
11
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
13
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
14
|
+
get: () => from[key],
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
}));
|
|
17
|
+
__moduleCache.set(from, entry);
|
|
18
|
+
return entry;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, {
|
|
23
|
+
get: all[name],
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// packages/client/index.ts
|
|
31
|
+
var exports_client = {};
|
|
32
|
+
__export(exports_client, {
|
|
33
|
+
createTypedClient: () => createTypedClient,
|
|
34
|
+
createClient: () => createClient,
|
|
35
|
+
HTTPError: () => HTTPError,
|
|
36
|
+
ClientValidationError: () => ClientValidationError
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(exports_client);
|
|
39
|
+
var import_core = require("@richie-rpc/core");
|
|
40
|
+
|
|
41
|
+
class ClientValidationError extends Error {
|
|
42
|
+
field;
|
|
43
|
+
issues;
|
|
44
|
+
constructor(field, issues) {
|
|
45
|
+
super(`Validation failed for ${field}`);
|
|
46
|
+
this.field = field;
|
|
47
|
+
this.issues = issues;
|
|
48
|
+
this.name = "ClientValidationError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class HTTPError extends Error {
|
|
53
|
+
status;
|
|
54
|
+
statusText;
|
|
55
|
+
body;
|
|
56
|
+
constructor(status, statusText, body) {
|
|
57
|
+
super(`HTTP Error ${status}: ${statusText}`);
|
|
58
|
+
this.status = status;
|
|
59
|
+
this.statusText = statusText;
|
|
60
|
+
this.body = body;
|
|
61
|
+
this.name = "HTTPError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function validateRequest(endpoint, options) {
|
|
65
|
+
if (endpoint.params && options.params) {
|
|
66
|
+
const result = endpoint.params.safeParse(options.params);
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
throw new ClientValidationError("params", result.error.issues);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (endpoint.query && options.query) {
|
|
72
|
+
const result = endpoint.query.safeParse(options.query);
|
|
73
|
+
if (!result.success) {
|
|
74
|
+
throw new ClientValidationError("query", result.error.issues);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (endpoint.headers && options.headers) {
|
|
78
|
+
const result = endpoint.headers.safeParse(options.headers);
|
|
79
|
+
if (!result.success) {
|
|
80
|
+
throw new ClientValidationError("headers", result.error.issues);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (endpoint.body && options.body) {
|
|
84
|
+
const result = endpoint.body.safeParse(options.body);
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
throw new ClientValidationError("body", result.error.issues);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function validateResponse(endpoint, status, data) {
|
|
91
|
+
const responseSchema = endpoint.responses[status];
|
|
92
|
+
if (responseSchema) {
|
|
93
|
+
const result = responseSchema.safeParse(data);
|
|
94
|
+
if (!result.success) {
|
|
95
|
+
throw new ClientValidationError(`response[${status}]`, result.error.issues);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function makeRequest(config, endpoint, options) {
|
|
100
|
+
if (config.validateRequest !== false) {
|
|
101
|
+
validateRequest(endpoint, options);
|
|
102
|
+
}
|
|
103
|
+
let path = endpoint.path;
|
|
104
|
+
if (options.params) {
|
|
105
|
+
path = import_core.interpolatePath(path, options.params);
|
|
106
|
+
}
|
|
107
|
+
const url = import_core.buildUrl(config.baseUrl, path, options.query);
|
|
108
|
+
const headers = new Headers(config.headers);
|
|
109
|
+
if (options.headers) {
|
|
110
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
111
|
+
headers.set(key, String(value));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const init = {
|
|
115
|
+
method: endpoint.method,
|
|
116
|
+
headers
|
|
117
|
+
};
|
|
118
|
+
if (options.body !== undefined) {
|
|
119
|
+
headers.set("content-type", "application/json");
|
|
120
|
+
init.body = JSON.stringify(options.body);
|
|
121
|
+
}
|
|
122
|
+
const response = await fetch(url, init);
|
|
123
|
+
let data;
|
|
124
|
+
if (response.status === 204) {
|
|
125
|
+
data = {};
|
|
126
|
+
} else {
|
|
127
|
+
const contentType = response.headers.get("content-type") || "";
|
|
128
|
+
if (contentType.includes("application/json")) {
|
|
129
|
+
data = await response.json();
|
|
130
|
+
} else if (contentType.includes("text/")) {
|
|
131
|
+
data = await response.text();
|
|
132
|
+
} else {
|
|
133
|
+
const text = await response.text();
|
|
134
|
+
if (text) {
|
|
135
|
+
data = text;
|
|
136
|
+
} else {
|
|
137
|
+
data = {};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!response.ok && !(response.status in endpoint.responses)) {
|
|
142
|
+
throw new HTTPError(response.status, response.statusText, data);
|
|
143
|
+
}
|
|
144
|
+
if (config.validateResponse !== false) {
|
|
145
|
+
validateResponse(endpoint, response.status, data);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
status: response.status,
|
|
149
|
+
data
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function createClient(contract, config) {
|
|
153
|
+
const client = {};
|
|
154
|
+
for (const [name, endpoint] of Object.entries(contract)) {
|
|
155
|
+
client[name] = (options = {}) => {
|
|
156
|
+
return makeRequest(config, endpoint, options);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return client;
|
|
160
|
+
}
|
|
161
|
+
function createTypedClient(_config) {
|
|
162
|
+
return new Proxy({}, {
|
|
163
|
+
get(_target, _prop) {
|
|
164
|
+
return async (_options = {}) => {
|
|
165
|
+
throw new Error("createTypedClient requires contract at runtime for validation. Use createClient instead.");
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
//# debugId=BC1C6659CB2717F064756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { buildUrl, interpolatePath } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Client configuration\nexport interface ClientConfig {\n baseUrl: string;\n headers?: Record<string, string>;\n validateRequest?: boolean;\n validateResponse?: boolean;\n}\n\n// Request options for an endpoint\nexport type EndpointRequestOptions<T extends EndpointDefinition> = {\n params?: ExtractParams<T> extends never ? never : ExtractParams<T>;\n query?: ExtractQuery<T> extends never ? never : ExtractQuery<T>;\n headers?: ExtractHeaders<T> extends never ? never : ExtractHeaders<T>;\n body?: ExtractBody<T> extends never ? never : ExtractBody<T>;\n};\n\n// Response type for an endpoint (union of all possible responses)\nexport type EndpointResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n data: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n };\n}[keyof T['responses']];\n\n// Client method type for an endpoint\nexport type ClientMethod<T extends EndpointDefinition> = (\n options: EndpointRequestOptions<T>,\n) => Promise<EndpointResponse<T>>;\n\n// Client type for a contract\nexport type Client<T extends Contract> = {\n [K in keyof T]: ClientMethod<T[K]>;\n};\n\n// Validation error\nexport class ClientValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n ) {\n super(`Validation failed for ${field}`);\n this.name = 'ClientValidationError';\n }\n}\n\n// HTTP error\nexport class HTTPError extends Error {\n constructor(\n public status: number,\n public statusText: string,\n public body: unknown,\n ) {\n super(`HTTP Error ${status}: ${statusText}`);\n this.name = 'HTTPError';\n }\n}\n\n/**\n * Validate request data before sending\n */\nfunction validateRequest<T extends EndpointDefinition>(\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): void {\n // Validate params\n if (endpoint.params && options.params) {\n const result = endpoint.params.safeParse(options.params);\n if (!result.success) {\n throw new ClientValidationError('params', result.error.issues);\n }\n }\n\n // Validate query\n if (endpoint.query && options.query) {\n const result = endpoint.query.safeParse(options.query);\n if (!result.success) {\n throw new ClientValidationError('query', result.error.issues);\n }\n }\n\n // Validate headers\n if (endpoint.headers && options.headers) {\n const result = endpoint.headers.safeParse(options.headers);\n if (!result.success) {\n throw new ClientValidationError('headers', result.error.issues);\n }\n }\n\n // Validate body\n if (endpoint.body && options.body) {\n const result = endpoint.body.safeParse(options.body);\n if (!result.success) {\n throw new ClientValidationError('body', result.error.issues);\n }\n }\n}\n\n/**\n * Validate response data after receiving\n */\nfunction validateResponse<T extends EndpointDefinition>(\n endpoint: T,\n status: number,\n data: unknown,\n): void {\n const responseSchema = endpoint.responses[status];\n if (responseSchema) {\n const result = responseSchema.safeParse(data);\n if (!result.success) {\n throw new ClientValidationError(`response[${status}]`, result.error.issues);\n }\n }\n}\n\n/**\n * Make a request to an endpoint\n */\nasync function makeRequest<T extends EndpointDefinition>(\n config: ClientConfig,\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): Promise<EndpointResponse<T>> {\n // Validate request if enabled\n if (config.validateRequest !== false) {\n validateRequest(endpoint, options);\n }\n\n // Build URL\n let path = endpoint.path;\n if (options.params) {\n path = interpolatePath(path, options.params as Record<string, string | number>);\n }\n\n const url = buildUrl(\n config.baseUrl,\n path,\n options.query as Record<string, string | number | boolean | string[]> | undefined,\n );\n\n // Build headers\n const headers = new Headers(config.headers);\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n headers.set(key, String(value));\n }\n }\n\n // Build request init\n const init: RequestInit = {\n method: endpoint.method,\n headers,\n };\n\n // Add body if present\n if (options.body !== undefined) {\n headers.set('content-type', 'application/json');\n init.body = JSON.stringify(options.body);\n }\n\n // Make request\n const response = await fetch(url, init);\n\n // Parse response\n let data: unknown;\n\n // Handle 204 No Content\n if (response.status === 204) {\n data = {};\n } else {\n const contentType = response.headers.get('content-type') || '';\n\n if (contentType.includes('application/json')) {\n data = await response.json();\n } else if (contentType.includes('text/')) {\n data = await response.text();\n } else {\n // Check if there's any content\n const text = await response.text();\n if (text) {\n data = text;\n } else {\n data = {};\n }\n }\n }\n\n // Check for HTTP errors\n if (!response.ok && !(response.status in endpoint.responses)) {\n throw new HTTPError(response.status, response.statusText, data);\n }\n\n // Validate response if enabled\n if (config.validateResponse !== false) {\n validateResponse(endpoint, response.status, data);\n }\n\n return {\n status: response.status,\n data,\n } as EndpointResponse<T>;\n}\n\n/**\n * Create a typesafe client for a contract\n */\nexport function createClient<T extends Contract>(contract: T, config: ClientConfig): Client<T> {\n const client: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n client[name] = (options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n return makeRequest(config, endpoint, options);\n };\n }\n\n return client as Client<T>;\n}\n\n/**\n * Create a client without providing the contract at runtime\n * Useful when you only need types and want a lighter bundle\n */\nexport function createTypedClient<T extends Contract>(_config: ClientConfig): Client<T> {\n return new Proxy({} as Client<T>, {\n get(_target, _prop: string) {\n return async (_options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n // Without the contract, we can't validate or infer the endpoint\n // This is just a basic fetch wrapper with typing\n throw new Error(\n 'createTypedClient requires contract at runtime for validation. Use createClient instead.',\n );\n };\n },\n });\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "BC1C6659CB2717F064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/client/index.ts
|
|
3
|
+
import { buildUrl, interpolatePath } from "@richie-rpc/core";
|
|
4
|
+
|
|
5
|
+
class ClientValidationError extends Error {
|
|
6
|
+
field;
|
|
7
|
+
issues;
|
|
8
|
+
constructor(field, issues) {
|
|
9
|
+
super(`Validation failed for ${field}`);
|
|
10
|
+
this.field = field;
|
|
11
|
+
this.issues = issues;
|
|
12
|
+
this.name = "ClientValidationError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class HTTPError extends Error {
|
|
17
|
+
status;
|
|
18
|
+
statusText;
|
|
19
|
+
body;
|
|
20
|
+
constructor(status, statusText, body) {
|
|
21
|
+
super(`HTTP Error ${status}: ${statusText}`);
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.statusText = statusText;
|
|
24
|
+
this.body = body;
|
|
25
|
+
this.name = "HTTPError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function validateRequest(endpoint, options) {
|
|
29
|
+
if (endpoint.params && options.params) {
|
|
30
|
+
const result = endpoint.params.safeParse(options.params);
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
throw new ClientValidationError("params", result.error.issues);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (endpoint.query && options.query) {
|
|
36
|
+
const result = endpoint.query.safeParse(options.query);
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
throw new ClientValidationError("query", result.error.issues);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (endpoint.headers && options.headers) {
|
|
42
|
+
const result = endpoint.headers.safeParse(options.headers);
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
throw new ClientValidationError("headers", result.error.issues);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (endpoint.body && options.body) {
|
|
48
|
+
const result = endpoint.body.safeParse(options.body);
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
throw new ClientValidationError("body", result.error.issues);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function validateResponse(endpoint, status, data) {
|
|
55
|
+
const responseSchema = endpoint.responses[status];
|
|
56
|
+
if (responseSchema) {
|
|
57
|
+
const result = responseSchema.safeParse(data);
|
|
58
|
+
if (!result.success) {
|
|
59
|
+
throw new ClientValidationError(`response[${status}]`, result.error.issues);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function makeRequest(config, endpoint, options) {
|
|
64
|
+
if (config.validateRequest !== false) {
|
|
65
|
+
validateRequest(endpoint, options);
|
|
66
|
+
}
|
|
67
|
+
let path = endpoint.path;
|
|
68
|
+
if (options.params) {
|
|
69
|
+
path = interpolatePath(path, options.params);
|
|
70
|
+
}
|
|
71
|
+
const url = buildUrl(config.baseUrl, path, options.query);
|
|
72
|
+
const headers = new Headers(config.headers);
|
|
73
|
+
if (options.headers) {
|
|
74
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
75
|
+
headers.set(key, String(value));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const init = {
|
|
79
|
+
method: endpoint.method,
|
|
80
|
+
headers
|
|
81
|
+
};
|
|
82
|
+
if (options.body !== undefined) {
|
|
83
|
+
headers.set("content-type", "application/json");
|
|
84
|
+
init.body = JSON.stringify(options.body);
|
|
85
|
+
}
|
|
86
|
+
const response = await fetch(url, init);
|
|
87
|
+
let data;
|
|
88
|
+
if (response.status === 204) {
|
|
89
|
+
data = {};
|
|
90
|
+
} else {
|
|
91
|
+
const contentType = response.headers.get("content-type") || "";
|
|
92
|
+
if (contentType.includes("application/json")) {
|
|
93
|
+
data = await response.json();
|
|
94
|
+
} else if (contentType.includes("text/")) {
|
|
95
|
+
data = await response.text();
|
|
96
|
+
} else {
|
|
97
|
+
const text = await response.text();
|
|
98
|
+
if (text) {
|
|
99
|
+
data = text;
|
|
100
|
+
} else {
|
|
101
|
+
data = {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!response.ok && !(response.status in endpoint.responses)) {
|
|
106
|
+
throw new HTTPError(response.status, response.statusText, data);
|
|
107
|
+
}
|
|
108
|
+
if (config.validateResponse !== false) {
|
|
109
|
+
validateResponse(endpoint, response.status, data);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
status: response.status,
|
|
113
|
+
data
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function createClient(contract, config) {
|
|
117
|
+
const client = {};
|
|
118
|
+
for (const [name, endpoint] of Object.entries(contract)) {
|
|
119
|
+
client[name] = (options = {}) => {
|
|
120
|
+
return makeRequest(config, endpoint, options);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return client;
|
|
124
|
+
}
|
|
125
|
+
function createTypedClient(_config) {
|
|
126
|
+
return new Proxy({}, {
|
|
127
|
+
get(_target, _prop) {
|
|
128
|
+
return async (_options = {}) => {
|
|
129
|
+
throw new Error("createTypedClient requires contract at runtime for validation. Use createClient instead.");
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
createTypedClient,
|
|
136
|
+
createClient,
|
|
137
|
+
HTTPError,
|
|
138
|
+
ClientValidationError
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
//# debugId=1B7C82F232BA1F0964756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { buildUrl, interpolatePath } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Client configuration\nexport interface ClientConfig {\n baseUrl: string;\n headers?: Record<string, string>;\n validateRequest?: boolean;\n validateResponse?: boolean;\n}\n\n// Request options for an endpoint\nexport type EndpointRequestOptions<T extends EndpointDefinition> = {\n params?: ExtractParams<T> extends never ? never : ExtractParams<T>;\n query?: ExtractQuery<T> extends never ? never : ExtractQuery<T>;\n headers?: ExtractHeaders<T> extends never ? never : ExtractHeaders<T>;\n body?: ExtractBody<T> extends never ? never : ExtractBody<T>;\n};\n\n// Response type for an endpoint (union of all possible responses)\nexport type EndpointResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n data: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n };\n}[keyof T['responses']];\n\n// Client method type for an endpoint\nexport type ClientMethod<T extends EndpointDefinition> = (\n options: EndpointRequestOptions<T>,\n) => Promise<EndpointResponse<T>>;\n\n// Client type for a contract\nexport type Client<T extends Contract> = {\n [K in keyof T]: ClientMethod<T[K]>;\n};\n\n// Validation error\nexport class ClientValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n ) {\n super(`Validation failed for ${field}`);\n this.name = 'ClientValidationError';\n }\n}\n\n// HTTP error\nexport class HTTPError extends Error {\n constructor(\n public status: number,\n public statusText: string,\n public body: unknown,\n ) {\n super(`HTTP Error ${status}: ${statusText}`);\n this.name = 'HTTPError';\n }\n}\n\n/**\n * Validate request data before sending\n */\nfunction validateRequest<T extends EndpointDefinition>(\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): void {\n // Validate params\n if (endpoint.params && options.params) {\n const result = endpoint.params.safeParse(options.params);\n if (!result.success) {\n throw new ClientValidationError('params', result.error.issues);\n }\n }\n\n // Validate query\n if (endpoint.query && options.query) {\n const result = endpoint.query.safeParse(options.query);\n if (!result.success) {\n throw new ClientValidationError('query', result.error.issues);\n }\n }\n\n // Validate headers\n if (endpoint.headers && options.headers) {\n const result = endpoint.headers.safeParse(options.headers);\n if (!result.success) {\n throw new ClientValidationError('headers', result.error.issues);\n }\n }\n\n // Validate body\n if (endpoint.body && options.body) {\n const result = endpoint.body.safeParse(options.body);\n if (!result.success) {\n throw new ClientValidationError('body', result.error.issues);\n }\n }\n}\n\n/**\n * Validate response data after receiving\n */\nfunction validateResponse<T extends EndpointDefinition>(\n endpoint: T,\n status: number,\n data: unknown,\n): void {\n const responseSchema = endpoint.responses[status];\n if (responseSchema) {\n const result = responseSchema.safeParse(data);\n if (!result.success) {\n throw new ClientValidationError(`response[${status}]`, result.error.issues);\n }\n }\n}\n\n/**\n * Make a request to an endpoint\n */\nasync function makeRequest<T extends EndpointDefinition>(\n config: ClientConfig,\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): Promise<EndpointResponse<T>> {\n // Validate request if enabled\n if (config.validateRequest !== false) {\n validateRequest(endpoint, options);\n }\n\n // Build URL\n let path = endpoint.path;\n if (options.params) {\n path = interpolatePath(path, options.params as Record<string, string | number>);\n }\n\n const url = buildUrl(\n config.baseUrl,\n path,\n options.query as Record<string, string | number | boolean | string[]> | undefined,\n );\n\n // Build headers\n const headers = new Headers(config.headers);\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n headers.set(key, String(value));\n }\n }\n\n // Build request init\n const init: RequestInit = {\n method: endpoint.method,\n headers,\n };\n\n // Add body if present\n if (options.body !== undefined) {\n headers.set('content-type', 'application/json');\n init.body = JSON.stringify(options.body);\n }\n\n // Make request\n const response = await fetch(url, init);\n\n // Parse response\n let data: unknown;\n\n // Handle 204 No Content\n if (response.status === 204) {\n data = {};\n } else {\n const contentType = response.headers.get('content-type') || '';\n\n if (contentType.includes('application/json')) {\n data = await response.json();\n } else if (contentType.includes('text/')) {\n data = await response.text();\n } else {\n // Check if there's any content\n const text = await response.text();\n if (text) {\n data = text;\n } else {\n data = {};\n }\n }\n }\n\n // Check for HTTP errors\n if (!response.ok && !(response.status in endpoint.responses)) {\n throw new HTTPError(response.status, response.statusText, data);\n }\n\n // Validate response if enabled\n if (config.validateResponse !== false) {\n validateResponse(endpoint, response.status, data);\n }\n\n return {\n status: response.status,\n data,\n } as EndpointResponse<T>;\n}\n\n/**\n * Create a typesafe client for a contract\n */\nexport function createClient<T extends Contract>(contract: T, config: ClientConfig): Client<T> {\n const client: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n client[name] = (options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n return makeRequest(config, endpoint, options);\n };\n }\n\n return client as Client<T>;\n}\n\n/**\n * Create a client without providing the contract at runtime\n * Useful when you only need types and want a lighter bundle\n */\nexport function createTypedClient<T extends Contract>(_config: ClientConfig): Client<T> {\n return new Proxy({} as Client<T>, {\n get(_target, _prop: string) {\n return async (_options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n // Without the contract, we can't validate or infer the endpoint\n // This is just a basic fetch wrapper with typing\n throw new Error(\n 'createTypedClient requires contract at runtime for validation. Use createClient instead.',\n );\n };\n },\n });\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "1B7C82F232BA1F0964756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Contract, EndpointDefinition, ExtractBody, ExtractHeaders, ExtractParams, ExtractQuery } from '@richie-rpc/core';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
export interface ClientConfig {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
validateRequest?: boolean;
|
|
7
|
+
validateResponse?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export type EndpointRequestOptions<T extends EndpointDefinition> = {
|
|
10
|
+
params?: ExtractParams<T> extends never ? never : ExtractParams<T>;
|
|
11
|
+
query?: ExtractQuery<T> extends never ? never : ExtractQuery<T>;
|
|
12
|
+
headers?: ExtractHeaders<T> extends never ? never : ExtractHeaders<T>;
|
|
13
|
+
body?: ExtractBody<T> extends never ? never : ExtractBody<T>;
|
|
14
|
+
};
|
|
15
|
+
export type EndpointResponse<T extends EndpointDefinition> = {
|
|
16
|
+
[Status in keyof T['responses']]: {
|
|
17
|
+
status: Status;
|
|
18
|
+
data: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;
|
|
19
|
+
};
|
|
20
|
+
}[keyof T['responses']];
|
|
21
|
+
export type ClientMethod<T extends EndpointDefinition> = (options: EndpointRequestOptions<T>) => Promise<EndpointResponse<T>>;
|
|
22
|
+
export type Client<T extends Contract> = {
|
|
23
|
+
[K in keyof T]: ClientMethod<T[K]>;
|
|
24
|
+
};
|
|
25
|
+
export declare class ClientValidationError extends Error {
|
|
26
|
+
field: string;
|
|
27
|
+
issues: z.ZodIssue[];
|
|
28
|
+
constructor(field: string, issues: z.ZodIssue[]);
|
|
29
|
+
}
|
|
30
|
+
export declare class HTTPError extends Error {
|
|
31
|
+
status: number;
|
|
32
|
+
statusText: string;
|
|
33
|
+
body: unknown;
|
|
34
|
+
constructor(status: number, statusText: string, body: unknown);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create a typesafe client for a contract
|
|
38
|
+
*/
|
|
39
|
+
export declare function createClient<T extends Contract>(contract: T, config: ClientConfig): Client<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Create a client without providing the contract at runtime
|
|
42
|
+
* Useful when you only need types and want a lighter bundle
|
|
43
|
+
*/
|
|
44
|
+
export declare function createTypedClient<T extends Contract>(_config: ClientConfig): Client<T>;
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@richie-rpc/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "./dist/cjs/index.cjs",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/types/index.d.ts",
|
|
8
|
+
"require": "./dist/cjs/index.cjs",
|
|
9
|
+
"import": "./dist/mjs/index.mjs"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@richie-rpc/core": "^0.1.0",
|
|
14
|
+
"zod": "^3.23.8"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"typescript": "^5"
|
|
18
|
+
},
|
|
19
|
+
"author": "Richie <oss@ricsam.dev>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/ricsam/richie-rpc.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/ricsam/richie-rpc/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/ricsam/richie-rpc#readme",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"typescript",
|
|
31
|
+
"bun",
|
|
32
|
+
"zod",
|
|
33
|
+
"api",
|
|
34
|
+
"contract",
|
|
35
|
+
"rpc",
|
|
36
|
+
"rest",
|
|
37
|
+
"openapi",
|
|
38
|
+
"type-safe"
|
|
39
|
+
],
|
|
40
|
+
"description": "Type-safe fetch client for Richie RPC contracts",
|
|
41
|
+
"module": "./dist/mjs/index.mjs",
|
|
42
|
+
"types": "./dist/types/index.d.ts",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist",
|
|
48
|
+
"README.md"
|
|
49
|
+
]
|
|
50
|
+
}
|