@hypequery/datasets 0.13.2 → 0.13.4
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 +46 -427
- package/package.json +19 -5
package/README.md
CHANGED
|
@@ -1,477 +1,96 @@
|
|
|
1
1
|
# @hypequery/datasets
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The code-first TypeScript semantic layer for ClickHouse.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@hypequery/datasets` gives product teams one trusted definition for dimensions, measures, metrics, relationships, time grains, and multi-tenant isolation. Keep analytics meaning in the same repo as your application, then reuse it in backend jobs, HTTP APIs, React dashboards, and MCP tools for AI agents.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
It builds on the `@hypequery/clickhouse` type-safe query builder, so semantic models and direct ClickHouse queries share generated schema types.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- dimensions and measures define the allowed query surface
|
|
11
|
-
- metrics name reusable business calculations
|
|
12
|
-
- derived metrics compose same-dataset metrics with symbolic formula helpers
|
|
13
|
-
- runtime validation rejects invalid dimensions, filters, ordering, limits, tenant filters, and derived metric plans before SQL is executed
|
|
9
|
+
No YAML project. No separate semantic-layer server. Just typed, testable product analytics in code.
|
|
14
10
|
|
|
15
11
|
## Install
|
|
16
12
|
|
|
17
13
|
```bash
|
|
18
|
-
npm install @hypequery/datasets
|
|
19
|
-
# or
|
|
20
|
-
pnpm add @hypequery/datasets
|
|
14
|
+
npm install @hypequery/datasets @hypequery/clickhouse
|
|
21
15
|
```
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npm install @hypequery/clickhouse
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Quick Start
|
|
17
|
+
## Define analytics once
|
|
30
18
|
|
|
31
19
|
```ts
|
|
32
|
-
import {
|
|
33
|
-
dataset,
|
|
34
|
-
dimension,
|
|
35
|
-
divide,
|
|
36
|
-
eq,
|
|
37
|
-
measure,
|
|
38
|
-
nullIfZero,
|
|
39
|
-
createDatasetClient,
|
|
40
|
-
} from '@hypequery/datasets';
|
|
41
|
-
import { createQueryBuilder } from '@hypequery/clickhouse';
|
|
20
|
+
import { dataset, dimension, measure } from '@hypequery/datasets';
|
|
42
21
|
|
|
43
|
-
const Orders = dataset('orders', {
|
|
22
|
+
export const Orders = dataset('orders', {
|
|
44
23
|
source: 'orders',
|
|
45
24
|
tenantKey: 'tenant_id',
|
|
46
25
|
timeKey: 'created_at',
|
|
47
26
|
dimensions: {
|
|
48
|
-
|
|
49
|
-
tenantId: dimension.string({ column: 'tenant_id' }),
|
|
27
|
+
region: dimension.string(),
|
|
50
28
|
status: dimension.string(),
|
|
51
|
-
country: dimension.string(),
|
|
52
29
|
createdAt: dimension.timestamp({ column: 'created_at' }),
|
|
53
30
|
},
|
|
54
31
|
measures: {
|
|
55
32
|
revenue: measure.sum('amount'),
|
|
56
33
|
orderCount: measure.count('id'),
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}),
|
|
60
|
-
},
|
|
61
|
-
filters: {
|
|
62
|
-
status: {
|
|
63
|
-
__type: 'filter_definition',
|
|
64
|
-
field: 'status',
|
|
65
|
-
operators: ['eq', 'neq', 'in', 'notIn'],
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const revenue = Orders.metric('revenue', { measure: 'revenue' });
|
|
71
|
-
const orderCount = Orders.metric('orderCount', { measure: 'orderCount' });
|
|
72
|
-
|
|
73
|
-
const averageOrderValue = Orders.metric('averageOrderValue', {
|
|
74
|
-
uses: { revenue, orderCount },
|
|
75
|
-
formula: ({ revenue, orderCount }) =>
|
|
76
|
-
divide(revenue, nullIfZero(orderCount)),
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const db = createQueryBuilder({
|
|
80
|
-
url: process.env.CLICKHOUSE_URL,
|
|
81
|
-
username: process.env.CLICKHOUSE_USER,
|
|
82
|
-
password: process.env.CLICKHOUSE_PASSWORD,
|
|
83
|
-
database: process.env.CLICKHOUSE_DATABASE,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const analytics = createDatasetClient({ queryBuilder: db });
|
|
87
|
-
|
|
88
|
-
const result = await analytics.execute(revenue, {
|
|
89
|
-
dimensions: ['country'],
|
|
90
|
-
filters: [eq('status', 'completed')],
|
|
91
|
-
orderBy: [{ field: 'revenue', direction: 'desc' }],
|
|
92
|
-
limit: 10,
|
|
93
|
-
}, {
|
|
94
|
-
runtime: {
|
|
95
|
-
tenant: 'tenant_123',
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const datasetResult = await analytics.execute(Orders, {
|
|
100
|
-
dimensions: ['country', 'status'],
|
|
101
|
-
measures: ['revenue', 'orderCount'],
|
|
102
|
-
filters: [eq('status', 'completed')],
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const monthlySql = analytics.toSQL(revenue.by('month'), {
|
|
106
|
-
dimensions: ['country'],
|
|
107
|
-
}, {
|
|
108
|
-
runtime: {
|
|
109
|
-
tenant: 'tenant_123',
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## Public API
|
|
115
|
-
|
|
116
|
-
### `dataset(name, config)`
|
|
117
|
-
|
|
118
|
-
Creates a typed semantic model over a source table or view.
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
const Orders = dataset('orders', {
|
|
122
|
-
source: 'orders',
|
|
123
|
-
tenantKey: 'tenant_id',
|
|
124
|
-
timeKey: 'created_at',
|
|
125
|
-
dimensions: {
|
|
126
|
-
id: dimension.number(),
|
|
127
|
-
createdAt: dimension.timestamp({ column: 'created_at' }),
|
|
128
|
-
},
|
|
129
|
-
measures: {
|
|
130
|
-
revenue: measure.sum('amount'),
|
|
34
|
+
p95OrderValue: measure.percentile('amount', 0.95),
|
|
35
|
+
latestStatus: measure.argMax('status', 'createdAt'),
|
|
131
36
|
},
|
|
132
37
|
});
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
`source` is the physical table or view name. The first `dataset()` argument is the logical dataset name. `tenantKey` and `timeKey` are physical column names used for runtime tenant isolation and time graining.
|
|
136
|
-
|
|
137
|
-
If `tenantKey` is set, queries against the dataset require runtime tenant context. This is fail-closed: Hypequery will reject metric and dataset queries that do not include a tenant runtime value or an explicitly enabled cross-tenant scope.
|
|
138
|
-
|
|
139
|
-
### Dimensions
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
dimensions: {
|
|
143
|
-
id: dimension.number(),
|
|
144
|
-
status: dimension.string({ label: 'Status' }),
|
|
145
|
-
isTrial: dimension.boolean({ column: 'is_trial' }),
|
|
146
|
-
createdAt: dimension.timestamp({ column: 'created_at' }),
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Dimension helpers are:
|
|
151
|
-
|
|
152
|
-
- `dimension.string(opts?)`
|
|
153
|
-
- `dimension.number(opts?)`
|
|
154
|
-
- `dimension.boolean(opts?)`
|
|
155
|
-
- `dimension.timestamp(opts?)`
|
|
156
|
-
|
|
157
|
-
Use `opts.column` when the semantic field name differs from the physical column. `opts.sql` exists for SQL-backed dimensions, but schema compatibility can only inspect simple column references and reports a warning for complex SQL expressions.
|
|
158
|
-
|
|
159
|
-
### Measures
|
|
160
38
|
|
|
161
|
-
|
|
162
|
-
measures: {
|
|
163
|
-
revenue: measure.sum('amount'),
|
|
164
|
-
orderCount: measure.count('id'),
|
|
165
|
-
uniqueCustomers: measure.countDistinct('customerId'),
|
|
166
|
-
averageAmount: measure.avg('amount'),
|
|
167
|
-
minAmount: measure.min('amount'),
|
|
168
|
-
maxAmount: measure.max('amount'),
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Filtered measures use semantic filter helpers:
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
import { eq, measure } from '@hypequery/datasets';
|
|
176
|
-
|
|
177
|
-
const Orders = dataset('orders', {
|
|
178
|
-
source: 'orders',
|
|
179
|
-
dimensions: {
|
|
180
|
-
status: dimension.string(),
|
|
181
|
-
},
|
|
182
|
-
measures: {
|
|
183
|
-
completedRevenue: measure.sum('amount', {
|
|
184
|
-
filters: [eq('status', 'completed')],
|
|
185
|
-
}),
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### Metrics
|
|
191
|
-
|
|
192
|
-
Metrics are attached to a dataset and are defined from measures.
|
|
193
|
-
|
|
194
|
-
```ts
|
|
195
|
-
const revenue = Orders.metric('revenue', {
|
|
39
|
+
export const revenue = Orders.metric('revenue', {
|
|
196
40
|
measure: 'revenue',
|
|
197
41
|
label: 'Revenue',
|
|
198
42
|
});
|
|
199
43
|
```
|
|
200
44
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
const averageOrderValue = Orders.metric('averageOrderValue', {
|
|
205
|
-
uses: { revenue, orderCount },
|
|
206
|
-
formula: ({ revenue, orderCount }) =>
|
|
207
|
-
divide(revenue, nullIfZero(orderCount)),
|
|
208
|
-
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
Cross-dataset derived metrics and derived-from-derived metrics are intentionally rejected in the current public surface.
|
|
45
|
+
The model is now the contract. Callers can only use the dimensions, measures, filters, and relationships you publish. A declared `tenantKey` requires trusted runtime scope and rejects unscoped execution.
|
|
212
46
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
Metrics and datasets support two related access patterns:
|
|
216
|
-
|
|
217
|
-
- Metric queries execute one named metric at a time. They are best for reusable product metrics such as `revenue`, `averageOrderValue`, or `monthlyRevenue`, while still allowing valid dimensions, filters, order fields, limits, and time grains.
|
|
218
|
-
- Dataset queries execute an ad-hoc selection of dimensions and measures from one dataset. They are best when callers need Cube-style flexibility within the same dataset, such as grouping by `country` and `status` while selecting both `revenue` and `orderCount`.
|
|
219
|
-
|
|
220
|
-
```ts
|
|
221
|
-
await analytics.execute(revenue, {
|
|
222
|
-
dimensions: ['country'],
|
|
223
|
-
filters: [eq('status', 'completed')],
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
await analytics.execute(Orders, {
|
|
227
|
-
dimensions: ['country', 'status'],
|
|
228
|
-
measures: ['revenue', 'orderCount'],
|
|
229
|
-
});
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
Both paths use the same dataset definition and validation rules. Metric queries provide named, reusable business contracts; dataset queries provide same-dataset exploration.
|
|
233
|
-
|
|
234
|
-
### Time Grains
|
|
235
|
-
|
|
236
|
-
Use `.by(grain)` on a metric when the dataset has a `timeKey`.
|
|
47
|
+
## Query it anywhere
|
|
237
48
|
|
|
238
49
|
```ts
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
},
|
|
244
|
-
|
|
245
|
-
tenant: 'tenant_123',
|
|
50
|
+
const result = await analytics.execute(
|
|
51
|
+
revenue,
|
|
52
|
+
{
|
|
53
|
+
dimensions: ['region'],
|
|
54
|
+
orderBy: [{ field: 'revenue', direction: 'desc' }],
|
|
55
|
+
limit: 10,
|
|
246
56
|
},
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
Supported grains are `day`, `week`, `month`, `quarter`, and `year`.
|
|
251
|
-
|
|
252
|
-
### Runtime Tenancy
|
|
253
|
-
|
|
254
|
-
Runtime tenancy uses the dataset `tenantKey` and a runtime tenant identity.
|
|
255
|
-
|
|
256
|
-
```ts
|
|
257
|
-
const Orders = dataset('orders', {
|
|
258
|
-
source: 'orders',
|
|
259
|
-
tenantKey: 'tenant_id',
|
|
260
|
-
dimensions: {
|
|
261
|
-
tenantId: dimension.string({ column: 'tenant_id' }),
|
|
262
|
-
country: dimension.string(),
|
|
57
|
+
{
|
|
58
|
+
runtime: { tenant: session.accountId },
|
|
263
59
|
},
|
|
264
|
-
|
|
265
|
-
revenue: measure.sum('amount'),
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const revenue = Orders.metric('revenue', { measure: 'revenue' });
|
|
270
|
-
|
|
271
|
-
await analytics.execute(revenue, {}, {
|
|
272
|
-
runtime: {
|
|
273
|
-
tenant: 'tenant_123',
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
Here `tenantKey` is the physical column and `runtime.tenant` is the trusted runtime value. Together they produce a tenant predicate equivalent to:
|
|
279
|
-
|
|
280
|
-
```sql
|
|
281
|
-
WHERE tenant_id = 'tenant_123'
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
If a dataset has `tenantKey`, runtime tenant context is required:
|
|
285
|
-
|
|
286
|
-
```ts
|
|
287
|
-
await analytics.execute(revenue);
|
|
288
|
-
// Error: Dataset "orders" requires runtime tenant scoping.
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
You can also provide a trusted set of tenants for admin dashboards that are scoped to multiple accounts:
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
await analytics.execute(revenue, {}, {
|
|
295
|
-
runtime: {
|
|
296
|
-
tenant: { in: ['tenant_123', 'tenant_456'] },
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
This produces a tenant predicate equivalent to:
|
|
302
|
-
|
|
303
|
-
```sql
|
|
304
|
-
WHERE tenant_id IN ('tenant_123', 'tenant_456')
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
When runtime tenancy is active, explicit filters on the tenant field are rejected. This prevents duplicate or conflicting tenant predicates:
|
|
308
|
-
|
|
309
|
-
```ts
|
|
310
|
-
await analytics.execute(revenue, {
|
|
311
|
-
filters: [eq('tenantId', 'tenant_123')],
|
|
312
|
-
}, {
|
|
313
|
-
runtime: {
|
|
314
|
-
tenant: 'tenant_123',
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
// Error: Cannot filter on tenant field "tenantId" when runtime tenancy enforcement is active.
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
The runtime integration should provide tenant identity from trusted server/session state. Do not accept tenant ids from end-user query input. In `@hypequery/serve`, regular hand-written queries can use Serve tenant auto-injection, but semantic dataset and metric endpoints pass tenant identity to `@hypequery/datasets`, and datasets injects the filter from `tenantKey`.
|
|
321
|
-
|
|
322
|
-
For jobs, admin tools, or reporting surfaces that intentionally query across tenants, use `runtime.tenant: { scope: 'all' }`:
|
|
323
|
-
|
|
324
|
-
```ts
|
|
325
|
-
await analytics.execute(revenue, {}, {
|
|
326
|
-
runtime: {
|
|
327
|
-
tenant: { scope: 'all' },
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
`scope: 'all'` omits the tenant predicate for every `tenantKey` dataset touched by the semantic query. Use this only in trusted contexts like background jobs or admin dashboards, not in request-facing clients or MCP servers.
|
|
333
|
-
|
|
334
|
-
### Relationships
|
|
335
|
-
|
|
336
|
-
Relationships can be stored as semantic metadata and exposed through introspection.
|
|
337
|
-
|
|
338
|
-
```ts
|
|
339
|
-
import { belongsTo } from '@hypequery/datasets';
|
|
340
|
-
|
|
341
|
-
const Orders = dataset('orders', {
|
|
342
|
-
source: 'orders',
|
|
343
|
-
dimensions: {
|
|
344
|
-
customerId: dimension.string({ column: 'customer_id' }),
|
|
345
|
-
},
|
|
346
|
-
relationships: {
|
|
347
|
-
customer: belongsTo(() => Customers, { from: 'customerId', to: 'id' }),
|
|
348
|
-
},
|
|
349
|
-
});
|
|
60
|
+
);
|
|
350
61
|
```
|
|
351
62
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
## Execution
|
|
355
|
-
|
|
356
|
-
Use `createDatasetClient` from `@hypequery/datasets` with a backend implementation to execute semantic targets.
|
|
357
|
-
|
|
358
|
-
```ts
|
|
359
|
-
const validation = analytics.validate(revenue, {
|
|
360
|
-
dimensions: ['country'],
|
|
361
|
-
}, {
|
|
362
|
-
runtime: {
|
|
363
|
-
tenant: 'tenant_123',
|
|
364
|
-
},
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
const sql = analytics.toSQL(revenue, {
|
|
368
|
-
dimensions: ['country'],
|
|
369
|
-
}, {
|
|
370
|
-
runtime: {
|
|
371
|
-
tenant: 'tenant_123',
|
|
372
|
-
},
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
const result = await analytics.execute(revenue, {
|
|
376
|
-
dimensions: ['country'],
|
|
377
|
-
}, {
|
|
378
|
-
runtime: {
|
|
379
|
-
tenant: 'tenant_123',
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
const datasetResult = await analytics.execute(Orders, {
|
|
384
|
-
dimensions: ['country'],
|
|
385
|
-
measures: ['revenue'],
|
|
386
|
-
}, {
|
|
387
|
-
runtime: {
|
|
388
|
-
tenant: 'tenant_123',
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
The semantic client validates dimensions, filters, order fields, limits, time grain requirements, tenant filtering, and derived metric plans before execution.
|
|
394
|
-
|
|
395
|
-
### ClickHouse
|
|
396
|
-
|
|
397
|
-
For ClickHouse, pass a query builder from `@hypequery/clickhouse`. This is the recommended path: the dataset client reuses the same connection and builder you use for hand-written queries.
|
|
398
|
-
|
|
399
|
-
```ts
|
|
400
|
-
import { createDatasetClient } from '@hypequery/datasets';
|
|
401
|
-
import { createQueryBuilder } from '@hypequery/clickhouse';
|
|
402
|
-
|
|
403
|
-
const db = createQueryBuilder({
|
|
404
|
-
url: process.env.CLICKHOUSE_URL,
|
|
405
|
-
username: process.env.CLICKHOUSE_USER,
|
|
406
|
-
password: process.env.CLICKHOUSE_PASSWORD,
|
|
407
|
-
database: process.env.CLICKHOUSE_DATABASE,
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
const analytics = createDatasetClient({ queryBuilder: db });
|
|
411
|
-
|
|
412
|
-
await analytics.execute(revenue, { dimensions: ['country'] }, {
|
|
413
|
-
runtime: {
|
|
414
|
-
tenant: 'tenant_123',
|
|
415
|
-
},
|
|
416
|
-
});
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
### Advanced: SemanticBackend protocol
|
|
420
|
-
|
|
421
|
-
`createDatasetClient` also accepts a `backend` implementing the database-agnostic `SemanticBackend` interface. For ClickHouse, `createBackend` from `@hypequery/clickhouse/datasets` builds one from connection config — use it when you want a standalone backend instance instead of sharing a query builder.
|
|
422
|
-
|
|
423
|
-
```ts
|
|
424
|
-
import { createDatasetClient } from '@hypequery/datasets';
|
|
425
|
-
import { createBackend } from '@hypequery/clickhouse/datasets';
|
|
426
|
-
|
|
427
|
-
const analytics = createDatasetClient({
|
|
428
|
-
backend: createBackend({
|
|
429
|
-
url: process.env.CLICKHOUSE_URL,
|
|
430
|
-
username: process.env.CLICKHOUSE_USER,
|
|
431
|
-
password: process.env.CLICKHOUSE_PASSWORD,
|
|
432
|
-
database: process.env.CLICKHOUSE_DATABASE,
|
|
433
|
-
}),
|
|
434
|
-
});
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
The same `SemanticBackend` interface enables support for other databases. Future packages like `@hypequery/duckdb` or `@hypequery/postgres` would follow the same pattern.
|
|
438
|
-
|
|
439
|
-
## Integration Surfaces
|
|
440
|
-
|
|
441
|
-
Dataset definitions can be reused in several places:
|
|
442
|
-
|
|
443
|
-
- direct execution with `createDatasetClient(...)`
|
|
444
|
-
- SQL inspection with `analytics.toSQL(...)`
|
|
445
|
-
- runtime validation with `analytics.validate(...)`
|
|
446
|
-
- HTTP metric and dataset endpoints through `@hypequery/serve`
|
|
447
|
-
- agent-facing tools through `@hypequery/mcp`
|
|
448
|
-
|
|
449
|
-
## Serve Integration
|
|
450
|
-
|
|
451
|
-
`@hypequery/serve` can expose metric and dataset endpoints from dataset definitions. Dataset endpoint planning uses `@hypequery/datasets/internal` as a package-integration boundary.
|
|
63
|
+
The same definition can become:
|
|
452
64
|
|
|
453
|
-
|
|
65
|
+
- a named KPI or flexible dataset query;
|
|
66
|
+
- a validated `@hypequery/serve` endpoint;
|
|
67
|
+
- a typed `@hypequery/react` hook;
|
|
68
|
+
- an OpenAI, AI SDK, or MCP tool schema;
|
|
69
|
+
- a stable semantic contract for CI and deployment.
|
|
454
70
|
|
|
455
|
-
|
|
71
|
+
## Analytics teams actually need
|
|
456
72
|
|
|
457
|
-
|
|
73
|
+
- sums, counts, distinct counts, averages, min, and max;
|
|
74
|
+
- percentiles, median, `argMax`, `argMin`, standard deviation, and variance;
|
|
75
|
+
- filtered measures and derived metric formulas;
|
|
76
|
+
- daily through yearly time grains;
|
|
77
|
+
- fail-closed multi-tenant analytics;
|
|
78
|
+
- one-hop typed `belongsTo` and `hasOne` dimensions;
|
|
79
|
+
- validated filtering, sorting, and pagination.
|
|
458
80
|
|
|
459
|
-
|
|
81
|
+
See the [current capability matrix](https://hypequery.com/docs/capabilities) for exact syntax and the complete shipped surface.
|
|
460
82
|
|
|
461
|
-
##
|
|
83
|
+
## Why code-first
|
|
462
84
|
|
|
463
|
-
|
|
85
|
+
Metric changes travel through the workflow your team already trusts: TypeScript, code review, tests, CI, and version control. Backend, frontend, and agent consumers stop maintaining their own definition of “revenue.”
|
|
464
86
|
|
|
465
|
-
|
|
466
|
-
- Root exports for dataset endpoint execution helpers are not public API
|
|
467
|
-
- Deep imports from package internals are not application API
|
|
468
|
-
- Automatic relationship JOIN execution is not shipped
|
|
469
|
-
- Cross-dataset derived metrics are rejected
|
|
470
|
-
- Derived-from-derived metrics are rejected
|
|
471
|
-
- Pre-aggregations or materialized rollups are not implemented
|
|
472
|
-
- BI tool protocol compatibility is not implemented
|
|
87
|
+
## Learn more
|
|
473
88
|
|
|
474
|
-
|
|
89
|
+
- [Datasets overview](https://hypequery.com/docs/datasets/overview)
|
|
90
|
+
- [Measures and metrics](https://hypequery.com/docs/datasets/measures)
|
|
91
|
+
- [Multi-tenancy](https://hypequery.com/docs/datasets/multi-tenancy)
|
|
92
|
+
- [Relationships](https://hypequery.com/docs/datasets/relationships)
|
|
93
|
+
- [MCP tool generation](https://hypequery.com/docs/datasets/tool-generation)
|
|
475
94
|
|
|
476
95
|
## License
|
|
477
96
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/datasets",
|
|
3
|
-
"version": "0.13.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.13.4",
|
|
4
|
+
"description": "Code-first TypeScript semantic layer for ClickHouse datasets, metrics, multi-tenancy, and AI agents",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"semantic-layer",
|
|
7
|
+
"clickhouse",
|
|
8
|
+
"typescript",
|
|
9
|
+
"analytics",
|
|
10
|
+
"query-builder",
|
|
11
|
+
"orm",
|
|
12
|
+
"metrics",
|
|
13
|
+
"datasets",
|
|
14
|
+
"multi-tenant",
|
|
15
|
+
"mcp",
|
|
16
|
+
"ai-agents"
|
|
17
|
+
],
|
|
5
18
|
"main": "dist/index.js",
|
|
6
19
|
"types": "dist/index.d.ts",
|
|
7
20
|
"type": "module",
|
|
@@ -29,7 +42,7 @@
|
|
|
29
42
|
"dist"
|
|
30
43
|
],
|
|
31
44
|
"dependencies": {
|
|
32
|
-
"@hypequery/protocol": "^0.
|
|
45
|
+
"@hypequery/protocol": "^0.11.0",
|
|
33
46
|
"@noble/hashes": "^1.8.0",
|
|
34
47
|
"zod": "^3.22.4"
|
|
35
48
|
},
|
|
@@ -38,11 +51,12 @@
|
|
|
38
51
|
"typescript": "^5.7.3",
|
|
39
52
|
"@vitest/coverage-v8": "^3.2.6",
|
|
40
53
|
"vitest": "^3.2.6",
|
|
41
|
-
"@hypequery/protocol-conformance": "^0.
|
|
54
|
+
"@hypequery/protocol-conformance": "^0.10.0"
|
|
42
55
|
},
|
|
43
56
|
"repository": {
|
|
44
57
|
"type": "git",
|
|
45
|
-
"url": "https://github.com/hypequery/hypequery.git"
|
|
58
|
+
"url": "git+https://github.com/hypequery/hypequery.git",
|
|
59
|
+
"directory": "packages/datasets"
|
|
46
60
|
},
|
|
47
61
|
"homepage": "https://hypequery.com",
|
|
48
62
|
"bugs": {
|