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