@hypequery/serve 0.2.0 → 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 (2) hide show
  1. package/README.md +53 -931
  2. package/package.json +2 -2
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,76 @@ export const api = serve({
45
39
  queries: { weeklyRevenue },
46
40
  });
47
41
 
48
- // Register an HTTP route
49
- api.route('/weeklyRevenue', api.queries.weeklyRevenue);
50
- ```
51
-
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>[];
90
-
91
- // Global middlewares (run before every endpoint)
92
- middlewares?: ServeMiddleware<any, any, TContext, TAuth>[];
93
-
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:**
109
-
110
- ```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:
118
-
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();
129
- },
130
- });
131
- ```
132
-
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
- });
164
-
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
- },
172
- });
173
-
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
- },
184
- });
185
-
186
- export const api = serve({
187
- queries: { getUser, weeklyRevenue },
188
- });
189
-
190
- api.route('/users/:id', api.queries.getUser, { method: 'GET' });
191
42
  api.route('/weeklyRevenue', api.queries.weeklyRevenue);
192
43
  ```
193
44
 
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,
236
- });
237
- ```
238
-
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)`
273
-
274
- Creates an authentication strategy that validates API keys from headers or query parameters.
275
-
276
- **Parameters:**
277
-
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
- ```
285
-
286
- **Example:**
287
-
288
- ```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(),
315
- });
316
-
317
- const api = serve({
318
- queries: { revenue },
319
- });
320
- ```
321
-
322
- **Usage:**
323
-
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)`
335
-
336
- Creates an authentication strategy that validates Bearer tokens (JWT, OAuth).
337
-
338
- **Parameters:**
339
-
340
- ```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(),
379
- });
380
-
381
- const api = serve({
382
- queries: { revenue },
383
- });
384
- ```
385
-
386
- **Usage:**
387
-
388
- ```bash
389
- curl -H "Authorization: Bearer eyJhbGc..." http://localhost:3000/revenue
390
- ```
391
-
392
- ---
393
-
394
- ### 4. Query Definition Options
395
-
396
- `query({ ... })` accepts query logic plus optional metadata used for validation, docs, caching, and routing.
397
-
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:**
416
-
417
- ```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
- ---
45
+ Now you can:
442
46
 
443
- ### 5. Multi-Tenancy
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
444
51
 
445
- Hypequery supports multi-tenant applications with automatic tenant isolation.
52
+ ## Main Ideas
446
53
 
447
- **Configuration:**
54
+ ### `query({ ... })`
448
55
 
449
- ```ts
450
- interface TenantConfig<TAuth> {
451
- // Extract tenant ID from auth context
452
- extract: (auth: TAuth) => string | null;
56
+ Defines a typed contract:
453
57
 
454
- // Tenant isolation mode
455
- mode?: 'manual' | 'auto-inject'; // Default: 'manual'
58
+ - description
59
+ - optional input schema
60
+ - query implementation
456
61
 
457
- // Column name for tenant filtering (required for auto-inject mode)
458
- column?: string;
62
+ ### `serve({ queries })`
459
63
 
460
- // Is tenant required? (default: true)
461
- required?: boolean;
64
+ Builds a runtime around those contracts:
462
65
 
463
- // Custom error message when tenant is missing
464
- errorMessage?: string;
465
- }
466
- ```
66
+ - direct execution
67
+ - route registration
68
+ - docs and OpenAPI support
69
+ - hooks, auth, and tenancy features when needed
467
70
 
468
- **Example (Manual Mode):**
71
+ ## Common Example
469
72
 
470
73
  ```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,
480
- }),
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
- tenant: {
503
- extract: (auth) => auth?.organizationId ?? null,
504
- 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
- },
521
- });
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
- ```
662
-
663
- **Example:**
664
-
665
- ```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
- };
682
-
683
- const { query, serve } = initServe({
684
- context: () => ({ db: createDatabase() }),
685
- middlewares: [logMiddleware],
686
- });
687
-
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: { /* ... */ },
776
- });
777
- ```
778
-
779
- ---
780
-
781
- ### OpenAPI Configuration
782
-
783
- **Options:**
784
-
785
- ```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:**
802
-
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: { /* ... */ },
818
- });
819
- ```
820
-
821
- ---
822
-
823
- ### Documentation UI
824
-
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: { /* ... */ },
844
- });
845
- ```
846
-
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
912
-
913
- All functions are fully typed with automatic inference:
914
-
915
- ```ts
916
- import { initServe } from '@hypequery/serve';
917
- import { z } from 'zod';
918
-
919
- const { query, serve } = initServe({
920
- context: async ({ auth }) => ({
921
- db: createDatabase(),
922
- userId: auth?.userId,
923
- }),
924
- });
925
-
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
- },
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(),
938
86
  });
939
87
 
940
88
  export const api = serve({
941
- queries: { getUser },
89
+ queries: { topCustomers },
942
90
  });
943
91
 
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 }
92
+ api.route('/topCustomers', api.queries.topCustomers);
948
93
  ```
949
94
 
950
- ---
95
+ ## Adapters And Runtimes
951
96
 
952
- ## 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.
953
98
 
954
- All errors follow a consistent format:
99
+ If you need framework-specific integration, see the docs for:
955
100
 
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
- ```
101
+ - Node handlers
102
+ - Fetch handlers
103
+ - OpenAPI generation
104
+ - auth and middleware
965
105
 
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
- ```
106
+ ## Docs
987
107
 
988
- ---
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)
989
111
 
990
112
  ## License
991
113
 
992
- Apache-2.0
114
+ Apache-2.0.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/serve",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Declarative HTTP server for exposing hypequery analytics endpoints",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "dist"
18
18
  ],
19
19
  "dependencies": {
20
- "openapi-typescript": "^7.4.2",
20
+ "openapi-typescript": "^7.13.0",
21
21
  "zod": "^3.23.8",
22
22
  "zod-to-json-schema": "^3.23.5"
23
23
  },