@hypequery/serve 0.0.1
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/LICENSE +201 -0
- package/README.md +1039 -0
- package/dist/adapters/fetch.d.ts +3 -0
- package/dist/adapters/fetch.d.ts.map +1 -0
- package/dist/adapters/fetch.js +26 -0
- package/dist/adapters/node.d.ts +8 -0
- package/dist/adapters/node.d.ts.map +1 -0
- package/dist/adapters/node.js +105 -0
- package/dist/adapters/utils.d.ts +39 -0
- package/dist/adapters/utils.d.ts.map +1 -0
- package/dist/adapters/utils.js +114 -0
- package/dist/adapters/vercel.d.ts +7 -0
- package/dist/adapters/vercel.d.ts.map +1 -0
- package/dist/adapters/vercel.js +13 -0
- package/dist/auth.d.ts +14 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +37 -0
- package/dist/client-config.d.ts +44 -0
- package/dist/client-config.d.ts.map +1 -0
- package/dist/client-config.js +53 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +24 -0
- package/dist/docs-ui.d.ts +3 -0
- package/dist/docs-ui.d.ts.map +1 -0
- package/dist/docs-ui.js +34 -0
- package/dist/endpoint.d.ts +5 -0
- package/dist/endpoint.d.ts.map +1 -0
- package/dist/endpoint.js +58 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/openapi.d.ts +3 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +189 -0
- package/dist/queries.d.ts +3 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +1 -0
- package/dist/query.d.ts +4 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +1 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +56 -0
- package/dist/sdk-generator.d.ts +7 -0
- package/dist/sdk-generator.d.ts.map +1 -0
- package/dist/sdk-generator.js +143 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +580 -0
- package/dist/tenant.d.ts +35 -0
- package/dist/tenant.d.ts.map +1 -0
- package/dist/tenant.js +49 -0
- package/dist/type-tests/builder.test-d.d.ts +13 -0
- package/dist/type-tests/builder.test-d.d.ts.map +1 -0
- package/dist/type-tests/builder.test-d.js +20 -0
- package/dist/types.d.ts +363 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
# @hypequery/serve
|
|
2
|
+
|
|
3
|
+
Declarative HTTP server for exposing hypequery analytics endpoints. Build type-safe REST APIs with automatic OpenAPI documentation, authentication, middleware, and multi-platform deployment support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @hypequery/serve zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies: `zod@^3`, `tsx@^4` (optional, for dev server)
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// analytics/server.ts
|
|
17
|
+
import { defineServe } from '@hypequery/serve';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { db } from './database';
|
|
20
|
+
|
|
21
|
+
const api = defineServe({
|
|
22
|
+
queries: {
|
|
23
|
+
weeklyRevenue: {
|
|
24
|
+
inputSchema: z.object({ startDate: z.string() }),
|
|
25
|
+
outputSchema: z.object({ total: z.number() }),
|
|
26
|
+
query: async ({ input, ctx }) => {
|
|
27
|
+
const result = await db
|
|
28
|
+
.select({ total: sum(sales.amount) })
|
|
29
|
+
.from(sales)
|
|
30
|
+
.where(gte(sales.date, input.startDate));
|
|
31
|
+
return { total: result[0].total };
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Auto-register routes
|
|
38
|
+
api.route('/weeklyRevenue', api.queries.weeklyRevenue);
|
|
39
|
+
|
|
40
|
+
// Start server
|
|
41
|
+
await api.start({ port: 3000 });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Your API is now running with:
|
|
45
|
+
- **Endpoint**: `GET http://localhost:3000/weeklyRevenue?startDate=2025-01-01`
|
|
46
|
+
- **Docs**: `http://localhost:3000/docs` (interactive Swagger UI)
|
|
47
|
+
- **OpenAPI**: `http://localhost:3000/openapi.json` (machine-readable schema)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Core Concepts
|
|
52
|
+
|
|
53
|
+
### 1. Server Builders
|
|
54
|
+
|
|
55
|
+
#### `defineServe<TContext, TAuth, TQueries>(config)`
|
|
56
|
+
|
|
57
|
+
Main entry point for creating a hypequery server. Returns a `ServeBuilder` with methods to configure routes, middleware, and start the server.
|
|
58
|
+
|
|
59
|
+
**Parameters:**
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
interface ServeConfig<TContext, TAuth, TQueries> {
|
|
63
|
+
// Query definitions
|
|
64
|
+
queries?: TQueries;
|
|
65
|
+
|
|
66
|
+
// Base path for all routes
|
|
67
|
+
basePath?: string;
|
|
68
|
+
|
|
69
|
+
// Context factory (runs per-request to inject dependencies)
|
|
70
|
+
context?: TContext | ((opts: { request: ServeRequest; auth: TAuth | null }) => TContext | Promise<TContext>);
|
|
71
|
+
|
|
72
|
+
// Authentication strategies
|
|
73
|
+
auth?: AuthStrategy<TAuth> | AuthStrategy<TAuth>[];
|
|
74
|
+
|
|
75
|
+
// Global middlewares (run before every endpoint)
|
|
76
|
+
middlewares?: ServeMiddleware<any, any, TContext, TAuth>[];
|
|
77
|
+
|
|
78
|
+
// Multi-tenancy configuration
|
|
79
|
+
tenant?: TenantConfig<TAuth>;
|
|
80
|
+
|
|
81
|
+
// Lifecycle hooks
|
|
82
|
+
hooks?: ServeLifecycleHooks<TAuth>;
|
|
83
|
+
|
|
84
|
+
// OpenAPI configuration
|
|
85
|
+
openapi?: OpenApiOptions;
|
|
86
|
+
|
|
87
|
+
// Docs UI configuration
|
|
88
|
+
docs?: DocsOptions;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Returns:**
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
interface ServeBuilder<TQueries, TContext, TAuth> {
|
|
96
|
+
queries: TQueries; // Registered query definitions
|
|
97
|
+
_routeConfig: Record<string, { method: HttpMethod }>; // Route-level HTTP method overrides
|
|
98
|
+
|
|
99
|
+
// Register a route
|
|
100
|
+
route(path: string, endpoint: ServeEndpoint, options?: RouteOptions): ServeBuilder;
|
|
101
|
+
|
|
102
|
+
// Add global middleware
|
|
103
|
+
use(middleware: ServeMiddleware): ServeBuilder;
|
|
104
|
+
|
|
105
|
+
// Add authentication strategy
|
|
106
|
+
useAuth(strategy: AuthStrategy<TAuth>): ServeBuilder;
|
|
107
|
+
|
|
108
|
+
// Execute query directly (bypasses HTTP)
|
|
109
|
+
execute<K extends keyof TQueries>(
|
|
110
|
+
key: K,
|
|
111
|
+
options?: { input?: any; context?: Partial<TContext>; request?: Partial<ServeRequest> }
|
|
112
|
+
): Promise<QueryResult<TQueries[K]>>;
|
|
113
|
+
|
|
114
|
+
// Get toolkit description (for LLM integration)
|
|
115
|
+
describe(): ToolkitDescription;
|
|
116
|
+
|
|
117
|
+
// Raw HTTP handler (for custom adapters)
|
|
118
|
+
handler: ServeHandler;
|
|
119
|
+
|
|
120
|
+
// Start Node.js HTTP server
|
|
121
|
+
start(options?: StartServerOptions): Promise<{ server: Server; stop: () => Promise<void> }>;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Example:**
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const api = defineServe({
|
|
129
|
+
basePath: '/api',
|
|
130
|
+
context: async ({ request, auth }) => ({
|
|
131
|
+
db: createDatabaseConnection(),
|
|
132
|
+
userId: auth?.userId,
|
|
133
|
+
}),
|
|
134
|
+
auth: createBearerTokenStrategy({
|
|
135
|
+
validate: async (token) => {
|
|
136
|
+
const user = await verifyJWT(token);
|
|
137
|
+
return user ? { userId: user.id, role: user.role } : null;
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
queries: {
|
|
141
|
+
getUser: {
|
|
142
|
+
inputSchema: z.object({ id: z.string() }),
|
|
143
|
+
outputSchema: z.object({ name: z.string(), email: z.string() }),
|
|
144
|
+
query: async ({ input, ctx }) => {
|
|
145
|
+
return ctx.db.query.users.findFirst({
|
|
146
|
+
where: eq(users.id, input.id),
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
api.route('/users/:id', api.queries.getUser, { method: 'GET' });
|
|
154
|
+
|
|
155
|
+
await api.start({ port: 4000 });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
#### `initServe<TContext, TAuth>(options)`
|
|
161
|
+
|
|
162
|
+
Advanced pattern for defining reusable query builders with context type inference. Use this when you want to define context once and create multiple queries that share the same types.
|
|
163
|
+
|
|
164
|
+
**Parameters:**
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
interface ServeInitializerOptions<TContext, TAuth> {
|
|
168
|
+
context: TContext | ((opts: { request: ServeRequest; auth: TAuth | null }) => TContext | Promise<TContext>);
|
|
169
|
+
basePath?: string;
|
|
170
|
+
auth?: AuthStrategy<TAuth> | AuthStrategy<TAuth>[];
|
|
171
|
+
middlewares?: ServeMiddleware<any, any, TContext, TAuth>[];
|
|
172
|
+
tenant?: TenantConfig<TAuth>;
|
|
173
|
+
hooks?: ServeLifecycleHooks<TAuth>;
|
|
174
|
+
openapi?: OpenApiOptions;
|
|
175
|
+
docs?: DocsOptions;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Returns:**
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
interface ServeInitializer<TContext, TAuth> {
|
|
183
|
+
// Procedure builder (chainable query configuration)
|
|
184
|
+
procedure: QueryProcedureBuilder<TContext, TAuth>;
|
|
185
|
+
|
|
186
|
+
// Alias for procedure
|
|
187
|
+
query: QueryProcedureBuilder<TContext, TAuth>;
|
|
188
|
+
|
|
189
|
+
// Helper to group queries
|
|
190
|
+
queries<TQueries>(definitions: TQueries): TQueries;
|
|
191
|
+
|
|
192
|
+
// Define server with queries
|
|
193
|
+
define<TQueries>(config: { queries: TQueries }): ServeBuilder<TQueries, TContext, TAuth>;
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Example:**
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
// Define context once
|
|
201
|
+
const t = initServe({
|
|
202
|
+
context: async ({ auth }) => ({
|
|
203
|
+
db: createDatabase(),
|
|
204
|
+
userId: auth?.userId,
|
|
205
|
+
}),
|
|
206
|
+
auth: createApiKeyStrategy({
|
|
207
|
+
validate: async (key) => {
|
|
208
|
+
const user = await validateApiKey(key);
|
|
209
|
+
return user ? { userId: user.id } : null;
|
|
210
|
+
},
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Create queries with inferred types
|
|
215
|
+
const queries = t.queries({
|
|
216
|
+
getRevenue: t.procedure
|
|
217
|
+
.input(z.object({ startDate: z.string() }))
|
|
218
|
+
.output(z.object({ total: z.number() }))
|
|
219
|
+
.query(async ({ input, ctx }) => {
|
|
220
|
+
// ctx is fully typed as { db, userId }
|
|
221
|
+
return ctx.db.query.sales.aggregate({ startDate: input.startDate });
|
|
222
|
+
}),
|
|
223
|
+
|
|
224
|
+
createSale: t.procedure
|
|
225
|
+
.input(z.object({ amount: z.number(), product: z.string() }))
|
|
226
|
+
.output(z.object({ id: z.string() }))
|
|
227
|
+
.method('POST')
|
|
228
|
+
.query(async ({ input, ctx }) => {
|
|
229
|
+
const [sale] = await ctx.db.insert(sales).values({
|
|
230
|
+
amount: input.amount,
|
|
231
|
+
product: input.product,
|
|
232
|
+
userId: ctx.userId,
|
|
233
|
+
});
|
|
234
|
+
return { id: sale.id };
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Define server
|
|
239
|
+
const api = t.define({ queries });
|
|
240
|
+
|
|
241
|
+
api
|
|
242
|
+
.route('/revenue', api.queries.getRevenue)
|
|
243
|
+
.route('/sales', api.queries.createSale);
|
|
244
|
+
|
|
245
|
+
await api.start();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### 2. HTTP Adapters
|
|
251
|
+
|
|
252
|
+
Adapters convert the framework-agnostic `ServeHandler` into platform-specific handlers.
|
|
253
|
+
|
|
254
|
+
#### `createNodeHandler(handler)`
|
|
255
|
+
|
|
256
|
+
Creates a Node.js HTTP handler for use with `http.createServer()` or Express.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { createNodeHandler } from '@hypequery/serve';
|
|
260
|
+
import { createServer } from 'http';
|
|
261
|
+
|
|
262
|
+
const nodeHandler = createNodeHandler(api.handler);
|
|
263
|
+
const server = createServer(nodeHandler);
|
|
264
|
+
server.listen(3000);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
#### `createFetchHandler(handler)`
|
|
270
|
+
|
|
271
|
+
Creates a Web Fetch API handler for modern runtimes (Cloudflare Workers, Deno, Bun).
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { createFetchHandler } from '@hypequery/serve';
|
|
275
|
+
|
|
276
|
+
const fetchHandler = createFetchHandler(api.handler);
|
|
277
|
+
|
|
278
|
+
// Cloudflare Workers
|
|
279
|
+
export default {
|
|
280
|
+
fetch: fetchHandler,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// Deno
|
|
284
|
+
Deno.serve(fetchHandler);
|
|
285
|
+
|
|
286
|
+
// Bun
|
|
287
|
+
Bun.serve({
|
|
288
|
+
fetch: fetchHandler,
|
|
289
|
+
port: 3000,
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
#### `createVercelEdgeHandler(handler)`
|
|
296
|
+
|
|
297
|
+
Creates a Vercel Edge Runtime handler (uses Fetch API).
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
// pages/api/analytics.ts
|
|
301
|
+
import { createVercelEdgeHandler } from '@hypequery/serve';
|
|
302
|
+
import { api } from '@/analytics/server';
|
|
303
|
+
|
|
304
|
+
export const config = { runtime: 'edge' };
|
|
305
|
+
export default createVercelEdgeHandler(api.handler);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
#### `createVercelNodeHandler(handler)`
|
|
311
|
+
|
|
312
|
+
Creates a Vercel Node.js handler (uses Node HTTP).
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
// pages/api/analytics.ts
|
|
316
|
+
import { createVercelNodeHandler } from '@hypequery/serve';
|
|
317
|
+
import { api } from '@/analytics/server';
|
|
318
|
+
|
|
319
|
+
export default createVercelNodeHandler(api.handler);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
### 3. Authentication
|
|
325
|
+
|
|
326
|
+
#### `createApiKeyStrategy<TAuth>(options)`
|
|
327
|
+
|
|
328
|
+
Creates an authentication strategy that validates API keys from headers or query parameters.
|
|
329
|
+
|
|
330
|
+
**Parameters:**
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
interface ApiKeyStrategyOptions<TAuth> {
|
|
334
|
+
header?: string; // Header name (default: "authorization")
|
|
335
|
+
queryParam?: string; // Query param name (optional)
|
|
336
|
+
validate: (key: string, request: ServeRequest) => Promise<TAuth | null> | TAuth | null;
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Example:**
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import { createApiKeyStrategy } from '@hypequery/serve';
|
|
344
|
+
|
|
345
|
+
const apiKeyAuth = createApiKeyStrategy({
|
|
346
|
+
header: 'x-api-key',
|
|
347
|
+
queryParam: 'apiKey', // Allow ?apiKey=xxx for development
|
|
348
|
+
validate: async (key, request) => {
|
|
349
|
+
const user = await db.query.apiKeys.findFirst({
|
|
350
|
+
where: eq(apiKeys.key, key),
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
if (!user || user.revoked) return null;
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
userId: user.userId,
|
|
357
|
+
scopes: user.scopes,
|
|
358
|
+
};
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const api = defineServe({
|
|
363
|
+
auth: apiKeyAuth,
|
|
364
|
+
queries: { /* ... */ },
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Usage:**
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
# Header (preferred)
|
|
372
|
+
curl -H "x-api-key: sk_live_abc123" http://localhost:3000/revenue
|
|
373
|
+
|
|
374
|
+
# Query param (development only)
|
|
375
|
+
curl http://localhost:3000/revenue?apiKey=sk_live_abc123
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
#### `createBearerTokenStrategy<TAuth>(options)`
|
|
381
|
+
|
|
382
|
+
Creates an authentication strategy that validates Bearer tokens (JWT, OAuth).
|
|
383
|
+
|
|
384
|
+
**Parameters:**
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
interface BearerTokenStrategyOptions<TAuth> {
|
|
388
|
+
header?: string; // Header name (default: "authorization")
|
|
389
|
+
prefix?: string; // Token prefix (default: "Bearer ")
|
|
390
|
+
validate: (token: string, request: ServeRequest) => Promise<TAuth | null> | TAuth | null;
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Example:**
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
import { createBearerTokenStrategy } from '@hypequery/serve';
|
|
398
|
+
import jwt from 'jsonwebtoken';
|
|
399
|
+
|
|
400
|
+
const jwtAuth = createBearerTokenStrategy({
|
|
401
|
+
validate: async (token) => {
|
|
402
|
+
try {
|
|
403
|
+
const payload = jwt.verify(token, process.env.JWT_SECRET) as {
|
|
404
|
+
sub: string;
|
|
405
|
+
role: string;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
userId: payload.sub,
|
|
410
|
+
role: payload.role,
|
|
411
|
+
};
|
|
412
|
+
} catch {
|
|
413
|
+
return null; // Invalid token
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const api = defineServe({
|
|
419
|
+
auth: jwtAuth,
|
|
420
|
+
queries: { /* ... */ },
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Usage:**
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
curl -H "Authorization: Bearer eyJhbGc..." http://localhost:3000/revenue
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
### 4. Procedure Builder (Advanced Query Configuration)
|
|
433
|
+
|
|
434
|
+
The procedure builder provides a chainable API for configuring queries with full type inference.
|
|
435
|
+
|
|
436
|
+
**Available Methods:**
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
interface QueryProcedureBuilder<TContext, TAuth> {
|
|
440
|
+
// Input schema (Zod)
|
|
441
|
+
input<TSchema extends ZodTypeAny>(schema: TSchema): QueryProcedureBuilder;
|
|
442
|
+
|
|
443
|
+
// Output schema (Zod)
|
|
444
|
+
output<TSchema extends ZodTypeAny>(schema: TSchema): QueryProcedureBuilder;
|
|
445
|
+
|
|
446
|
+
// HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
447
|
+
method(method: HttpMethod): QueryProcedureBuilder;
|
|
448
|
+
|
|
449
|
+
// Summary (OpenAPI short description)
|
|
450
|
+
summary(summary: string): QueryProcedureBuilder;
|
|
451
|
+
|
|
452
|
+
// Description (OpenAPI detailed description, supports Markdown)
|
|
453
|
+
describe(description: string): QueryProcedureBuilder;
|
|
454
|
+
|
|
455
|
+
// Add single tag (for OpenAPI grouping)
|
|
456
|
+
tag(tag: string): QueryProcedureBuilder;
|
|
457
|
+
|
|
458
|
+
// Add multiple tags
|
|
459
|
+
tags(tags: string[]): QueryProcedureBuilder;
|
|
460
|
+
|
|
461
|
+
// Cache TTL in milliseconds (sets Cache-Control header)
|
|
462
|
+
cache(ttlMs: number | null): QueryProcedureBuilder;
|
|
463
|
+
|
|
464
|
+
// Authentication strategy (overrides global auth)
|
|
465
|
+
auth(strategy: AuthStrategy<TAuth>): QueryProcedureBuilder;
|
|
466
|
+
|
|
467
|
+
// Multi-tenancy configuration
|
|
468
|
+
tenant(config: TenantConfig<TAuth>): QueryProcedureBuilder;
|
|
469
|
+
|
|
470
|
+
// Custom metadata (for extensions)
|
|
471
|
+
custom(metadata: Record<string, unknown>): QueryProcedureBuilder;
|
|
472
|
+
|
|
473
|
+
// Add middleware (runs before query handler)
|
|
474
|
+
use(...middlewares: ServeMiddleware[]): QueryProcedureBuilder;
|
|
475
|
+
|
|
476
|
+
// Define query handler (terminal operation)
|
|
477
|
+
query<TExecutable extends ExecutableQuery>(
|
|
478
|
+
executable: TExecutable
|
|
479
|
+
): ServeQueryConfig;
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Example:**
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
const t = initServe({
|
|
487
|
+
context: async () => ({ db: createDatabase() }),
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const getAnalytics = t.procedure
|
|
491
|
+
.input(z.object({
|
|
492
|
+
startDate: z.string(),
|
|
493
|
+
endDate: z.string(),
|
|
494
|
+
metric: z.enum(['revenue', 'users', 'sessions']),
|
|
495
|
+
}))
|
|
496
|
+
.output(z.object({
|
|
497
|
+
total: z.number(),
|
|
498
|
+
trend: z.number(),
|
|
499
|
+
}))
|
|
500
|
+
.method('GET')
|
|
501
|
+
.summary('Fetch analytics data')
|
|
502
|
+
.describe(`
|
|
503
|
+
Returns aggregated analytics for the specified metric and date range.
|
|
504
|
+
Supports revenue, user count, and session metrics.
|
|
505
|
+
`)
|
|
506
|
+
.tag('Analytics')
|
|
507
|
+
.cache(300000) // Cache for 5 minutes
|
|
508
|
+
.query(async ({ input, ctx }) => {
|
|
509
|
+
const result = await ctx.db.query.analytics.aggregate({
|
|
510
|
+
where: and(
|
|
511
|
+
gte(analytics.date, input.startDate),
|
|
512
|
+
lte(analytics.date, input.endDate),
|
|
513
|
+
),
|
|
514
|
+
metric: input.metric,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
total: result.total,
|
|
519
|
+
trend: result.trend,
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
### 5. Multi-Tenancy
|
|
527
|
+
|
|
528
|
+
Hypequery supports multi-tenant applications with automatic tenant isolation.
|
|
529
|
+
|
|
530
|
+
**Configuration:**
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
interface TenantConfig<TAuth> {
|
|
534
|
+
// Extract tenant ID from auth context
|
|
535
|
+
extract: (auth: TAuth) => string | null;
|
|
536
|
+
|
|
537
|
+
// Tenant isolation mode
|
|
538
|
+
mode?: 'manual' | 'auto-inject'; // Default: 'manual'
|
|
539
|
+
|
|
540
|
+
// Column name for tenant filtering (required for auto-inject mode)
|
|
541
|
+
column?: string;
|
|
542
|
+
|
|
543
|
+
// Is tenant required? (default: true)
|
|
544
|
+
required?: boolean;
|
|
545
|
+
|
|
546
|
+
// Custom error message when tenant is missing
|
|
547
|
+
errorMessage?: string;
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Example (Manual Mode):**
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
const api = defineServe({
|
|
555
|
+
tenant: {
|
|
556
|
+
extract: (auth) => auth?.tenantId ?? null,
|
|
557
|
+
mode: 'manual', // You manually filter by tenantId
|
|
558
|
+
required: true,
|
|
559
|
+
},
|
|
560
|
+
context: async ({ auth }) => ({
|
|
561
|
+
db: createDatabase(),
|
|
562
|
+
tenantId: auth?.tenantId, // Injected by framework
|
|
563
|
+
}),
|
|
564
|
+
queries: {
|
|
565
|
+
getUsers: {
|
|
566
|
+
query: async ({ ctx }) => {
|
|
567
|
+
// Manually filter by tenant
|
|
568
|
+
return ctx.db.query.users.findMany({
|
|
569
|
+
where: eq(users.tenantId, ctx.tenantId),
|
|
570
|
+
});
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Example (Auto-Inject Mode):**
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
const api = defineServe({
|
|
581
|
+
tenant: {
|
|
582
|
+
extract: (auth) => auth?.organizationId ?? null,
|
|
583
|
+
mode: 'auto-inject',
|
|
584
|
+
column: 'organization_id', // Column to filter on
|
|
585
|
+
},
|
|
586
|
+
context: async () => ({
|
|
587
|
+
db: createDatabase(),
|
|
588
|
+
}),
|
|
589
|
+
queries: {
|
|
590
|
+
getUsers: {
|
|
591
|
+
query: async ({ ctx }) => {
|
|
592
|
+
// Tenant filter is automatically injected
|
|
593
|
+
return ctx.db.query.users.findMany();
|
|
594
|
+
// Equivalent to: WHERE organization_id = <tenant_id>
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
### 6. Client Configuration
|
|
604
|
+
|
|
605
|
+
#### `extractClientConfig(api)`
|
|
606
|
+
|
|
607
|
+
Extracts serializable client configuration from a `ServeBuilder`. Returns HTTP method information for each query, used by `@hypequery/react` to configure hooks.
|
|
608
|
+
|
|
609
|
+
**Example:**
|
|
610
|
+
|
|
611
|
+
```ts
|
|
612
|
+
// Server-side API route
|
|
613
|
+
import { api } from '@/analytics/server';
|
|
614
|
+
import { extractClientConfig } from '@hypequery/serve';
|
|
615
|
+
|
|
616
|
+
export async function GET() {
|
|
617
|
+
return Response.json(extractClientConfig(api));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Returns:
|
|
621
|
+
// {
|
|
622
|
+
// "weeklyRevenue": { "method": "GET" },
|
|
623
|
+
// "createSale": { "method": "POST" }
|
|
624
|
+
// }
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**Client-side usage:**
|
|
628
|
+
|
|
629
|
+
```ts
|
|
630
|
+
// lib/analytics.ts
|
|
631
|
+
import { createHooks } from '@hypequery/react';
|
|
632
|
+
import type { Api } from '@/analytics/server';
|
|
633
|
+
|
|
634
|
+
const config = await fetch('/api/config').then(r => r.json());
|
|
635
|
+
|
|
636
|
+
export const { useQuery, useMutation } = createHooks<Api>({
|
|
637
|
+
baseUrl: '/api/analytics',
|
|
638
|
+
config, // Auto-configures HTTP methods
|
|
639
|
+
});
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
#### `defineClientConfig(config)`
|
|
645
|
+
|
|
646
|
+
Type-safe helper to manually define client configuration when you can't access the API object.
|
|
647
|
+
|
|
648
|
+
**Example:**
|
|
649
|
+
|
|
650
|
+
```ts
|
|
651
|
+
import { defineClientConfig } from '@hypequery/serve';
|
|
652
|
+
|
|
653
|
+
const config = defineClientConfig({
|
|
654
|
+
weeklyRevenue: { method: 'GET' },
|
|
655
|
+
createSale: { method: 'POST' },
|
|
656
|
+
updateProduct: { method: 'PUT' },
|
|
657
|
+
deleteOrder: { method: 'DELETE' },
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
export const { useQuery, useMutation } = createHooks<Api>({
|
|
661
|
+
baseUrl: '/api',
|
|
662
|
+
config,
|
|
663
|
+
});
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
### 7. Development Server
|
|
669
|
+
|
|
670
|
+
#### `serveDev(api, options?)`
|
|
671
|
+
|
|
672
|
+
Starts a development server with enhanced logging and automatic documentation.
|
|
673
|
+
|
|
674
|
+
**Parameters:**
|
|
675
|
+
|
|
676
|
+
```ts
|
|
677
|
+
interface ServeDevOptions {
|
|
678
|
+
port?: number; // Default: 4000 or process.env.PORT
|
|
679
|
+
hostname?: string; // Default: 'localhost'
|
|
680
|
+
quiet?: boolean; // Suppress logs (default: false)
|
|
681
|
+
signal?: AbortSignal; // Graceful shutdown signal
|
|
682
|
+
logger?: (message: string) => void; // Custom logger
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
**Example:**
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
import { serveDev } from '@hypequery/serve';
|
|
690
|
+
import { api } from './analytics/server';
|
|
691
|
+
|
|
692
|
+
await serveDev(api, {
|
|
693
|
+
port: 4000,
|
|
694
|
+
logger: (msg) => console.log(`[API] ${msg}`),
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// Output:
|
|
698
|
+
// [API] hypequery dev server running at http://localhost:4000
|
|
699
|
+
// [API] Docs available at http://localhost:4000/docs
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## Advanced Features
|
|
705
|
+
|
|
706
|
+
### Middleware
|
|
707
|
+
|
|
708
|
+
Middlewares run before query handlers and can modify context, validate permissions, or log requests.
|
|
709
|
+
|
|
710
|
+
**Signature:**
|
|
711
|
+
|
|
712
|
+
```ts
|
|
713
|
+
type ServeMiddleware<TInput, TOutput, TContext, TAuth> = (
|
|
714
|
+
ctx: EndpointContext<TInput, TContext, TAuth>,
|
|
715
|
+
next: () => Promise<TOutput>
|
|
716
|
+
) => Promise<TOutput>;
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
**Example:**
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
// Logging middleware
|
|
723
|
+
const logMiddleware: ServeMiddleware<any, any, any, any> = async (ctx, next) => {
|
|
724
|
+
console.log(`[${ctx.request.method}] ${ctx.request.path}`);
|
|
725
|
+
const start = Date.now();
|
|
726
|
+
const result = await next();
|
|
727
|
+
console.log(`Completed in ${Date.now() - start}ms`);
|
|
728
|
+
return result;
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// Permission middleware
|
|
732
|
+
const requireAdmin: ServeMiddleware<any, any, any, { role: string }> = async (ctx, next) => {
|
|
733
|
+
if (ctx.auth?.role !== 'admin') {
|
|
734
|
+
throw new Error('Admin access required');
|
|
735
|
+
}
|
|
736
|
+
return next();
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// Apply globally
|
|
740
|
+
const api = defineServe({
|
|
741
|
+
middlewares: [logMiddleware],
|
|
742
|
+
queries: { /* ... */ },
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Apply per-query
|
|
746
|
+
const deleteUser = t.procedure
|
|
747
|
+
.use(requireAdmin)
|
|
748
|
+
.query(async ({ input, ctx }) => {
|
|
749
|
+
// Only admins can reach here
|
|
750
|
+
});
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
### Lifecycle Hooks
|
|
756
|
+
|
|
757
|
+
Hooks provide observability into the request lifecycle.
|
|
758
|
+
|
|
759
|
+
**Available Hooks:**
|
|
760
|
+
|
|
761
|
+
```ts
|
|
762
|
+
interface ServeLifecycleHooks<TAuth> {
|
|
763
|
+
// Before request processing
|
|
764
|
+
onRequestStart?: (event: {
|
|
765
|
+
requestId: string;
|
|
766
|
+
queryKey: string;
|
|
767
|
+
metadata: EndpointMetadata;
|
|
768
|
+
request: ServeRequest;
|
|
769
|
+
auth: TAuth | null;
|
|
770
|
+
}) => void | Promise<void>;
|
|
771
|
+
|
|
772
|
+
// After successful request
|
|
773
|
+
onRequestEnd?: (event: {
|
|
774
|
+
requestId: string;
|
|
775
|
+
queryKey: string;
|
|
776
|
+
metadata: EndpointMetadata;
|
|
777
|
+
request: ServeRequest;
|
|
778
|
+
auth: TAuth | null;
|
|
779
|
+
durationMs: number;
|
|
780
|
+
result: unknown;
|
|
781
|
+
}) => void | Promise<void>;
|
|
782
|
+
|
|
783
|
+
// On authentication failure
|
|
784
|
+
onAuthFailure?: (event: {
|
|
785
|
+
requestId: string;
|
|
786
|
+
queryKey: string;
|
|
787
|
+
metadata: EndpointMetadata;
|
|
788
|
+
request: ServeRequest;
|
|
789
|
+
auth: TAuth | null;
|
|
790
|
+
reason: 'MISSING' | 'INVALID';
|
|
791
|
+
}) => void | Promise<void>;
|
|
792
|
+
|
|
793
|
+
// On any error
|
|
794
|
+
onError?: (event: {
|
|
795
|
+
requestId: string;
|
|
796
|
+
queryKey: string;
|
|
797
|
+
metadata: EndpointMetadata;
|
|
798
|
+
request: ServeRequest;
|
|
799
|
+
auth: TAuth | null;
|
|
800
|
+
durationMs: number;
|
|
801
|
+
error: unknown;
|
|
802
|
+
}) => void | Promise<void>;
|
|
803
|
+
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
**Example:**
|
|
807
|
+
|
|
808
|
+
```ts
|
|
809
|
+
const api = defineServe({
|
|
810
|
+
hooks: {
|
|
811
|
+
onRequestStart: async (event) => {
|
|
812
|
+
await analytics.track({
|
|
813
|
+
event: 'api_request_start',
|
|
814
|
+
queryKey: event.queryKey,
|
|
815
|
+
userId: event.auth?.userId,
|
|
816
|
+
});
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
onError: async (event) => {
|
|
820
|
+
await errorReporting.captureException(event.error, {
|
|
821
|
+
queryKey: event.queryKey,
|
|
822
|
+
requestId: event.requestId,
|
|
823
|
+
});
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
queries: { /* ... */ },
|
|
827
|
+
});
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
### OpenAPI Configuration
|
|
833
|
+
|
|
834
|
+
**Options:**
|
|
835
|
+
|
|
836
|
+
```ts
|
|
837
|
+
interface OpenApiOptions {
|
|
838
|
+
enabled?: boolean; // Enable OpenAPI endpoint (default: true)
|
|
839
|
+
path?: string; // OpenAPI JSON path (default: '/openapi.json')
|
|
840
|
+
info?: {
|
|
841
|
+
title?: string; // API title (default: 'Hypequery API')
|
|
842
|
+
version?: string; // API version (default: '1.0.0')
|
|
843
|
+
description?: string; // API description
|
|
844
|
+
};
|
|
845
|
+
servers?: Array<{
|
|
846
|
+
url: string;
|
|
847
|
+
description?: string;
|
|
848
|
+
}>;
|
|
849
|
+
}
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
**Example:**
|
|
853
|
+
|
|
854
|
+
```ts
|
|
855
|
+
const api = defineServe({
|
|
856
|
+
openapi: {
|
|
857
|
+
path: '/api-schema.json',
|
|
858
|
+
info: {
|
|
859
|
+
title: 'Analytics API',
|
|
860
|
+
version: '2.0.0',
|
|
861
|
+
description: 'Real-time analytics and reporting API',
|
|
862
|
+
},
|
|
863
|
+
servers: [
|
|
864
|
+
{ url: 'https://api.example.com', description: 'Production' },
|
|
865
|
+
{ url: 'http://localhost:4000', description: 'Development' },
|
|
866
|
+
],
|
|
867
|
+
},
|
|
868
|
+
queries: { /* ... */ },
|
|
869
|
+
});
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
---
|
|
873
|
+
|
|
874
|
+
### Documentation UI
|
|
875
|
+
|
|
876
|
+
**Options:**
|
|
877
|
+
|
|
878
|
+
```ts
|
|
879
|
+
interface DocsOptions {
|
|
880
|
+
enabled?: boolean; // Enable docs UI (default: true)
|
|
881
|
+
path?: string; // Docs UI path (default: '/docs')
|
|
882
|
+
title?: string; // Page title (default: 'API Documentation')
|
|
883
|
+
}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
**Example:**
|
|
887
|
+
|
|
888
|
+
```ts
|
|
889
|
+
const api = defineServe({
|
|
890
|
+
docs: {
|
|
891
|
+
path: '/api-docs',
|
|
892
|
+
title: 'Analytics API Reference',
|
|
893
|
+
},
|
|
894
|
+
queries: { /* ... */ },
|
|
895
|
+
});
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
---
|
|
899
|
+
|
|
900
|
+
## Deployment Examples
|
|
901
|
+
|
|
902
|
+
### Vercel (Edge Runtime)
|
|
903
|
+
|
|
904
|
+
```ts
|
|
905
|
+
// pages/api/analytics/[...path].ts
|
|
906
|
+
import { createVercelEdgeHandler } from '@hypequery/serve';
|
|
907
|
+
import { api } from '@/analytics/server';
|
|
908
|
+
|
|
909
|
+
export const config = { runtime: 'edge' };
|
|
910
|
+
export default createVercelEdgeHandler(api.handler);
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
---
|
|
914
|
+
|
|
915
|
+
### Cloudflare Workers
|
|
916
|
+
|
|
917
|
+
```ts
|
|
918
|
+
// src/index.ts
|
|
919
|
+
import { createFetchHandler } from '@hypequery/serve';
|
|
920
|
+
import { api } from './analytics/server';
|
|
921
|
+
|
|
922
|
+
const handler = createFetchHandler(api.handler);
|
|
923
|
+
|
|
924
|
+
export default {
|
|
925
|
+
fetch: handler,
|
|
926
|
+
};
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
---
|
|
930
|
+
|
|
931
|
+
### Express.js
|
|
932
|
+
|
|
933
|
+
```ts
|
|
934
|
+
// server.ts
|
|
935
|
+
import express from 'express';
|
|
936
|
+
import { createNodeHandler } from '@hypequery/serve';
|
|
937
|
+
import { api } from './analytics/server';
|
|
938
|
+
|
|
939
|
+
const app = express();
|
|
940
|
+
const analyticsHandler = createNodeHandler(api.handler);
|
|
941
|
+
|
|
942
|
+
app.use('/api/analytics', analyticsHandler);
|
|
943
|
+
app.listen(3000);
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
### Next.js App Router
|
|
949
|
+
|
|
950
|
+
```ts
|
|
951
|
+
// app/api/analytics/[...path]/route.ts
|
|
952
|
+
import { createFetchHandler } from '@hypequery/serve';
|
|
953
|
+
import { api } from '@/analytics/server';
|
|
954
|
+
|
|
955
|
+
const handler = createFetchHandler(api.handler);
|
|
956
|
+
|
|
957
|
+
export { handler as GET, handler as POST, handler as PUT, handler as DELETE };
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
## TypeScript
|
|
963
|
+
|
|
964
|
+
All functions are fully typed with automatic inference:
|
|
965
|
+
|
|
966
|
+
```ts
|
|
967
|
+
import { defineServe } from '@hypequery/serve';
|
|
968
|
+
import { z } from 'zod';
|
|
969
|
+
|
|
970
|
+
const api = defineServe({
|
|
971
|
+
context: async ({ auth }) => ({
|
|
972
|
+
db: createDatabase(),
|
|
973
|
+
userId: auth?.userId,
|
|
974
|
+
}),
|
|
975
|
+
queries: {
|
|
976
|
+
getUser: {
|
|
977
|
+
inputSchema: z.object({ id: z.string() }),
|
|
978
|
+
outputSchema: z.object({ name: z.string(), email: z.string() }),
|
|
979
|
+
query: async ({ input, ctx }) => {
|
|
980
|
+
// input: { id: string }
|
|
981
|
+
// ctx: { db: Database; userId: string | undefined }
|
|
982
|
+
return ctx.db.query.users.findFirst({
|
|
983
|
+
where: eq(users.id, input.id),
|
|
984
|
+
});
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// Execute with type safety
|
|
991
|
+
const user = await api.execute('getUser', {
|
|
992
|
+
input: { id: '123' }
|
|
993
|
+
});
|
|
994
|
+
// user: { name: string; email: string }
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
---
|
|
998
|
+
|
|
999
|
+
## Error Handling
|
|
1000
|
+
|
|
1001
|
+
All errors follow a consistent format:
|
|
1002
|
+
|
|
1003
|
+
```ts
|
|
1004
|
+
interface ErrorEnvelope {
|
|
1005
|
+
error: {
|
|
1006
|
+
type: 'VALIDATION_ERROR' | 'UNAUTHORIZED' | 'NOT_FOUND' | 'INTERNAL_SERVER_ERROR';
|
|
1007
|
+
message: string;
|
|
1008
|
+
details?: Record<string, unknown>;
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
**Example Error Response:**
|
|
1014
|
+
|
|
1015
|
+
```json
|
|
1016
|
+
{
|
|
1017
|
+
"error": {
|
|
1018
|
+
"type": "VALIDATION_ERROR",
|
|
1019
|
+
"message": "Request validation failed",
|
|
1020
|
+
"details": {
|
|
1021
|
+
"issues": [
|
|
1022
|
+
{
|
|
1023
|
+
"code": "invalid_type",
|
|
1024
|
+
"expected": "string",
|
|
1025
|
+
"received": "number",
|
|
1026
|
+
"path": ["startDate"],
|
|
1027
|
+
"message": "Expected string, received number"
|
|
1028
|
+
}
|
|
1029
|
+
]
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
---
|
|
1036
|
+
|
|
1037
|
+
## License
|
|
1038
|
+
|
|
1039
|
+
Apache-2.0
|