@hypequery/serve 0.15.1 → 0.15.2
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 +28 -216
- package/package.json +18 -5
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @hypequery/serve
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Turn TypeScript ClickHouse queries into trusted analytics APIs.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@hypequery/serve` lets one query contract run inside your app, over HTTP, through OpenAPI, in typed React hooks, or as an agent tool. Inputs are validated, outputs stay typed, and authentication and tenant context live at the server boundary.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -10,9 +10,7 @@ Use it when a local query should become something the rest of your app can call
|
|
|
10
10
|
npm install @hypequery/serve zod
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Quick Start
|
|
13
|
+
## Define once
|
|
16
14
|
|
|
17
15
|
```ts
|
|
18
16
|
import { initServe } from '@hypequery/serve';
|
|
@@ -25,245 +23,59 @@ const { query, serve } = initServe({
|
|
|
25
23
|
});
|
|
26
24
|
|
|
27
25
|
const weeklyRevenue = query({
|
|
28
|
-
description: '
|
|
26
|
+
description: 'Weekly revenue since a given date',
|
|
29
27
|
input: z.object({ startDate: z.string() }),
|
|
30
28
|
query: ({ ctx, input }) =>
|
|
31
29
|
ctx.db
|
|
32
30
|
.table('orders')
|
|
33
31
|
.where('created_at', 'gte', input.startDate)
|
|
34
|
-
.sum('
|
|
32
|
+
.sum('amount', 'revenue')
|
|
35
33
|
.execute(),
|
|
36
34
|
});
|
|
37
35
|
|
|
38
|
-
export const api = serve({
|
|
39
|
-
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
api.route('/weeklyRevenue', api.queries.weeklyRevenue);
|
|
36
|
+
export const api = serve({ queries: { weeklyRevenue } });
|
|
37
|
+
api.route('/weekly-revenue', api.queries.weeklyRevenue);
|
|
43
38
|
```
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
The same `api.execute(...)` call works for configured metrics and datasets:
|
|
40
|
+
## Run anywhere
|
|
53
41
|
|
|
54
42
|
```ts
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
import { dataset, dimension, measure } from '@hypequery/datasets';
|
|
58
|
-
|
|
59
|
-
const Orders = dataset('orders', {
|
|
60
|
-
source: 'orders',
|
|
61
|
-
dimensions: {
|
|
62
|
-
country: dimension.string(),
|
|
63
|
-
},
|
|
64
|
-
measures: {
|
|
65
|
-
revenue: measure.sum('amount'),
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const revenue = Orders.metric('revenue', { measure: 'revenue' });
|
|
70
|
-
const queryBuilder = createQueryBuilder({ url, username, password, database });
|
|
71
|
-
|
|
72
|
-
const { serve } = initServe({
|
|
73
|
-
context: () => ({ db: queryBuilder }), // ✅ Pass queryBuilder via context once
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const api = serve({
|
|
77
|
-
metrics: { revenue }, // ✅ Auto-extracts queryBuilder from context
|
|
78
|
-
datasets: { orders: Orders },
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
await api.execute('revenue', {
|
|
82
|
-
input: { dimensions: ['country'] },
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
await api.execute('dataset:orders', {
|
|
86
|
-
input: { dimensions: ['country'], measures: ['revenue'] },
|
|
43
|
+
await api.execute('weeklyRevenue', {
|
|
44
|
+
input: { startDate: '2026-01-01' },
|
|
87
45
|
});
|
|
88
46
|
```
|
|
89
47
|
|
|
90
|
-
|
|
48
|
+
The same contract can power a REST endpoint, generated OpenAPI, a typed `@hypequery/react` hook, or a tool description for an AI agent.
|
|
91
49
|
|
|
92
|
-
|
|
93
|
-
entry with no local strategy, including `auth: null`, still inherits the global
|
|
94
|
-
`auth` configuration. Use `requiresAuth: false` for an explicitly public
|
|
95
|
-
endpoint; `requiredRoles` or `requiredScopes` always require authentication.
|
|
50
|
+
Serve also accepts semantic metrics and datasets directly:
|
|
96
51
|
|
|
97
52
|
```ts
|
|
98
|
-
const api = serve({
|
|
99
|
-
auth: globalAuth,
|
|
100
|
-
metrics: {
|
|
101
|
-
revenue: { metric: revenue, auth: null }, // inherits globalAuth
|
|
102
|
-
},
|
|
103
|
-
datasets: {
|
|
104
|
-
publicOrders: { dataset: Orders, requiresAuth: false },
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Main Ideas
|
|
110
|
-
|
|
111
|
-
### `query({ ... })`
|
|
112
|
-
|
|
113
|
-
Defines a typed contract:
|
|
114
|
-
|
|
115
|
-
- description
|
|
116
|
-
- optional input schema
|
|
117
|
-
- query implementation
|
|
118
|
-
|
|
119
|
-
Standalone queries can execute without creating a served API:
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
const topCustomers = query({
|
|
123
|
-
input: z.object({ limit: z.number().int().positive() }),
|
|
124
|
-
query: async ({ input }) =>
|
|
125
|
-
db
|
|
126
|
-
.table('orders')
|
|
127
|
-
.select(['customer_id'])
|
|
128
|
-
.sum('total', 'revenue')
|
|
129
|
-
.groupBy('customer_id')
|
|
130
|
-
.limit(input.limit)
|
|
131
|
-
.execute(),
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
await topCustomers.execute({
|
|
135
|
-
input: { limit: 10 },
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### `serve({ queries, metrics, datasets })`
|
|
140
|
-
|
|
141
|
-
Builds a runtime around those contracts:
|
|
142
|
-
|
|
143
|
-
- direct execution
|
|
144
|
-
- route registration
|
|
145
|
-
- docs and OpenAPI support
|
|
146
|
-
- hooks, auth, and tenancy features when needed
|
|
147
|
-
|
|
148
|
-
## Common Example
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
const topCustomers = query({
|
|
152
|
-
description: 'Top customers by revenue',
|
|
153
|
-
input: z.object({ limit: z.number().int().positive().default(10) }),
|
|
154
|
-
query: ({ ctx, input }) =>
|
|
155
|
-
ctx.db
|
|
156
|
-
.table('orders')
|
|
157
|
-
.select(['customer_id'])
|
|
158
|
-
.sum('total', 'revenue')
|
|
159
|
-
.groupBy('customer_id')
|
|
160
|
-
.orderBy('revenue', 'DESC')
|
|
161
|
-
.limit(input.limit)
|
|
162
|
-
.execute(),
|
|
163
|
-
});
|
|
164
|
-
|
|
165
53
|
export const api = serve({
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
api.route('/topCustomers', api.queries.topCustomers);
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## Authentication
|
|
173
|
-
|
|
174
|
-
Pass an auth strategy (or array of strategies) to `serve({ auth })` / `createAPI({ auth })`.
|
|
175
|
-
When auth is configured, endpoints require authentication by default. Mark exceptions
|
|
176
|
-
with `query.public()`.
|
|
177
|
-
|
|
178
|
-
For same-app APIs, prefer reading the host app's authenticated request context:
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
import { createAPI, fromContext } from '@hypequery/serve';
|
|
182
|
-
|
|
183
|
-
const api = createAPI({
|
|
184
|
-
queryBuilder: db,
|
|
185
|
-
datasets,
|
|
186
|
-
auth: fromContext(({ request }) => {
|
|
187
|
-
const user = getUserFromRequest(request.raw);
|
|
188
|
-
return user
|
|
189
|
-
? { userId: user.id, tenantId: user.orgId, roles: user.roles }
|
|
190
|
-
: null;
|
|
191
|
-
}),
|
|
192
|
-
tenant: {
|
|
193
|
-
extract: (auth) => auth.tenantId,
|
|
194
|
-
column: 'tenant_id',
|
|
195
|
-
mode: 'auto-inject',
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
For cross-origin embedding, verify JWT bearer tokens with a shared secret or a
|
|
201
|
-
provider JWKS:
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
import { createJwtStrategy } from '@hypequery/serve';
|
|
205
|
-
|
|
206
|
-
const auth = createJwtStrategy({
|
|
207
|
-
// Use `secret` for HS256 tokens you mint yourself.
|
|
208
|
-
secret: process.env.HYPEQUERY_AUTH_SECRET!,
|
|
209
|
-
issuer: 'https://your-app.example.com',
|
|
210
|
-
audience: 'hypequery-analytics',
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
const providerAuth = createJwtStrategy({
|
|
214
|
-
// Use `jwksUri` for Auth0/Clerk/Cognito/etc.
|
|
215
|
-
jwksUri: 'https://example.auth0.com/.well-known/jwks.json',
|
|
216
|
-
issuer: 'https://example.auth0.com/',
|
|
217
|
-
audience: 'https://api.example.com',
|
|
218
|
-
});
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
For signed embedding, mint short-lived analytics tokens server-side:
|
|
222
|
-
|
|
223
|
-
```ts
|
|
224
|
-
import { createAnalyticsTokenIssuer } from '@hypequery/serve';
|
|
225
|
-
|
|
226
|
-
const issueAnalyticsToken = createAnalyticsTokenIssuer({
|
|
227
|
-
secret: process.env.HYPEQUERY_AUTH_SECRET!,
|
|
228
|
-
expiresIn: '15m',
|
|
229
|
-
issuer: 'https://your-app.example.com',
|
|
230
|
-
audience: 'hypequery-analytics',
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
app.get('/api/analytics/token', requireUser, async (req, res) => {
|
|
234
|
-
res.json({
|
|
235
|
-
token: await issueAnalyticsToken({
|
|
236
|
-
userId: req.user.id,
|
|
237
|
-
tenantId: req.user.orgId,
|
|
238
|
-
roles: req.user.roles,
|
|
239
|
-
}),
|
|
240
|
-
});
|
|
54
|
+
metrics: { revenue },
|
|
55
|
+
datasets: { orders: Orders },
|
|
241
56
|
});
|
|
242
57
|
```
|
|
243
58
|
|
|
244
|
-
|
|
245
|
-
authentication systems.
|
|
246
|
-
|
|
247
|
-
> **Rate limiting:** the default `RateLimitStore` is in-memory and therefore
|
|
248
|
-
> per-instance. Behind multiple instances, supply a shared store (e.g. Redis) by
|
|
249
|
-
> implementing the `RateLimitStore` interface.
|
|
250
|
-
|
|
251
|
-
## Adapters And Runtimes
|
|
59
|
+
## Built for real analytics APIs
|
|
252
60
|
|
|
253
|
-
|
|
61
|
+
- zod input and output validation;
|
|
62
|
+
- authentication, roles, scopes, and explicit public routes;
|
|
63
|
+
- trusted multi-tenant context and automatic tenant predicates;
|
|
64
|
+
- OpenAPI and static React route manifests;
|
|
65
|
+
- CORS, rate limiting, request IDs, logging, and cache observability;
|
|
66
|
+
- Node and Fetch adapters for existing application stacks.
|
|
254
67
|
|
|
255
|
-
|
|
68
|
+
## Why it matters
|
|
256
69
|
|
|
257
|
-
|
|
258
|
-
- Fetch handlers
|
|
259
|
-
- OpenAPI generation
|
|
260
|
-
- auth and middleware
|
|
70
|
+
Analytics endpoints usually drift across three copies: the SQL, the API type, and the frontend client. Serve keeps them on one TypeScript contract while leaving deployment and framework choices with your application.
|
|
261
71
|
|
|
262
|
-
##
|
|
72
|
+
## Learn more
|
|
263
73
|
|
|
264
74
|
- [Core concepts](https://hypequery.com/docs/core-concepts)
|
|
265
|
-
- [
|
|
266
|
-
- [
|
|
75
|
+
- [HTTP and OpenAPI](https://hypequery.com/docs/http-openapi)
|
|
76
|
+
- [Authentication](https://hypequery.com/docs/authentication)
|
|
77
|
+
- [Multi-tenancy](https://hypequery.com/docs/multi-tenancy)
|
|
78
|
+
- [Current capabilities](https://hypequery.com/docs/capabilities)
|
|
267
79
|
|
|
268
80
|
## License
|
|
269
81
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/serve",
|
|
3
|
-
"version": "0.15.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.15.2",
|
|
4
|
+
"description": "Code-first TypeScript runtime for validated ClickHouse analytics APIs, OpenAPI, auth, and tenancy",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"clickhouse",
|
|
7
|
+
"typescript",
|
|
8
|
+
"analytics-api",
|
|
9
|
+
"semantic-layer",
|
|
10
|
+
"query-builder",
|
|
11
|
+
"orm",
|
|
12
|
+
"openapi",
|
|
13
|
+
"multi-tenant",
|
|
14
|
+
"http",
|
|
15
|
+
"zod"
|
|
16
|
+
],
|
|
5
17
|
"license": "Apache-2.0",
|
|
6
18
|
"type": "module",
|
|
7
19
|
"main": "dist/index.js",
|
|
@@ -22,12 +34,12 @@
|
|
|
22
34
|
"dist"
|
|
23
35
|
],
|
|
24
36
|
"dependencies": {
|
|
25
|
-
"@hypequery/protocol": "^0.10.
|
|
37
|
+
"@hypequery/protocol": "^0.10.2",
|
|
26
38
|
"jose": "^5.9.6",
|
|
27
39
|
"openapi-typescript": "^7.13.0",
|
|
28
40
|
"zod": "^3.23.8",
|
|
29
41
|
"zod-to-json-schema": "^3.23.5",
|
|
30
|
-
"@hypequery/datasets": "^0.13.
|
|
42
|
+
"@hypequery/datasets": "^0.13.3"
|
|
31
43
|
},
|
|
32
44
|
"peerDependencies": {
|
|
33
45
|
"tsx": "^4.0.0"
|
|
@@ -46,7 +58,8 @@
|
|
|
46
58
|
},
|
|
47
59
|
"repository": {
|
|
48
60
|
"type": "git",
|
|
49
|
-
"url": "https://github.com/hypequery/hypequery.git"
|
|
61
|
+
"url": "git+https://github.com/hypequery/hypequery.git",
|
|
62
|
+
"directory": "packages/serve"
|
|
50
63
|
},
|
|
51
64
|
"homepage": "https://hypequery.com",
|
|
52
65
|
"bugs": {
|