metrickit 0.1.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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +379 -0
- package/dist/cache-redis.d.ts +10 -0
- package/dist/cache-redis.js +24 -0
- package/dist/cache-utils.d.ts +5 -0
- package/dist/cache.d.ts +18 -0
- package/dist/catalog.d.ts +12 -0
- package/dist/define-metric.d.ts +37 -0
- package/dist/engine.d.ts +200 -0
- package/dist/filters/default-metadata.d.ts +28 -0
- package/dist/filters/index.d.ts +3 -0
- package/dist/filters/parse.d.ts +7 -0
- package/dist/filters/types.d.ts +19 -0
- package/dist/frontend/catalog.d.ts +24 -0
- package/dist/frontend/dashboard.d.ts +12 -0
- package/dist/frontend/format.d.ts +10 -0
- package/dist/frontend/index.d.ts +10 -0
- package/dist/frontend/markers.d.ts +19 -0
- package/dist/frontend/renderers.d.ts +14 -0
- package/dist/frontend/requests.d.ts +17 -0
- package/dist/frontend/stream-state.d.ts +14 -0
- package/dist/frontend/time.d.ts +5 -0
- package/dist/frontend/transport.d.ts +19 -0
- package/dist/frontend/types.d.ts +108 -0
- package/dist/frontend.d.ts +1 -0
- package/dist/frontend.js +752 -0
- package/dist/helpers/clickhouse.d.ts +27 -0
- package/dist/helpers/distribution.d.ts +15 -0
- package/dist/helpers/index.d.ts +6 -0
- package/dist/helpers/metric-type.d.ts +21 -0
- package/dist/helpers/pivot.d.ts +15 -0
- package/dist/helpers/prisma.d.ts +20 -0
- package/dist/helpers/timeseries.d.ts +6 -0
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +668 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1322 -0
- package/dist/orpc.d.ts +36 -0
- package/dist/orpc.js +1157 -0
- package/dist/registry.d.ts +269 -0
- package/dist/run-metrics.d.ts +14 -0
- package/dist/schemas/index.d.ts +4 -0
- package/dist/schemas/inputs.d.ts +19 -0
- package/dist/schemas/metric-type.d.ts +7 -0
- package/dist/schemas/output.d.ts +842 -0
- package/dist/schemas/time.d.ts +24 -0
- package/dist/time.d.ts +6 -0
- package/dist/type-guards.d.ts +7 -0
- package/package.json +91 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project follows a pragmatic `0.x` release policy:
|
|
7
|
+
|
|
8
|
+
- breaking API changes may still happen between minor releases
|
|
9
|
+
- supported public entry points are documented in `README.md`
|
|
10
|
+
- undocumented/internal utilities may change without notice
|
|
11
|
+
|
|
12
|
+
## [0.1.0] - 2026-04-07
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Initial release of the single-package `metrickit` library
|
|
17
|
+
- Core metrics engine, registry, runtime execution, and cache interfaces
|
|
18
|
+
- ORPC helpers via `metrickit/orpc`
|
|
19
|
+
- Redis cache adapter via `metrickit/cache-redis`
|
|
20
|
+
- Framework-neutral frontend helpers via `metrickit/frontend`
|
|
21
|
+
- Advanced helper utilities grouped under `metrickit/helpers`
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kristoffer Hansen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# metrickit
|
|
2
|
+
|
|
3
|
+
Type-safe metrics, dashboards, and transport primitives for TypeScript apps.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Strong TypeScript inference from metric definition to request, result, and dashboard usage
|
|
8
|
+
- A single engine that owns shared context, base filters, caching, and custom metric kinds
|
|
9
|
+
- Registry-driven execution so one metric list powers runtime lookup and compile-time typing
|
|
10
|
+
- Built-in metric kinds for KPI, time series, distribution, table, leaderboard, and pivot outputs
|
|
11
|
+
- Extensible custom metric kinds through `engine.defineMetric(kind, def)`
|
|
12
|
+
- Pluggable caching through `CacheAdapter` with a Redis adapter at `metrickit/cache-redis`
|
|
13
|
+
- ORPC-friendly helpers at `metrickit/orpc`
|
|
14
|
+
- Framework-neutral frontend helpers at `metrickit/frontend`
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add metrickit zod
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
or
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install metrickit zod
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`zod` is a peer dependency so your app and `metrickit` share the same Zod version.
|
|
29
|
+
|
|
30
|
+
## Runtime Support
|
|
31
|
+
|
|
32
|
+
- Node.js `>=18`
|
|
33
|
+
- Bun `>=1.0`
|
|
34
|
+
|
|
35
|
+
## Package Entry Points
|
|
36
|
+
|
|
37
|
+
- `metrickit`: engine, registry, schemas, runtime helpers, cache interfaces, and filters
|
|
38
|
+
- `metrickit/orpc`: typed router helpers for `runMetrics`, streaming, and catalog discovery
|
|
39
|
+
- `metrickit/cache-redis`: Redis cache adapter
|
|
40
|
+
- `metrickit/frontend`: typed frontend request, dashboard, stream-state, catalog, and formatting helpers
|
|
41
|
+
- `metrickit/helpers`: advanced helper utilities for ClickHouse, Prisma, distributions, timeseries shaping, pivot building, and metric-type helpers
|
|
42
|
+
|
|
43
|
+
## Supported Public API
|
|
44
|
+
|
|
45
|
+
The normal supported path is:
|
|
46
|
+
|
|
47
|
+
- `createMetricsEngine()`
|
|
48
|
+
- `engine.define*Metric(...)`
|
|
49
|
+
- `engine.createRegistry(...)`
|
|
50
|
+
- `runMetrics()` or `createMetricsRouter(...)`
|
|
51
|
+
- optional frontend helpers from `metrickit/frontend`
|
|
52
|
+
|
|
53
|
+
The following are also supported, but are more advanced:
|
|
54
|
+
|
|
55
|
+
- schemas and filter utilities re-exported from `metrickit`
|
|
56
|
+
- `metrickit/helpers` for framework/database-specific helper utilities
|
|
57
|
+
|
|
58
|
+
Avoid depending on internal source paths or unpublished files inside `dist`.
|
|
59
|
+
|
|
60
|
+
## Core Workflow
|
|
61
|
+
|
|
62
|
+
1. Create a metrics engine with your shared context and base filters.
|
|
63
|
+
2. Define metrics through that engine.
|
|
64
|
+
3. Register the metrics once in a registry.
|
|
65
|
+
4. Execute the registry directly or expose it through ORPC.
|
|
66
|
+
5. Reuse the same registry types in frontend helpers.
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { z } from 'zod'
|
|
72
|
+
import {
|
|
73
|
+
BaseFiltersSchema,
|
|
74
|
+
createMetricsEngine,
|
|
75
|
+
defineKpiOutput,
|
|
76
|
+
runMetrics,
|
|
77
|
+
} from 'metrickit'
|
|
78
|
+
import { createMetricsRouter } from 'metrickit/orpc'
|
|
79
|
+
import { redisCacheAdapter } from 'metrickit/cache-redis'
|
|
80
|
+
|
|
81
|
+
const FunnelOutputSchema = z.object({
|
|
82
|
+
kind: z.literal('funnel'),
|
|
83
|
+
steps: z.array(
|
|
84
|
+
z.object({
|
|
85
|
+
key: z.string(),
|
|
86
|
+
value: z.number(),
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const AppFiltersSchema = BaseFiltersSchema.extend({
|
|
92
|
+
country: z.string().optional(),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const metricsEngine = createMetricsEngine<
|
|
96
|
+
{ db: unknown; viewerId: string },
|
|
97
|
+
z.infer<typeof AppFiltersSchema>,
|
|
98
|
+
{ funnel: typeof FunnelOutputSchema }
|
|
99
|
+
>({
|
|
100
|
+
baseFilters: AppFiltersSchema,
|
|
101
|
+
cache: redisCacheAdapter(redisClient),
|
|
102
|
+
customKinds: {
|
|
103
|
+
funnel: FunnelOutputSchema,
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const totalRevenueMetric = metricsEngine.defineKpiMetric({
|
|
108
|
+
key: 'revenue.total',
|
|
109
|
+
description: 'Total revenue',
|
|
110
|
+
supportsTimeRange: true,
|
|
111
|
+
filterSchema: AppFiltersSchema,
|
|
112
|
+
async resolve({ filters }) {
|
|
113
|
+
return defineKpiOutput({
|
|
114
|
+
value: filters.country === 'DK' ? 1200 : 900,
|
|
115
|
+
label: 'Revenue',
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const pipelineFunnelMetric = metricsEngine.defineMetric('funnel', {
|
|
121
|
+
key: 'pipeline.funnel',
|
|
122
|
+
description: 'Pipeline conversion funnel',
|
|
123
|
+
supportsTimeRange: false,
|
|
124
|
+
filterSchema: AppFiltersSchema,
|
|
125
|
+
async resolve() {
|
|
126
|
+
return {
|
|
127
|
+
kind: 'funnel',
|
|
128
|
+
steps: [
|
|
129
|
+
{ key: 'visits', value: 1200 },
|
|
130
|
+
{ key: 'signups', value: 240 },
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
export const metricsRegistry = metricsEngine.createRegistry([
|
|
137
|
+
totalRevenueMetric,
|
|
138
|
+
pipelineFunnelMetric,
|
|
139
|
+
] as const)
|
|
140
|
+
|
|
141
|
+
export type MetricsRegistry = typeof metricsRegistry
|
|
142
|
+
|
|
143
|
+
const result = await runMetrics({
|
|
144
|
+
registry: metricsRegistry,
|
|
145
|
+
request: {
|
|
146
|
+
metrics: [
|
|
147
|
+
{ key: 'revenue.total', filters: { country: 'DK' } },
|
|
148
|
+
{ key: 'pipeline.funnel', requestKey: 'pipeline' },
|
|
149
|
+
],
|
|
150
|
+
compareToPrevious: true,
|
|
151
|
+
},
|
|
152
|
+
createContext: () => ({
|
|
153
|
+
db,
|
|
154
|
+
viewerId: 'viewer_1',
|
|
155
|
+
}),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
result.metrics['revenue.total']?.current.value
|
|
159
|
+
result.metrics.pipeline?.current.kind
|
|
160
|
+
|
|
161
|
+
const metricsRouter = createMetricsRouter({
|
|
162
|
+
registry: metricsRegistry,
|
|
163
|
+
createContext: async (orpcCtx: { db: unknown; viewerId: string }) => ({
|
|
164
|
+
db: orpcCtx.db,
|
|
165
|
+
viewerId: orpcCtx.viewerId,
|
|
166
|
+
}),
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Define An Engine
|
|
171
|
+
|
|
172
|
+
`createMetricsEngine()` is the main entry point. It owns:
|
|
173
|
+
|
|
174
|
+
- your resolver context type
|
|
175
|
+
- your base filter schema
|
|
176
|
+
- your cache adapter
|
|
177
|
+
- your custom metric kinds
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const engine = createMetricsEngine<
|
|
181
|
+
{ db: DbClient; tenantId: string },
|
|
182
|
+
z.infer<typeof AppFiltersSchema>
|
|
183
|
+
>({
|
|
184
|
+
baseFilters: AppFiltersSchema,
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Use the engine methods to define metrics:
|
|
189
|
+
|
|
190
|
+
- `engine.defineKpiMetric(def)`
|
|
191
|
+
- `engine.defineTimeSeriesMetric(def)`
|
|
192
|
+
- `engine.defineDistributionMetric(def)`
|
|
193
|
+
- `engine.defineTableMetric(def)`
|
|
194
|
+
- `engine.defineLeaderboardMetric(def)`
|
|
195
|
+
- `engine.definePivotMetric(def)`
|
|
196
|
+
- `engine.defineMetric(kind, def)` for custom kinds
|
|
197
|
+
|
|
198
|
+
## Create A Registry
|
|
199
|
+
|
|
200
|
+
The registry is the central typed contract for your app.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
export const metricsRegistry = engine.createRegistry([
|
|
204
|
+
totalRevenueMetric,
|
|
205
|
+
pipelineFunnelMetric,
|
|
206
|
+
] as const)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The registry drives:
|
|
210
|
+
|
|
211
|
+
- valid metric keys
|
|
212
|
+
- request filter typing per metric
|
|
213
|
+
- result typing per request
|
|
214
|
+
- ORPC procedures
|
|
215
|
+
- frontend request and dashboard helpers
|
|
216
|
+
|
|
217
|
+
## Output Types
|
|
218
|
+
|
|
219
|
+
There are two similarly named output concepts:
|
|
220
|
+
|
|
221
|
+
- `MetricOutput` from `metrickit` is the schema-level union of built-in output shapes
|
|
222
|
+
- `MetricOutputFromDef` from `metrickit` gives you the resolved output type for a specific metric definition
|
|
223
|
+
|
|
224
|
+
Use `MetricOutputFromDef` when you want the output type of a concrete metric definition.
|
|
225
|
+
|
|
226
|
+
## Run Metrics Directly
|
|
227
|
+
|
|
228
|
+
Use `runMetrics()` on the server when you do not need ORPC.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const result = await runMetrics({
|
|
232
|
+
registry: metricsRegistry,
|
|
233
|
+
request: {
|
|
234
|
+
metrics: [
|
|
235
|
+
{ key: 'revenue.total' },
|
|
236
|
+
{ key: 'pipeline.funnel', requestKey: 'pipeline' },
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
createContext: async () => ({ db, viewerId }),
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Use `runMetricsStream()` when your consumer wants streamed chunks as metrics resolve.
|
|
244
|
+
|
|
245
|
+
## ORPC Integration
|
|
246
|
+
|
|
247
|
+
`metrickit/orpc` exposes typed helpers for the common API surface.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { createMetricsRouter } from 'metrickit/orpc'
|
|
251
|
+
|
|
252
|
+
export const metricsRouter = createMetricsRouter({
|
|
253
|
+
registry: metricsRegistry,
|
|
254
|
+
createContext: async (orpcCtx: { db: DbClient; viewerId: string }) => ({
|
|
255
|
+
db: orpcCtx.db,
|
|
256
|
+
viewerId: orpcCtx.viewerId,
|
|
257
|
+
}),
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const availableMetrics = metricsRouter.getAvailableMetrics({
|
|
261
|
+
db,
|
|
262
|
+
viewerId: 'viewer_1',
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const result = await metricsRouter.runMetrics(
|
|
266
|
+
{
|
|
267
|
+
metrics: [{ key: 'revenue.total' }],
|
|
268
|
+
},
|
|
269
|
+
{ db, viewerId: 'viewer_1' },
|
|
270
|
+
)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Caching
|
|
274
|
+
|
|
275
|
+
The engine accepts any `CacheAdapter`. If you already have a Redis-like client, use the built-in adapter:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { redisCacheAdapter } from 'metrickit/cache-redis'
|
|
279
|
+
|
|
280
|
+
const engine = createMetricsEngine({
|
|
281
|
+
cache: redisCacheAdapter(redisClient),
|
|
282
|
+
})
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Your Redis client only needs:
|
|
286
|
+
|
|
287
|
+
- `mget(...keys)`
|
|
288
|
+
- `pipeline().setex(...).exec()`
|
|
289
|
+
|
|
290
|
+
## Frontend Helpers
|
|
291
|
+
|
|
292
|
+
`metrickit/frontend` is intentionally framework-neutral. It gives you typed request builders, stream-state helpers, catalog helpers, and dashboard config utilities that can be wrapped by React, Vue, or another UI layer.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import {
|
|
296
|
+
createMetricsStreamState,
|
|
297
|
+
defineHeadline,
|
|
298
|
+
defineMetricRequest,
|
|
299
|
+
defineMetricsDashboard,
|
|
300
|
+
defineMetricsRequest,
|
|
301
|
+
defineWidget,
|
|
302
|
+
getMetricResult,
|
|
303
|
+
} from 'metrickit/frontend'
|
|
304
|
+
|
|
305
|
+
const request = defineMetricsRequest<typeof metricsRegistry>({
|
|
306
|
+
metrics: [
|
|
307
|
+
defineMetricRequest<typeof metricsRegistry, 'revenue.total'>({
|
|
308
|
+
key: 'revenue.total',
|
|
309
|
+
requestKey: 'revenue',
|
|
310
|
+
}),
|
|
311
|
+
] as const,
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const revenueResult = getMetricResult(
|
|
315
|
+
{
|
|
316
|
+
metrics: {
|
|
317
|
+
revenue: {
|
|
318
|
+
current: defineKpiOutput({ value: 1200, label: 'Revenue' }),
|
|
319
|
+
previous: undefined,
|
|
320
|
+
supportsTimeRange: true,
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
errors: {},
|
|
324
|
+
},
|
|
325
|
+
'revenue',
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
const dashboard = defineMetricsDashboard<typeof metricsRegistry>({
|
|
329
|
+
title: 'Overview',
|
|
330
|
+
widgets: [
|
|
331
|
+
defineHeadline('Revenue'),
|
|
332
|
+
defineWidget<typeof metricsRegistry, 'revenue.total'>('revenue.total', {
|
|
333
|
+
requestKey: 'revenue',
|
|
334
|
+
layout: { cols: 6 },
|
|
335
|
+
}),
|
|
336
|
+
] as const,
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
const streamState = createMetricsStreamState(request)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The frontend package includes helpers for:
|
|
343
|
+
|
|
344
|
+
- typed requests and results
|
|
345
|
+
- stream-state handling
|
|
346
|
+
- dashboard config and widget definitions
|
|
347
|
+
- available metric catalog shaping
|
|
348
|
+
- formatting and time labels
|
|
349
|
+
- chart markers and renderer registries
|
|
350
|
+
|
|
351
|
+
## Advanced Helpers
|
|
352
|
+
|
|
353
|
+
Advanced utility helpers are grouped under `metrickit/helpers` instead of the root package so the primary API stays smaller and easier to learn.
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
import {
|
|
357
|
+
buildTimeRangeWhere,
|
|
358
|
+
mapBucketsToPoints,
|
|
359
|
+
resolveMetricType,
|
|
360
|
+
} from 'metrickit/helpers'
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Notes
|
|
364
|
+
|
|
365
|
+
- `createRegistry()` is the main typed contract you should export from your app.
|
|
366
|
+
- `requestKey` lets you alias a metric result key while preserving type safety.
|
|
367
|
+
- `runMetrics()` and `runMetricsStream()` validate requests against the registry before executing resolvers.
|
|
368
|
+
- Most consumers only need the engine, registry, runtime helpers, and optional ORPC/frontend entry points.
|
|
369
|
+
- `metrickit/helpers` is the place for more specialized utilities that are not part of the minimal happy path.
|
|
370
|
+
|
|
371
|
+
## Local Development
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
bun run typecheck
|
|
375
|
+
bun test
|
|
376
|
+
bun run lint
|
|
377
|
+
bun run build
|
|
378
|
+
bun run smoke:pack
|
|
379
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CacheAdapter } from './cache.ts';
|
|
2
|
+
export interface RedisLikeClient {
|
|
3
|
+
mget(...keys: string[]): Promise<(string | null)[]>;
|
|
4
|
+
pipeline(): RedisPipeline;
|
|
5
|
+
}
|
|
6
|
+
export interface RedisPipeline {
|
|
7
|
+
setex(key: string, seconds: number, value: string): RedisPipeline;
|
|
8
|
+
exec(): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
export declare function redisCacheAdapter(client: RedisLikeClient): CacheAdapter;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/cache-redis.ts
|
|
2
|
+
function redisCacheAdapter(client) {
|
|
3
|
+
return {
|
|
4
|
+
async mget(keys) {
|
|
5
|
+
if (keys.length === 0)
|
|
6
|
+
return [];
|
|
7
|
+
return client.mget(...keys);
|
|
8
|
+
},
|
|
9
|
+
async mset(entries) {
|
|
10
|
+
if (entries.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
const pipeline = client.pipeline();
|
|
13
|
+
for (const { key, value, ttl } of entries) {
|
|
14
|
+
if (ttl > 0) {
|
|
15
|
+
pipeline.setex(key, ttl, value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
await pipeline.exec();
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
redisCacheAdapter
|
|
24
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type JsonPrimitive = string | number | boolean | null | JsonPrimitive[];
|
|
2
|
+
export declare function stableHash(obj: Record<string, JsonPrimitive>): string;
|
|
3
|
+
export declare function normalizeForCache(obj: Record<string, unknown>): Record<string, JsonPrimitive>;
|
|
4
|
+
export declare function getCacheKey(metricKey: string, filters: Record<string, unknown>, period?: 'current' | 'previous', granularity?: string): string;
|
|
5
|
+
export {};
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface CacheAdapter {
|
|
2
|
+
mget(keys: string[]): Promise<(string | null)[]>;
|
|
3
|
+
mset(entries: {
|
|
4
|
+
key: string;
|
|
5
|
+
value: string;
|
|
6
|
+
ttl: number;
|
|
7
|
+
}[]): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare const noopCacheAdapter: CacheAdapter;
|
|
10
|
+
interface SafeParseResult<T> {
|
|
11
|
+
success: boolean;
|
|
12
|
+
data?: T;
|
|
13
|
+
}
|
|
14
|
+
interface SafeParser<T> {
|
|
15
|
+
safeParse: (data: unknown) => SafeParseResult<T>;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseCache<T>(cached: string | null, schema: SafeParser<T>): T | null;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type MetricCatalogFreshness = 'realtime' | 'nearRealtime' | 'hourly' | 'daily' | 'manual';
|
|
2
|
+
export type MetricCatalogSource = 'prisma' | 'clickhouse' | 'redis' | 'computed';
|
|
3
|
+
export interface MetricCatalogMetadata {
|
|
4
|
+
displayName?: string;
|
|
5
|
+
owner?: string;
|
|
6
|
+
freshness?: MetricCatalogFreshness;
|
|
7
|
+
sources?: MetricCatalogSource[];
|
|
8
|
+
intendedUse?: string;
|
|
9
|
+
drilldownRoute?: string;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function defineMetricCatalogMetadata<T extends MetricCatalogMetadata>(metadata: T): T;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { MetricCatalogMetadata } from './catalog.ts';
|
|
3
|
+
import type { BaseFilters, OutputForKind, OutputKind, OutputSchemaForKind, TimeSeriesOutput } from './schemas/index.ts';
|
|
4
|
+
import type { TimeGranularity } from './schemas/time.ts';
|
|
5
|
+
import type { MetricFilterFieldMetadata } from './filters/types.ts';
|
|
6
|
+
export type MetricFilterFieldMetadataMap<TFilters extends BaseFilters> = Partial<Record<Extract<keyof Omit<TFilters, 'organizationIds' | 'from' | 'to'>, string>, MetricFilterFieldMetadata>>;
|
|
7
|
+
export interface MetricDefinition<TKey extends string, TKind extends string, TFilters extends BaseFilters, TOutput, TContext = unknown, TCatalog extends MetricCatalogMetadata | undefined = MetricCatalogMetadata | undefined> {
|
|
8
|
+
key: TKey;
|
|
9
|
+
kind: TKind;
|
|
10
|
+
outputSchema: z.ZodType<TOutput>;
|
|
11
|
+
description: string;
|
|
12
|
+
allowedRoles?: string[];
|
|
13
|
+
supportsTimeRange: boolean;
|
|
14
|
+
filterSchema: z.ZodType<TFilters>;
|
|
15
|
+
filterFieldMetadata?: MetricFilterFieldMetadataMap<TFilters>;
|
|
16
|
+
catalog?: TCatalog;
|
|
17
|
+
cacheTtl?: number;
|
|
18
|
+
resolve(args: {
|
|
19
|
+
filters: TFilters;
|
|
20
|
+
ctx: TContext & {
|
|
21
|
+
granularity?: TimeGranularity;
|
|
22
|
+
};
|
|
23
|
+
}): Promise<TOutput>;
|
|
24
|
+
}
|
|
25
|
+
export type MetricDefinitionInput<TKey extends string, TKind extends string, TFilters extends BaseFilters, TOutput, TContext = unknown, TCatalog extends MetricCatalogMetadata | undefined = MetricCatalogMetadata | undefined> = Omit<MetricDefinition<TKey, TKind, TFilters, TOutput, TContext, TCatalog>, 'kind' | 'outputSchema'>;
|
|
26
|
+
export declare function defineMetricWithSchema<TKey extends string, TKind extends string, TFilters extends BaseFilters, TSchema extends z.ZodTypeAny, TContext = unknown, TCatalog extends MetricCatalogMetadata | undefined = MetricCatalogMetadata | undefined>(kind: TKind, outputSchema: TSchema, def: MetricDefinitionInput<TKey, TKind, TFilters, z.output<TSchema>, TContext, TCatalog>): MetricDefinition<TKey, TKind, TFilters, z.output<TSchema>, TContext, TCatalog>;
|
|
27
|
+
export declare function defineKpiMetric<TKey extends string, TFilters extends BaseFilters, TContext = unknown, TCatalog extends MetricCatalogMetadata | undefined = MetricCatalogMetadata | undefined>(def: MetricDefinitionInput<TKey, 'kpi', TFilters, OutputForKind<'kpi'>, TContext, TCatalog>): MetricDefinition<TKey, 'kpi', TFilters, OutputForKind<'kpi'>, TContext, TCatalog>;
|
|
28
|
+
export declare function defineTimeSeriesMetric<TKey extends string, TFilters extends BaseFilters, TContext = unknown, TCatalog extends MetricCatalogMetadata | undefined = MetricCatalogMetadata | undefined>(def: MetricDefinitionInput<TKey, 'timeseries', TFilters, TimeSeriesOutput, TContext, TCatalog>): MetricDefinition<TKey, 'timeseries', TFilters, TimeSeriesOutput, TContext, TCatalog>;
|
|
29
|
+
export declare function defineDistributionMetric<TKey extends string, TFilters extends BaseFilters, TContext = unknown, TCatalog extends MetricCatalogMetadata | undefined = MetricCatalogMetadata | undefined>(def: MetricDefinitionInput<TKey, 'distribution', TFilters, OutputForKind<'distribution'>, TContext, TCatalog>): MetricDefinition<TKey, 'distribution', TFilters, OutputForKind<'distribution'>, TContext, TCatalog>;
|
|
30
|
+
export declare function defineTableMetric<TKey extends string, TFilters extends BaseFilters, TContext = unknown, TCatalog extends MetricCatalogMetadata | undefined = MetricCatalogMetadata | undefined>(def: MetricDefinitionInput<TKey, 'table', TFilters, OutputForKind<'table'>, TContext, TCatalog>): MetricDefinition<TKey, 'table', TFilters, OutputForKind<'table'>, TContext, TCatalog>;
|
|
31
|
+
export declare function defineLeaderboardMetric<TKey extends string, TFilters extends BaseFilters, TContext = unknown, TCatalog extends MetricCatalogMetadata | undefined = MetricCatalogMetadata | undefined>(def: MetricDefinitionInput<TKey, 'leaderboard', TFilters, OutputForKind<'leaderboard'>, TContext, TCatalog>): MetricDefinition<TKey, 'leaderboard', TFilters, OutputForKind<'leaderboard'>, TContext, TCatalog>;
|
|
32
|
+
export declare function definePivotMetric<TKey extends string, TFilters extends BaseFilters, TContext = unknown, TCatalog extends MetricCatalogMetadata | undefined = MetricCatalogMetadata | undefined>(def: MetricDefinitionInput<TKey, 'pivot', TFilters, OutputForKind<'pivot'>, TContext, TCatalog>): MetricDefinition<TKey, 'pivot', TFilters, OutputForKind<'pivot'>, TContext, TCatalog>;
|
|
33
|
+
export declare function getOutputSchema<T extends AnyMetricDefinition>(metric: T): T['outputSchema'] | OutputSchemaForKind<Extract<T['kind'], OutputKind>>;
|
|
34
|
+
export declare function validateMetricOutput<T extends AnyMetricDefinition>(metric: T, output: unknown): Awaited<ReturnType<T['resolve']>>;
|
|
35
|
+
export type AnyMetricDefinition = MetricDefinition<string, string, BaseFilters, unknown, unknown, MetricCatalogMetadata | undefined>;
|
|
36
|
+
export type MetricKey<T extends AnyMetricDefinition> = T['key'];
|
|
37
|
+
export type MetricOutput<T extends AnyMetricDefinition> = Awaited<ReturnType<T['resolve']>>;
|