@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.
- package/README.md +138 -879
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +3 -5
- package/dist/adapters/standalone.d.ts +41 -0
- package/dist/adapters/standalone.d.ts.map +1 -0
- package/dist/adapters/standalone.js +46 -0
- package/dist/auth.d.ts +59 -83
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +136 -102
- package/dist/client-config.d.ts +3 -2
- package/dist/client-config.d.ts.map +1 -1
- package/dist/client-config.js +4 -2
- package/dist/errors.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/openapi.js +1 -2
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +10 -22
- package/dist/query-logger.js +1 -3
- package/dist/rate-limit.js +4 -3
- package/dist/router.js +2 -1
- package/dist/semantic/datasets/dataset-endpoint.d.ts +85 -0
- package/dist/semantic/datasets/dataset-endpoint.d.ts.map +1 -0
- package/dist/semantic/datasets/dataset-endpoint.js +121 -0
- package/dist/semantic/datasets/index.d.ts +6 -0
- package/dist/semantic/datasets/index.d.ts.map +1 -0
- package/dist/semantic/datasets/index.js +5 -0
- package/dist/semantic/datasets/metric-endpoint.d.ts +82 -0
- package/dist/semantic/datasets/metric-endpoint.d.ts.map +1 -0
- package/dist/semantic/datasets/metric-endpoint.js +159 -0
- package/dist/semantic/datasets/utils/dataset-entry.d.ts +24 -0
- package/dist/semantic/datasets/utils/dataset-entry.d.ts.map +1 -0
- package/dist/semantic/datasets/utils/dataset-entry.js +15 -0
- package/dist/semantic/datasets/utils/dataset-query-metadata.d.ts +3 -0
- package/dist/semantic/datasets/utils/dataset-query-metadata.d.ts.map +1 -0
- package/dist/semantic/datasets/utils/dataset-query-metadata.js +12 -0
- package/dist/semantic/datasets/utils/semantic-input-schema.d.ts +107 -0
- package/dist/semantic/datasets/utils/semantic-input-schema.d.ts.map +1 -0
- package/dist/semantic/datasets/utils/semantic-input-schema.js +87 -0
- package/dist/semantic/index.d.ts +2 -0
- package/dist/semantic/index.d.ts.map +1 -0
- package/dist/semantic/index.js +1 -0
- package/dist/semantic/query-builder-context.d.ts +20 -0
- package/dist/semantic/query-builder-context.d.ts.map +1 -0
- package/dist/semantic/query-builder-context.js +66 -0
- package/dist/semantic/utils/tenant-runtime.d.ts +11 -0
- package/dist/semantic/utils/tenant-runtime.d.ts.map +1 -0
- package/dist/semantic/utils/tenant-runtime.js +48 -0
- package/dist/serve.d.ts +2 -2
- package/dist/serve.d.ts.map +1 -1
- package/dist/server/api-builder.d.ts +5 -0
- package/dist/server/api-builder.d.ts.map +1 -0
- package/dist/server/api-builder.js +76 -0
- package/dist/server/builder.d.ts.map +1 -1
- package/dist/server/builder.js +11 -1
- package/dist/server/create-api.d.ts +32 -0
- package/dist/server/create-api.d.ts.map +1 -0
- package/dist/server/create-api.js +211 -0
- package/dist/server/define-serve.d.ts +21 -2
- package/dist/server/define-serve.d.ts.map +1 -1
- package/dist/server/define-serve.js +53 -84
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/init-serve.d.ts +1 -1
- package/dist/server/init-serve.d.ts.map +1 -1
- package/dist/server/init-serve.js +7 -2
- package/dist/type-tests/builder.test-d.d.ts +4 -0
- package/dist/type-tests/builder.test-d.d.ts.map +1 -1
- package/dist/type-tests/builder.test-d.js +16 -1
- package/dist/type-tests/semantic.test-d.d.ts +2 -0
- package/dist/type-tests/semantic.test-d.d.ts.map +1 -0
- package/dist/type-tests/semantic.test-d.js +59 -0
- package/dist/types.d.ts +227 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
# @hypequery/serve
|
|
2
2
|
|
|
3
|
-
Code-first runtime for
|
|
3
|
+
Code-first runtime for turning hypequery queries into reusable contracts, direct execution helpers, and HTTP routes.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use it when a local query should become something the rest of your app can call consistently.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
10
|
npm install @hypequery/serve zod
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
`tsx` is an optional peer dependency used by the local dev workflow.
|
|
12
14
|
|
|
13
15
|
## Quick Start
|
|
14
16
|
|
|
15
|
-
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('
|
|
38
|
-
.
|
|
39
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
166
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
187
|
-
|
|
81
|
+
await api.execute('revenue', {
|
|
82
|
+
input: { dimensions: ['country'] },
|
|
188
83
|
});
|
|
189
84
|
|
|
190
|
-
api.
|
|
191
|
-
|
|
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
|
-
|
|
92
|
+
### `query({ ... })`
|
|
275
93
|
|
|
276
|
-
|
|
94
|
+
Defines a typed contract:
|
|
277
95
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
100
|
+
Standalone queries can execute without creating a served API:
|
|
287
101
|
|
|
288
102
|
```ts
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
318
|
-
|
|
115
|
+
await topCustomers.execute({
|
|
116
|
+
input: { limit: 10 },
|
|
319
117
|
});
|
|
320
118
|
```
|
|
321
119
|
|
|
322
|
-
|
|
120
|
+
### `serve({ queries, metrics, datasets })`
|
|
323
121
|
|
|
324
|
-
|
|
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
|
-
|
|
124
|
+
- direct execution
|
|
125
|
+
- route registration
|
|
126
|
+
- docs and OpenAPI support
|
|
127
|
+
- hooks, auth, and tenancy features when needed
|
|
337
128
|
|
|
338
|
-
|
|
129
|
+
## Common Example
|
|
339
130
|
|
|
340
131
|
```ts
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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: {
|
|
146
|
+
export const api = serve({
|
|
147
|
+
queries: { topCustomers },
|
|
383
148
|
});
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
**Usage:**
|
|
387
149
|
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
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
|
-
|
|
181
|
+
For cross-origin embedding, verify JWT bearer tokens with a shared secret or a
|
|
182
|
+
provider JWKS:
|
|
664
183
|
|
|
665
184
|
```ts
|
|
666
|
-
|
|
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
|
|
684
|
-
|
|
685
|
-
|
|
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
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
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
|
-
|
|
916
|
-
import { initServe } from '@hypequery/serve';
|
|
917
|
-
import { z } from 'zod';
|
|
232
|
+
## Adapters And Runtimes
|
|
918
233
|
|
|
919
|
-
|
|
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
|
-
|
|
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
|
-
|
|
941
|
-
|
|
942
|
-
|
|
238
|
+
- Node handlers
|
|
239
|
+
- Fetch handlers
|
|
240
|
+
- OpenAPI generation
|
|
241
|
+
- auth and middleware
|
|
943
242
|
|
|
944
|
-
|
|
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.
|