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