@hypequery/serve 0.0.8 → 0.1.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.
- package/README.md +150 -232
- package/dist/adapters/fetch.d.ts +3 -0
- package/dist/adapters/fetch.d.ts.map +1 -0
- package/dist/adapters/fetch.js +26 -0
- package/dist/adapters/node.d.ts +8 -0
- package/dist/adapters/node.d.ts.map +1 -0
- package/dist/adapters/node.js +105 -0
- package/dist/adapters/utils.d.ts +39 -0
- package/dist/adapters/utils.d.ts.map +1 -0
- package/dist/adapters/utils.js +114 -0
- package/dist/adapters/vercel.d.ts +7 -0
- package/dist/adapters/vercel.d.ts.map +1 -0
- package/dist/adapters/vercel.js +13 -0
- package/dist/auth.d.ts +192 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +221 -0
- package/dist/builder.d.ts +3 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +61 -0
- package/dist/client-config.d.ts +44 -0
- package/dist/client-config.d.ts.map +1 -0
- package/dist/client-config.js +53 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +30 -0
- package/dist/docs-ui.d.ts +3 -0
- package/dist/docs-ui.d.ts.map +1 -0
- package/dist/docs-ui.js +34 -0
- package/dist/endpoint.d.ts +5 -0
- package/dist/endpoint.d.ts.map +1 -0
- package/dist/endpoint.js +65 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/openapi.d.ts +3 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +205 -0
- package/dist/pipeline.d.ts +77 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +438 -0
- package/dist/query-logger.d.ts +65 -0
- package/dist/query-logger.d.ts.map +1 -0
- package/dist/query-logger.js +91 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +56 -0
- package/dist/server/builder.d.ts +7 -0
- package/dist/server/builder.d.ts.map +1 -0
- package/dist/server/builder.js +80 -0
- package/dist/server/define-serve.d.ts +3 -0
- package/dist/server/define-serve.d.ts.map +1 -0
- package/dist/server/define-serve.js +88 -0
- package/dist/server/execute-query.d.ts +8 -0
- package/dist/server/execute-query.d.ts.map +1 -0
- package/dist/server/execute-query.js +39 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +5 -0
- package/dist/server/init-serve.d.ts +8 -0
- package/dist/server/init-serve.d.ts.map +1 -0
- package/dist/server/init-serve.js +18 -0
- package/dist/server/mapper.d.ts +3 -0
- package/dist/server/mapper.d.ts.map +1 -0
- package/dist/server/mapper.js +30 -0
- package/dist/tenant.d.ts +35 -0
- package/dist/tenant.d.ts.map +1 -0
- package/dist/tenant.js +49 -0
- package/dist/type-tests/builder.test-d.d.ts +13 -0
- package/dist/type-tests/builder.test-d.d.ts.map +1 -0
- package/dist/type-tests/builder.test-d.js +20 -0
- package/dist/types.d.ts +529 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,37 +14,38 @@ Peer dependencies: `zod@^3`, `tsx@^4` (optional, for dev server)
|
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
// analytics/server.ts
|
|
17
|
-
import {
|
|
17
|
+
import { initServe } from '@hypequery/serve';
|
|
18
18
|
import { z } from 'zod';
|
|
19
|
-
import { db } from './
|
|
19
|
+
import { db } from './client';
|
|
20
20
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
weeklyRevenue: {
|
|
24
|
-
inputSchema: z.object({ startDate: z.string() }),
|
|
25
|
-
outputSchema: z.object({ total: z.number() }),
|
|
26
|
-
query: async ({ input, ctx }) => {
|
|
27
|
-
const result = await db
|
|
28
|
-
.select({ total: sum(sales.amount) })
|
|
29
|
-
.from(sales)
|
|
30
|
-
.where(gte(sales.date, input.startDate));
|
|
31
|
-
return { total: result[0].total };
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
},
|
|
21
|
+
const { define, queries, query } = initServe({
|
|
22
|
+
context: () => ({ db }),
|
|
35
23
|
});
|
|
36
24
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
});
|
|
39
40
|
|
|
40
|
-
//
|
|
41
|
-
|
|
41
|
+
// Expose as HTTP endpoint
|
|
42
|
+
api.route('/weeklyRevenue', api.queries.weeklyRevenue);
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
Your API is now running with:
|
|
45
|
-
- **Endpoint**: `
|
|
46
|
-
- **Docs**: `http://localhost:
|
|
47
|
-
- **OpenAPI**: `http://localhost:
|
|
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)
|
|
48
49
|
|
|
49
50
|
---
|
|
50
51
|
|
|
@@ -52,24 +53,21 @@ Your API is now running with:
|
|
|
52
53
|
|
|
53
54
|
### 1. Server Builders
|
|
54
55
|
|
|
55
|
-
#### `
|
|
56
|
+
#### `initServe<TContext, TAuth>(options)`
|
|
56
57
|
|
|
57
|
-
Main entry point for creating a hypequery server. Returns
|
|
58
|
+
Main entry point for creating a hypequery server. Returns an initializer with type-safe query builders and context inference.
|
|
58
59
|
|
|
59
60
|
**Parameters:**
|
|
60
61
|
|
|
61
62
|
```ts
|
|
62
|
-
interface
|
|
63
|
-
//
|
|
64
|
-
|
|
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>);
|
|
65
66
|
|
|
66
67
|
// Base path for all routes
|
|
67
68
|
basePath?: string;
|
|
68
69
|
|
|
69
|
-
//
|
|
70
|
-
context?: TContext | ((opts: { request: ServeRequest; auth: TAuth | null }) => TContext | Promise<TContext>);
|
|
71
|
-
|
|
72
|
-
// Authentication strategies
|
|
70
|
+
// Authentication strategy
|
|
73
71
|
auth?: AuthStrategy<TAuth> | AuthStrategy<TAuth>[];
|
|
74
72
|
|
|
75
73
|
// Global middlewares (run before every endpoint)
|
|
@@ -91,102 +89,12 @@ interface ServeConfig<TContext, TAuth, TQueries> {
|
|
|
91
89
|
|
|
92
90
|
**Returns:**
|
|
93
91
|
|
|
94
|
-
```ts
|
|
95
|
-
interface ServeBuilder<TQueries, TContext, TAuth> {
|
|
96
|
-
queries: TQueries; // Registered query definitions
|
|
97
|
-
_routeConfig: Record<string, { method: HttpMethod }>; // Route-level HTTP method overrides
|
|
98
|
-
|
|
99
|
-
// Register a route
|
|
100
|
-
route(path: string, endpoint: ServeEndpoint, options?: RouteOptions): ServeBuilder;
|
|
101
|
-
|
|
102
|
-
// Add global middleware
|
|
103
|
-
use(middleware: ServeMiddleware): ServeBuilder;
|
|
104
|
-
|
|
105
|
-
// Add authentication strategy
|
|
106
|
-
useAuth(strategy: AuthStrategy<TAuth>): ServeBuilder;
|
|
107
|
-
|
|
108
|
-
// Execute query directly (bypasses HTTP)
|
|
109
|
-
execute<K extends keyof TQueries>(
|
|
110
|
-
key: K,
|
|
111
|
-
options?: { input?: any; context?: Partial<TContext>; request?: Partial<ServeRequest> }
|
|
112
|
-
): Promise<QueryResult<TQueries[K]>>;
|
|
113
|
-
|
|
114
|
-
// Get toolkit description (for LLM integration)
|
|
115
|
-
describe(): ToolkitDescription;
|
|
116
|
-
|
|
117
|
-
// Raw HTTP handler (for custom adapters)
|
|
118
|
-
handler: ServeHandler;
|
|
119
|
-
|
|
120
|
-
// Start Node.js HTTP server
|
|
121
|
-
start(options?: StartServerOptions): Promise<{ server: Server; stop: () => Promise<void> }>;
|
|
122
|
-
}
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
**Example:**
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
const api = defineServe({
|
|
129
|
-
basePath: '/api',
|
|
130
|
-
context: async ({ request, auth }) => ({
|
|
131
|
-
db: createDatabaseConnection(),
|
|
132
|
-
userId: auth?.userId,
|
|
133
|
-
}),
|
|
134
|
-
auth: createBearerTokenStrategy({
|
|
135
|
-
validate: async (token) => {
|
|
136
|
-
const user = await verifyJWT(token);
|
|
137
|
-
return user ? { userId: user.id, role: user.role } : null;
|
|
138
|
-
},
|
|
139
|
-
}),
|
|
140
|
-
queries: {
|
|
141
|
-
getUser: {
|
|
142
|
-
inputSchema: z.object({ id: z.string() }),
|
|
143
|
-
outputSchema: z.object({ name: z.string(), email: z.string() }),
|
|
144
|
-
query: async ({ input, ctx }) => {
|
|
145
|
-
return ctx.db.query.users.findFirst({
|
|
146
|
-
where: eq(users.id, input.id),
|
|
147
|
-
});
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
api.route('/users/:id', api.queries.getUser, { method: 'GET' });
|
|
154
|
-
|
|
155
|
-
await api.start({ port: 4000 });
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
---
|
|
159
|
-
|
|
160
|
-
#### `initServe<TContext, TAuth>(options)`
|
|
161
|
-
|
|
162
|
-
Advanced pattern for defining reusable query builders with context type inference. Use this when you want to define context once and create multiple queries that share the same types.
|
|
163
|
-
|
|
164
|
-
**Parameters:**
|
|
165
|
-
|
|
166
|
-
```ts
|
|
167
|
-
interface ServeInitializerOptions<TContext, TAuth> {
|
|
168
|
-
context: TContext | ((opts: { request: ServeRequest; auth: TAuth | null }) => TContext | Promise<TContext>);
|
|
169
|
-
basePath?: string;
|
|
170
|
-
auth?: AuthStrategy<TAuth> | AuthStrategy<TAuth>[];
|
|
171
|
-
middlewares?: ServeMiddleware<any, any, TContext, TAuth>[];
|
|
172
|
-
tenant?: TenantConfig<TAuth>;
|
|
173
|
-
hooks?: ServeLifecycleHooks<TAuth>;
|
|
174
|
-
openapi?: OpenApiOptions;
|
|
175
|
-
docs?: DocsOptions;
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**Returns:**
|
|
180
|
-
|
|
181
92
|
```ts
|
|
182
93
|
interface ServeInitializer<TContext, TAuth> {
|
|
183
|
-
//
|
|
184
|
-
procedure: QueryProcedureBuilder<TContext, TAuth>;
|
|
185
|
-
|
|
186
|
-
// Alias for procedure
|
|
94
|
+
// Query builder (chainable query configuration)
|
|
187
95
|
query: QueryProcedureBuilder<TContext, TAuth>;
|
|
188
96
|
|
|
189
|
-
// Helper to group queries
|
|
97
|
+
// Helper to group multiple queries
|
|
190
98
|
queries<TQueries>(definitions: TQueries): TQueries;
|
|
191
99
|
|
|
192
100
|
// Define server with queries
|
|
@@ -198,51 +106,47 @@ interface ServeInitializer<TContext, TAuth> {
|
|
|
198
106
|
|
|
199
107
|
```ts
|
|
200
108
|
// Define context once
|
|
201
|
-
const
|
|
109
|
+
const { define, queries, query } = initServe({
|
|
110
|
+
basePath: '/api',
|
|
202
111
|
context: async ({ auth }) => ({
|
|
203
112
|
db: createDatabase(),
|
|
204
113
|
userId: auth?.userId,
|
|
205
114
|
}),
|
|
206
|
-
auth:
|
|
207
|
-
validate: async (
|
|
208
|
-
const user = await
|
|
209
|
-
return user ? { userId: user.id } : null;
|
|
115
|
+
auth: createBearerTokenStrategy({
|
|
116
|
+
validate: async (token) => {
|
|
117
|
+
const user = await verifyJWT(token);
|
|
118
|
+
return user ? { userId: user.id, role: user.role } : null;
|
|
210
119
|
},
|
|
211
120
|
}),
|
|
212
121
|
});
|
|
213
122
|
|
|
214
123
|
// Create queries with inferred types
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
+
}),
|
|
236
145
|
});
|
|
237
146
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
api
|
|
242
|
-
.route('/revenue', api.queries.getRevenue)
|
|
243
|
-
.route('/sales', api.queries.createSale);
|
|
244
|
-
|
|
245
|
-
await api.start();
|
|
147
|
+
// Expose as HTTP endpoints
|
|
148
|
+
api.route('/users/:id', api.queries.getUser, { method: 'GET' });
|
|
149
|
+
api.route('/weeklyRevenue', api.queries.weeklyRevenue);
|
|
246
150
|
```
|
|
247
151
|
|
|
248
152
|
---
|
|
@@ -429,29 +333,23 @@ curl -H "Authorization: Bearer eyJhbGc..." http://localhost:3000/revenue
|
|
|
429
333
|
|
|
430
334
|
---
|
|
431
335
|
|
|
432
|
-
### 4.
|
|
336
|
+
### 4. Query Builder (Advanced Query Configuration)
|
|
433
337
|
|
|
434
|
-
The
|
|
338
|
+
The query builder provides a chainable API for configuring queries with full type inference.
|
|
435
339
|
|
|
436
340
|
**Available Methods:**
|
|
437
341
|
|
|
438
342
|
```ts
|
|
439
343
|
interface QueryProcedureBuilder<TContext, TAuth> {
|
|
344
|
+
// Description (OpenAPI documentation, supports Markdown)
|
|
345
|
+
describe(description: string): QueryProcedureBuilder;
|
|
346
|
+
|
|
440
347
|
// Input schema (Zod)
|
|
441
348
|
input<TSchema extends ZodTypeAny>(schema: TSchema): QueryProcedureBuilder;
|
|
442
349
|
|
|
443
|
-
// Output schema (Zod)
|
|
444
|
-
output<TSchema extends ZodTypeAny>(schema: TSchema): QueryProcedureBuilder;
|
|
445
|
-
|
|
446
350
|
// HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
447
351
|
method(method: HttpMethod): QueryProcedureBuilder;
|
|
448
352
|
|
|
449
|
-
// Summary (OpenAPI short description)
|
|
450
|
-
summary(summary: string): QueryProcedureBuilder;
|
|
451
|
-
|
|
452
|
-
// Description (OpenAPI detailed description, supports Markdown)
|
|
453
|
-
describe(description: string): QueryProcedureBuilder;
|
|
454
|
-
|
|
455
353
|
// Add single tag (for OpenAPI grouping)
|
|
456
354
|
tag(tag: string): QueryProcedureBuilder;
|
|
457
355
|
|
|
@@ -465,7 +363,9 @@ interface QueryProcedureBuilder<TContext, TAuth> {
|
|
|
465
363
|
auth(strategy: AuthStrategy<TAuth>): QueryProcedureBuilder;
|
|
466
364
|
|
|
467
365
|
// Multi-tenancy configuration
|
|
468
|
-
tenant(config: TenantConfig<TAuth
|
|
366
|
+
tenant(config: Partial<TenantConfig<TAuth>>): QueryProcedureBuilder;
|
|
367
|
+
tenantOptional(config?: Partial<TenantConfig<TAuth>>): QueryProcedureBuilder;
|
|
368
|
+
require(): QueryProcedureBuilder;
|
|
469
369
|
|
|
470
370
|
// Custom metadata (for extensions)
|
|
471
371
|
custom(metadata: Record<string, unknown>): QueryProcedureBuilder;
|
|
@@ -483,41 +383,31 @@ interface QueryProcedureBuilder<TContext, TAuth> {
|
|
|
483
383
|
**Example:**
|
|
484
384
|
|
|
485
385
|
```ts
|
|
486
|
-
const
|
|
386
|
+
const { query } = initServe({
|
|
487
387
|
context: async () => ({ db: createDatabase() }),
|
|
488
388
|
});
|
|
489
389
|
|
|
490
|
-
const getAnalytics =
|
|
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
|
+
`)
|
|
491
395
|
.input(z.object({
|
|
492
396
|
startDate: z.string(),
|
|
493
397
|
endDate: z.string(),
|
|
494
398
|
metric: z.enum(['revenue', 'users', 'sessions']),
|
|
495
399
|
}))
|
|
496
|
-
.output(z.object({
|
|
497
|
-
total: z.number(),
|
|
498
|
-
trend: z.number(),
|
|
499
|
-
}))
|
|
500
|
-
.method('GET')
|
|
501
|
-
.summary('Fetch analytics data')
|
|
502
|
-
.describe(`
|
|
503
|
-
Returns aggregated analytics for the specified metric and date range.
|
|
504
|
-
Supports revenue, user count, and session metrics.
|
|
505
|
-
`)
|
|
506
400
|
.tag('Analytics')
|
|
507
401
|
.cache(300000) // Cache for 5 minutes
|
|
508
|
-
.query(async ({
|
|
509
|
-
const result = await ctx.db
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
)
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
return {
|
|
518
|
-
total: result.total,
|
|
519
|
-
trend: result.trend,
|
|
520
|
-
};
|
|
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];
|
|
521
411
|
});
|
|
522
412
|
```
|
|
523
413
|
|
|
@@ -551,7 +441,7 @@ interface TenantConfig<TAuth> {
|
|
|
551
441
|
**Example (Manual Mode):**
|
|
552
442
|
|
|
553
443
|
```ts
|
|
554
|
-
const
|
|
444
|
+
const { define, queries, query } = initServe({
|
|
555
445
|
tenant: {
|
|
556
446
|
extract: (auth) => auth?.tenantId ?? null,
|
|
557
447
|
mode: 'manual', // You manually filter by tenantId
|
|
@@ -559,25 +449,28 @@ const api = defineServe({
|
|
|
559
449
|
},
|
|
560
450
|
context: async ({ auth }) => ({
|
|
561
451
|
db: createDatabase(),
|
|
562
|
-
tenantId: auth?.tenantId,
|
|
452
|
+
tenantId: auth?.tenantId,
|
|
563
453
|
}),
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
export const api = define({
|
|
457
|
+
queries: queries({
|
|
458
|
+
getUsers: query
|
|
459
|
+
.query(async ({ ctx }) => {
|
|
567
460
|
// Manually filter by tenant
|
|
568
|
-
return ctx.db
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
},
|
|
461
|
+
return ctx.db
|
|
462
|
+
.table('users')
|
|
463
|
+
.where('tenant_id', 'eq', ctx.tenantId)
|
|
464
|
+
.execute();
|
|
465
|
+
}),
|
|
466
|
+
}),
|
|
574
467
|
});
|
|
575
468
|
```
|
|
576
469
|
|
|
577
470
|
**Example (Auto-Inject Mode):**
|
|
578
471
|
|
|
579
472
|
```ts
|
|
580
|
-
const
|
|
473
|
+
const { define, queries, query } = initServe({
|
|
581
474
|
tenant: {
|
|
582
475
|
extract: (auth) => auth?.organizationId ?? null,
|
|
583
476
|
mode: 'auto-inject',
|
|
@@ -586,15 +479,37 @@ const api = defineServe({
|
|
|
586
479
|
context: async () => ({
|
|
587
480
|
db: createDatabase(),
|
|
588
481
|
}),
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
export const api = define({
|
|
485
|
+
queries: queries({
|
|
486
|
+
getUsers: query
|
|
487
|
+
.query(async ({ ctx }) => {
|
|
592
488
|
// Tenant filter is automatically injected
|
|
593
|
-
return ctx.db
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
+
}),
|
|
598
513
|
});
|
|
599
514
|
```
|
|
600
515
|
|
|
@@ -964,33 +879,36 @@ export { handler as GET, handler as POST, handler as PUT, handler as DELETE };
|
|
|
964
879
|
All functions are fully typed with automatic inference:
|
|
965
880
|
|
|
966
881
|
```ts
|
|
967
|
-
import {
|
|
882
|
+
import { initServe } from '@hypequery/serve';
|
|
968
883
|
import { z } from 'zod';
|
|
969
884
|
|
|
970
|
-
const
|
|
885
|
+
const { define, queries, query } = initServe({
|
|
971
886
|
context: async ({ auth }) => ({
|
|
972
887
|
db: createDatabase(),
|
|
973
888
|
userId: auth?.userId,
|
|
974
889
|
}),
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
export const api = define({
|
|
893
|
+
queries: queries({
|
|
894
|
+
getUser: query
|
|
895
|
+
.input(z.object({ id: z.string() }))
|
|
896
|
+
.query(async ({ ctx, input }) => {
|
|
980
897
|
// input: { id: string }
|
|
981
898
|
// ctx: { db: Database; userId: string | undefined }
|
|
982
|
-
return ctx.db
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
899
|
+
return ctx.db
|
|
900
|
+
.table('users')
|
|
901
|
+
.where('id', 'eq', input.id)
|
|
902
|
+
.select(['name', 'email'])
|
|
903
|
+
.limit(1)
|
|
904
|
+
.execute();
|
|
905
|
+
}),
|
|
906
|
+
}),
|
|
988
907
|
});
|
|
989
908
|
|
|
990
|
-
// Execute with type safety
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
});
|
|
909
|
+
// Execute with type safety (aliases: api.execute, api.client)
|
|
910
|
+
const result = await api.run('getUser', { id: '123' });
|
|
911
|
+
const user = result[0];
|
|
994
912
|
// user: { name: string; email: string }
|
|
995
913
|
```
|
|
996
914
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/adapters/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAc,YAAY,EAAgB,MAAM,aAAa,CAAC;AAQxF,eAAO,MAAM,kBAAkB,GAAI,SAAS,YAAY,KAAG,YA4B1D,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { normalizeHeaders, parseQueryParams, parseRequestBody, serializeResponseBody, } from "./utils.js";
|
|
2
|
+
export const createFetchHandler = (handler) => {
|
|
3
|
+
return async (request) => {
|
|
4
|
+
const url = new URL(request.url);
|
|
5
|
+
const headers = normalizeHeaders(request.headers);
|
|
6
|
+
const contentType = headers["content-type"];
|
|
7
|
+
const serveRequest = {
|
|
8
|
+
method: (request.method ?? "GET").toUpperCase(),
|
|
9
|
+
path: url.pathname,
|
|
10
|
+
query: parseQueryParams(url.searchParams),
|
|
11
|
+
headers,
|
|
12
|
+
body: await parseRequestBody(request, contentType),
|
|
13
|
+
raw: request,
|
|
14
|
+
};
|
|
15
|
+
const response = await handler(serveRequest);
|
|
16
|
+
const responseHeaders = {
|
|
17
|
+
"content-type": "application/json; charset=utf-8",
|
|
18
|
+
...response.headers,
|
|
19
|
+
};
|
|
20
|
+
const body = serializeResponseBody(response.body);
|
|
21
|
+
return new Response(body, {
|
|
22
|
+
status: response.status,
|
|
23
|
+
headers: responseHeaders,
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from "http";
|
|
2
|
+
import type { ServeHandler, StartServerOptions } from "../types.js";
|
|
3
|
+
export declare const createNodeHandler: (handler: ServeHandler) => (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
4
|
+
export declare const startNodeServer: (handler: ServeHandler, options?: StartServerOptions) => Promise<{
|
|
5
|
+
server: import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
6
|
+
stop: () => Promise<void>;
|
|
7
|
+
}>;
|
|
8
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/adapters/node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,MAAM,CAAC;AAG/E,OAAO,KAAK,EAEV,YAAY,EAGZ,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAyErB,eAAO,MAAM,iBAAiB,GAAI,SAAS,YAAY,MACvC,KAAK,eAAe,EAAE,KAAK,cAAc,kBASxD,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,SAAS,YAAY,EACrB,UAAS,kBAAuB;;;EA8CjC,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createServer } from "http";
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
import { normalizeHeaders, parseQueryParams, parseRequestBody, serializeResponseBody, } from "./utils.js";
|
|
4
|
+
const readRequestBody = async (req) => {
|
|
5
|
+
const chunks = [];
|
|
6
|
+
for await (const chunk of req) {
|
|
7
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
8
|
+
}
|
|
9
|
+
return Buffer.concat(chunks);
|
|
10
|
+
};
|
|
11
|
+
const buildServeRequest = async (req) => {
|
|
12
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
13
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
14
|
+
const bodyBuffer = await readRequestBody(req);
|
|
15
|
+
const headers = normalizeHeaders(req.headers);
|
|
16
|
+
const contentType = headers["content-type"] ?? headers["Content-Type"];
|
|
17
|
+
const body = await parseRequestBody(bodyBuffer, contentType);
|
|
18
|
+
return {
|
|
19
|
+
method,
|
|
20
|
+
path: url.pathname,
|
|
21
|
+
query: parseQueryParams(url.searchParams),
|
|
22
|
+
headers,
|
|
23
|
+
body,
|
|
24
|
+
raw: req,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const sendResponse = (res, response) => {
|
|
28
|
+
res.statusCode = response.status;
|
|
29
|
+
const headers = response.headers ?? {};
|
|
30
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
31
|
+
if (value !== undefined) {
|
|
32
|
+
res.setHeader(key, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!res.hasHeader("content-type")) {
|
|
36
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
37
|
+
}
|
|
38
|
+
const serialized = serializeResponseBody(response.body);
|
|
39
|
+
res.end(serialized);
|
|
40
|
+
};
|
|
41
|
+
const sendError = (res, error) => {
|
|
42
|
+
const payload = error && typeof error === "object" && "status" in error
|
|
43
|
+
? error
|
|
44
|
+
: {
|
|
45
|
+
status: 500,
|
|
46
|
+
body: {
|
|
47
|
+
error: {
|
|
48
|
+
type: "INTERNAL_SERVER_ERROR",
|
|
49
|
+
message: error instanceof Error ? error.message : "Unexpected error",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
sendResponse(res, payload);
|
|
54
|
+
};
|
|
55
|
+
export const createNodeHandler = (handler) => {
|
|
56
|
+
return async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const request = await buildServeRequest(req);
|
|
59
|
+
const response = await handler(request);
|
|
60
|
+
sendResponse(res, response);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
sendError(res, error);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
export const startNodeServer = async (handler, options = {}) => {
|
|
68
|
+
const listener = createNodeHandler(handler);
|
|
69
|
+
const server = createServer(listener);
|
|
70
|
+
const port = options.port ?? 3000;
|
|
71
|
+
const hostname = options.hostname ?? "0.0.0.0";
|
|
72
|
+
const onAbort = () => {
|
|
73
|
+
server.close();
|
|
74
|
+
};
|
|
75
|
+
if (options.signal) {
|
|
76
|
+
if (options.signal.aborted) {
|
|
77
|
+
server.close();
|
|
78
|
+
throw new Error("Start signal already aborted");
|
|
79
|
+
}
|
|
80
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
81
|
+
}
|
|
82
|
+
server.listen(port, hostname);
|
|
83
|
+
await once(server, "listening");
|
|
84
|
+
if (!options.quiet) {
|
|
85
|
+
const address = server.address();
|
|
86
|
+
const display = typeof address === "object" && address
|
|
87
|
+
? `${address.address}:${address.port}`
|
|
88
|
+
: `${hostname}:${port}`;
|
|
89
|
+
console.log(`hypequery serve listening on ${display}`);
|
|
90
|
+
}
|
|
91
|
+
const stop = () => new Promise((resolve, reject) => {
|
|
92
|
+
server.close((err) => {
|
|
93
|
+
if (err) {
|
|
94
|
+
reject(err);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
resolve();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
server,
|
|
103
|
+
stop,
|
|
104
|
+
};
|
|
105
|
+
};
|