@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.
Files changed (41) hide show
  1. package/README.md +64 -907
  2. package/dist/adapters/node.d.ts +1 -1
  3. package/dist/adapters/node.d.ts.map +1 -1
  4. package/dist/adapters/node.js +114 -21
  5. package/dist/auth.d.ts +27 -17
  6. package/dist/auth.d.ts.map +1 -1
  7. package/dist/auth.js +27 -17
  8. package/dist/cors.d.ts +17 -0
  9. package/dist/cors.d.ts.map +1 -0
  10. package/dist/cors.js +82 -0
  11. package/dist/dev.js +1 -1
  12. package/dist/errors.d.ts +24 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +22 -0
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +4 -0
  18. package/dist/pipeline.d.ts +8 -1
  19. package/dist/pipeline.d.ts.map +1 -1
  20. package/dist/pipeline.js +36 -7
  21. package/dist/rate-limit.d.ts +86 -0
  22. package/dist/rate-limit.d.ts.map +1 -0
  23. package/dist/rate-limit.js +137 -0
  24. package/dist/serve.d.ts +16 -0
  25. package/dist/serve.d.ts.map +1 -0
  26. package/dist/serve.js +88 -0
  27. package/dist/server/builder.d.ts +1 -1
  28. package/dist/server/builder.d.ts.map +1 -1
  29. package/dist/server/builder.js +1 -0
  30. package/dist/server/define-serve.d.ts.map +1 -1
  31. package/dist/server/define-serve.js +3 -0
  32. package/dist/server/execute-query.d.ts.map +1 -1
  33. package/dist/server/execute-query.js +6 -1
  34. package/dist/server/init-serve.d.ts.map +1 -1
  35. package/dist/server/init-serve.js +23 -8
  36. package/dist/type-tests/builder.test-d.d.ts +8 -2
  37. package/dist/type-tests/builder.test-d.d.ts.map +1 -1
  38. package/dist/type-tests/builder.test-d.js +17 -1
  39. package/dist/types.d.ts +102 -5
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +10 -2
package/README.md CHANGED
@@ -1,957 +1,114 @@
1
1
  # @hypequery/serve
2
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.
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 dependencies: `zod@^3`, `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
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 { define, queries, query } = initServe({
22
+ const { query, serve } = initServe({
22
23
  context: () => ({ db }),
24
+ basePath: '/api/analytics',
23
25
  });
24
26
 
25
- export const api = define({
26
- queries: queries({
27
- weeklyRevenue: query
28
- .describe('Calculate weekly revenue')
29
- .input(z.object({ startDate: z.string() }))
30
- .query(({ ctx, input }) =>
31
- ctx.db
32
- .table('sales')
33
- .select(['total_amount'])
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
- // Create queries with inferred types
124
- export const api = define({
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
- ### Express.js
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
- ```ts
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
- const app = express();
855
- const analyticsHandler = createNodeHandler(api.handler);
54
+ ### `query({ ... })`
856
55
 
857
- app.use('/api/analytics', analyticsHandler);
858
- app.listen(3000);
859
- ```
860
-
861
- ---
56
+ Defines a typed contract:
862
57
 
863
- ### Next.js App Router
58
+ - description
59
+ - optional input schema
60
+ - query implementation
864
61
 
865
- ```ts
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
- const handler = createFetchHandler(api.handler);
64
+ Builds a runtime around those contracts:
871
65
 
872
- export { handler as GET, handler as POST, handler as PUT, handler as DELETE };
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
- import { initServe } from '@hypequery/serve';
883
- import { z } from 'zod';
884
-
885
- const { define, queries, query } = initServe({
886
- context: async ({ auth }) => ({
887
- db: createDatabase(),
888
- userId: auth?.userId,
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 = define({
893
- queries: 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
- // Execute with type safety (aliases: api.execute, api.client)
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
- ## Error Handling
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
- All errors follow a consistent format:
99
+ If you need framework-specific integration, see the docs for:
920
100
 
921
- ```ts
922
- interface ErrorEnvelope {
923
- error: {
924
- type: 'VALIDATION_ERROR' | 'UNAUTHORIZED' | 'NOT_FOUND' | 'INTERNAL_SERVER_ERROR';
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
- **Example Error Response:**
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.