@objectstack/service-analytics 17.0.0-rc.6 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4823 -1
- package/README.md +127 -328
- package/dist/index.cjs +356 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +333 -17
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
# @objectstack/service-analytics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The shipped provider for the kernel's **`analytics`** service slot — a cube/dataset
|
|
4
|
+
query engine implementing `IAnalyticsService` over a priority-ordered strategy chain.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- **Multi-Driver Architecture**: Choose the right execution strategy for your analytics queries
|
|
8
|
-
- **NativeSQL**: Direct SQL execution for maximum performance on large datasets
|
|
9
|
-
- **ObjectQL**: Leverage ObjectStack's query engine for metadata-aware analytics
|
|
10
|
-
- **InMemory**: Fast aggregations on small datasets without database round-trips
|
|
11
|
-
- **Aggregation Functions**: SUM, COUNT, AVG, MIN, MAX, GROUP BY, HAVING
|
|
12
|
-
- **Time Series Analysis**: Time-based aggregations and grouping
|
|
13
|
-
- **Custom Metrics**: Define and track custom business metrics
|
|
14
|
-
- **Dashboard Integration**: Auto-generated REST endpoints for visualization
|
|
15
|
-
- **Type-Safe**: Full TypeScript support with inferred result types
|
|
6
|
+
Slot criticality: `optional` (`ServiceRequirementDef` in `@objectstack/spec/system`).
|
|
7
|
+
Without it, `/api/v1/analytics/*` answers 404 rather than degrading.
|
|
16
8
|
|
|
17
9
|
## Installation
|
|
18
10
|
|
|
@@ -20,370 +12,177 @@ Analytics Service for ObjectStack — implements `IAnalyticsService` with multi-
|
|
|
20
12
|
pnpm add @objectstack/service-analytics
|
|
21
13
|
```
|
|
22
14
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
import { defineStack } from '@objectstack/spec';
|
|
27
|
-
import { ServiceAnalytics } from '@objectstack/service-analytics';
|
|
28
|
-
|
|
29
|
-
const stack = defineStack({
|
|
30
|
-
services: [
|
|
31
|
-
ServiceAnalytics.configure({
|
|
32
|
-
defaultDriver: 'objectql', // or 'sql', 'memory'
|
|
33
|
-
enableCaching: true,
|
|
34
|
-
}),
|
|
35
|
-
],
|
|
36
|
-
});
|
|
37
|
-
```
|
|
15
|
+
## Usage
|
|
38
16
|
|
|
39
|
-
|
|
17
|
+
The entry point is the kernel plugin `AnalyticsServicePlugin`. Construct it and hand
|
|
18
|
+
it to the kernel; it registers the service under `'analytics'` during `init`.
|
|
40
19
|
|
|
41
20
|
```typescript
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
21
|
+
import { LiteKernel } from '@objectstack/core';
|
|
22
|
+
import type { Cube } from '@objectstack/spec/data';
|
|
23
|
+
import type { IAnalyticsService } from '@objectstack/spec/contracts';
|
|
24
|
+
import { AnalyticsServicePlugin } from '@objectstack/service-analytics';
|
|
25
|
+
|
|
26
|
+
const ordersCube: Cube = {
|
|
27
|
+
name: 'orders',
|
|
28
|
+
title: 'Orders',
|
|
29
|
+
sql: 'orders',
|
|
30
|
+
measures: {
|
|
31
|
+
count: { name: 'count', label: 'Count', type: 'count', sql: '*' },
|
|
32
|
+
total_amount: { name: 'total_amount', label: 'Total Amount', type: 'sum', sql: 'amount' },
|
|
33
|
+
},
|
|
34
|
+
dimensions: {
|
|
35
|
+
status: { name: 'status', label: 'Status', type: 'string', sql: 'status' },
|
|
36
|
+
},
|
|
37
|
+
};
|
|
56
38
|
|
|
57
|
-
|
|
39
|
+
const kernel = new LiteKernel();
|
|
40
|
+
kernel.use(new AnalyticsServicePlugin({ cubes: [ordersCube] }));
|
|
41
|
+
await kernel.bootstrap();
|
|
58
42
|
|
|
59
|
-
```typescript
|
|
60
|
-
// Get analytics service from kernel
|
|
61
43
|
const analytics = kernel.getService<IAnalyticsService>('analytics');
|
|
44
|
+
const result = await analytics.query({ cube: 'orders', measures: ['orders.count'] });
|
|
62
45
|
```
|
|
63
46
|
|
|
64
|
-
|
|
47
|
+
`LiteKernel.use()` is synchronous; `ObjectKernel.use()` returns a promise — await it there.
|
|
65
48
|
|
|
66
|
-
|
|
67
|
-
// Count records
|
|
68
|
-
const totalOrders = await analytics.count({
|
|
69
|
-
object: 'order',
|
|
70
|
-
filters: [{ field: 'status', operator: 'eq', value: 'completed' }],
|
|
71
|
-
});
|
|
49
|
+
## Plugin options
|
|
72
50
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
field: 'amount',
|
|
77
|
-
filters: [{ field: 'created_at', operator: 'gte', value: '2024-01-01' }],
|
|
78
|
-
});
|
|
51
|
+
Every field of `AnalyticsServicePluginOptions` is optional. The plugin bridges the
|
|
52
|
+
host's engine into `AnalyticsServiceConfig`; anything left unset falls back to what
|
|
53
|
+
the plugin can auto-discover from the kernel.
|
|
79
54
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
55
|
+
| Option | Type | Default | Purpose |
|
|
56
|
+
|:---|:---|:---|:---|
|
|
57
|
+
| `cubes` | `Cube[]` | none | Cube definitions registered at init. |
|
|
58
|
+
| `queryCapabilities` | `(cubeName: string) => AnalyticsDriverCapabilities` | in-memory only | Which execution paths a cube's backing driver supports. |
|
|
59
|
+
| `executeRawSql` | `(objectName, sql, params) => Promise<Record<string, unknown>[]>` | auto-bridged to the ObjectQL engine | Enables `NativeSQLStrategy`. |
|
|
60
|
+
| `executeAggregate` | `(objectName, options) => Promise<Record<string, unknown>[]>` | auto-bridged to the ObjectQL engine | Enables `ObjectQLStrategy`. |
|
|
61
|
+
| `getReadScope` | `(objectName, context?) => FilterCondition \| null \| undefined \| Promise<…>` | auto-bridges to a registered `'security'` service exposing `getReadFilter` | Per-object tenant/RLS read scope (ADR-0021 D-C). |
|
|
62
|
+
| `getAllowedRelationships` | `(cubeName: string) => Set<string> \| undefined` | supplied by compiled datasets | Join allowlist per cube. |
|
|
63
|
+
| `debug` | `boolean` | `false` | Server-side log verbosity only. |
|
|
64
|
+
| `debugSql` | `boolean` | development only (`NODE_ENV === 'development'`) | Echo the executed statement back to callers in `AnalyticsResult.sql`. |
|
|
85
65
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
object: 'order',
|
|
89
|
-
field: 'amount',
|
|
90
|
-
});
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Group By Aggregations
|
|
66
|
+
`debug` and `debugSql` are deliberately separate: raising log verbosity must never
|
|
67
|
+
widen what travels to a tenant.
|
|
94
68
|
|
|
95
|
-
|
|
96
|
-
// Revenue by product category
|
|
97
|
-
const revenueByCategory = await analytics.groupBy({
|
|
98
|
-
object: 'order_item',
|
|
99
|
-
groupBy: ['product.category'],
|
|
100
|
-
aggregations: [
|
|
101
|
-
{ function: 'sum', field: 'total', as: 'revenue' },
|
|
102
|
-
{ function: 'count', as: 'order_count' },
|
|
103
|
-
],
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Result format:
|
|
107
|
-
// [
|
|
108
|
-
// { category: 'Electronics', revenue: 125000, order_count: 342 },
|
|
109
|
-
// { category: 'Clothing', revenue: 98000, order_count: 567 },
|
|
110
|
-
// ]
|
|
111
|
-
```
|
|
69
|
+
## Service API
|
|
112
70
|
|
|
113
|
-
|
|
71
|
+
`IAnalyticsService` (from `@objectstack/spec/contracts`) declares four members — two
|
|
72
|
+
required, two optional:
|
|
114
73
|
|
|
115
74
|
```typescript
|
|
116
|
-
|
|
117
|
-
const dailyRevenue = await analytics.timeSeries({
|
|
118
|
-
object: 'order',
|
|
119
|
-
dateField: 'created_at',
|
|
120
|
-
interval: 'day',
|
|
121
|
-
aggregations: [
|
|
122
|
-
{ function: 'sum', field: 'amount', as: 'revenue' },
|
|
123
|
-
{ function: 'count', as: 'orders' },
|
|
124
|
-
],
|
|
125
|
-
filters: [
|
|
126
|
-
{
|
|
127
|
-
field: 'created_at',
|
|
128
|
-
operator: 'gte',
|
|
129
|
-
value: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
|
130
|
-
},
|
|
131
|
-
],
|
|
132
|
-
});
|
|
75
|
+
import type { IAnalyticsService } from '@objectstack/spec/contracts';
|
|
133
76
|
|
|
134
|
-
//
|
|
135
|
-
// [
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
// ]
|
|
77
|
+
// query(query, context?) -> Promise<AnalyticsResult> (required)
|
|
78
|
+
// getMeta(cubeName?) -> Promise<CubeMeta[]> (required)
|
|
79
|
+
// generateSql?(query, context?) -> Promise<{ sql, params }> (optional)
|
|
80
|
+
// queryDataset?(dataset, selection, context?, options?) (optional)
|
|
139
81
|
```
|
|
140
82
|
|
|
141
|
-
|
|
83
|
+
This package implements all four. Pass the caller's `ExecutionContext` as the second
|
|
84
|
+
argument: without it the per-object read scope resolves to no filter and the query
|
|
85
|
+
runs unscoped.
|
|
142
86
|
|
|
143
|
-
|
|
144
|
-
// Define a metric
|
|
145
|
-
analytics.defineMetric({
|
|
146
|
-
name: 'monthly_recurring_revenue',
|
|
147
|
-
description: 'MRR from active subscriptions',
|
|
148
|
-
calculation: {
|
|
149
|
-
object: 'subscription',
|
|
150
|
-
aggregation: 'sum',
|
|
151
|
-
field: 'amount',
|
|
152
|
-
filters: [{ field: 'status', operator: 'eq', value: 'active' }],
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Query the metric
|
|
157
|
-
const mrr = await analytics.getMetric('monthly_recurring_revenue');
|
|
158
|
-
```
|
|
87
|
+
### AnalyticsQuery
|
|
159
88
|
|
|
160
|
-
|
|
89
|
+
`AnalyticsQuery` is a **strict** schema (`AnalyticsQuerySchema`, `@objectstack/spec/data`)
|
|
90
|
+
with exactly these fields; `measures` is the only required one, and an undeclared key
|
|
91
|
+
is rejected rather than dropped.
|
|
161
92
|
|
|
162
|
-
|
|
93
|
+
| Field | Type | Notes |
|
|
94
|
+
|:---|:---|:---|
|
|
95
|
+
| `cube` | `string?` | Optional when supplied by the request wrapper. |
|
|
96
|
+
| `measures` | `string[]` | Required. |
|
|
97
|
+
| `dimensions` | `string[]?` | |
|
|
98
|
+
| `where` | `FilterCondition?` | Canonical Query DSL filter — the same shape `find()` takes. |
|
|
99
|
+
| `timeDimensions` | `{ dimension, granularity?, dateRange? }[]?` | Also strict per item. |
|
|
100
|
+
| `order` | `Record<string, 'asc' \| 'desc'>?` | |
|
|
101
|
+
| `limit` | `number?` | |
|
|
102
|
+
| `offset` | `number?` | |
|
|
103
|
+
| `timezone` | `string?` | IANA name. No default — an absent timezone means the engine resolves it. |
|
|
163
104
|
|
|
164
|
-
|
|
165
|
-
|
|
105
|
+
There is no `filters` key and no `aggregations` key. `filters` is rejected at the REST
|
|
106
|
+
door with a 400 naming `where`; per-metric filtering lives on the cube metric's own
|
|
107
|
+
`filters`.
|
|
166
108
|
|
|
167
109
|
```typescript
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
110
|
+
const revenueByStatus = await analytics.query({
|
|
111
|
+
cube: 'orders',
|
|
112
|
+
measures: ['orders.total_amount'],
|
|
113
|
+
dimensions: ['orders.status'],
|
|
114
|
+
where: { is_active: true },
|
|
115
|
+
order: { 'orders.total_amount': 'desc' },
|
|
116
|
+
limit: 10,
|
|
174
117
|
});
|
|
118
|
+
// result.rows — Record<string, unknown>[]
|
|
119
|
+
// result.fields — column metadata (name, type, label?, format?, currency?, percentScale?)
|
|
175
120
|
```
|
|
176
121
|
|
|
177
|
-
|
|
178
|
-
- Direct SQL execution for maximum performance
|
|
179
|
-
- Leverages database indexes and query optimization
|
|
180
|
-
- Handles millions of records efficiently
|
|
122
|
+
## Strategy chain
|
|
181
123
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
- May miss field-level transformations
|
|
185
|
-
- Less portable across databases
|
|
124
|
+
`AnalyticsService` delegates to a priority-ordered chain; the first strategy whose
|
|
125
|
+
`canHandle` returns true serves the query.
|
|
186
126
|
|
|
187
|
-
|
|
188
|
-
|
|
127
|
+
| Priority | Strategy | Condition |
|
|
128
|
+
|:---:|:---|:---|
|
|
129
|
+
| 10 | `NativeSQLStrategy` | driver supports raw SQL (`executeRawSql`) |
|
|
130
|
+
| 20 | `ObjectQLStrategy` | driver supports aggregate AST (`executeAggregate`) |
|
|
131
|
+
| 30 | custom strategies, or the internal delegate added when `fallbackService` is set | injected by the host |
|
|
189
132
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
driver: 'objectql',
|
|
193
|
-
object: 'opportunity',
|
|
194
|
-
aggregations: [
|
|
195
|
-
{ function: 'sum', field: 'amount' },
|
|
196
|
-
{ function: 'count' },
|
|
197
|
-
],
|
|
198
|
-
groupBy: ['account.industry'],
|
|
199
|
-
});
|
|
200
|
-
```
|
|
133
|
+
`InMemoryStrategy` is **not** built in — it ships from `@objectstack/driver-memory` and
|
|
134
|
+
is injected through `AnalyticsServiceConfig.strategies` (or `fallbackService`).
|
|
201
135
|
|
|
202
|
-
|
|
203
|
-
- Respects object/field metadata and permissions
|
|
204
|
-
- Handles formula fields and computed values
|
|
205
|
-
- Consistent with ObjectQL query behavior
|
|
136
|
+
## REST API
|
|
206
137
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
- Additional abstraction layer
|
|
210
|
-
|
|
211
|
-
#### InMemory Driver
|
|
212
|
-
**Best for**: Small datasets, pre-filtered results, real-time dashboards
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
const result = await analytics.query({
|
|
216
|
-
driver: 'memory',
|
|
217
|
-
object: 'task',
|
|
218
|
-
aggregations: [{ function: 'count' }],
|
|
219
|
-
groupBy: ['status'],
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
**Advantages:**
|
|
224
|
-
- Zero database round-trips for cached data
|
|
225
|
-
- Instant results for small datasets
|
|
226
|
-
- Useful for client-side analytics
|
|
227
|
-
|
|
228
|
-
**Limitations:**
|
|
229
|
-
- Limited to `maxMemoryResults` (default: 10,000)
|
|
230
|
-
- Requires data to be loaded into memory first
|
|
231
|
-
|
|
232
|
-
## REST API Endpoints
|
|
233
|
-
|
|
234
|
-
When used with `@objectstack/rest`:
|
|
138
|
+
Served by the runtime dispatcher's `/analytics` domain when this service occupies the
|
|
139
|
+
slot. These four routes are the whole surface:
|
|
235
140
|
|
|
236
141
|
```
|
|
237
|
-
POST /api/v1/analytics/
|
|
238
|
-
|
|
239
|
-
POST /api/v1/analytics/
|
|
240
|
-
POST /api/v1/analytics/
|
|
241
|
-
POST /api/v1/analytics/max # Find maximum
|
|
242
|
-
POST /api/v1/analytics/group-by # Group by aggregation
|
|
243
|
-
POST /api/v1/analytics/time-series # Time series analysis
|
|
244
|
-
GET /api/v1/analytics/metrics # List custom metrics
|
|
245
|
-
GET /api/v1/analytics/metrics/:name # Get metric value
|
|
142
|
+
POST /api/v1/analytics/query # execute an AnalyticsQuery
|
|
143
|
+
GET /api/v1/analytics/meta[?cube=] # cube metadata for discovery
|
|
144
|
+
POST /api/v1/analytics/sql # generate SQL without executing (dry-run)
|
|
145
|
+
POST /api/v1/analytics/dataset/query # run a dataset selection (ADR-0021)
|
|
246
146
|
```
|
|
247
147
|
|
|
248
|
-
|
|
148
|
+
`POST /analytics/sql` answers 404 when the slot's occupant does not implement the
|
|
149
|
+
optional `generateSql`.
|
|
249
150
|
|
|
250
|
-
|
|
251
|
-
// Define a dashboard with multiple metrics
|
|
252
|
-
const salesDashboard = {
|
|
253
|
-
title: 'Sales Dashboard',
|
|
254
|
-
metrics: [
|
|
255
|
-
{
|
|
256
|
-
title: 'Total Revenue',
|
|
257
|
-
query: {
|
|
258
|
-
object: 'order',
|
|
259
|
-
aggregation: 'sum',
|
|
260
|
-
field: 'amount',
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
title: 'Revenue by Region',
|
|
265
|
-
query: {
|
|
266
|
-
object: 'order',
|
|
267
|
-
aggregations: [{ function: 'sum', field: 'amount', as: 'revenue' }],
|
|
268
|
-
groupBy: ['account.billing_region'],
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
],
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
// Execute all dashboard queries
|
|
275
|
-
const dashboardData = await analytics.executeDashboard(salesDashboard);
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
## Advanced Features
|
|
279
|
-
|
|
280
|
-
### Query Caching
|
|
151
|
+
## Exports
|
|
281
152
|
|
|
282
153
|
```typescript
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// Invalidate cache when data changes
|
|
294
|
-
analytics.invalidateCache('order');
|
|
154
|
+
import {
|
|
155
|
+
AnalyticsService, AnalyticsServicePlugin, CubeRegistry, DatasetExecutor,
|
|
156
|
+
NativeSQLStrategy, ObjectQLStrategy,
|
|
157
|
+
compileDataset, compileScopedFilterToSql,
|
|
158
|
+
combineFilters, evaluateDerivedMeasures, fillEmptyGroups, mergeByDimensions, shiftRange,
|
|
159
|
+
createOrderLabelResolver, pickDisplayField, resolveDimensionLabels, withLabelFetchCache,
|
|
160
|
+
} from '@objectstack/service-analytics';
|
|
295
161
|
```
|
|
296
162
|
|
|
297
|
-
|
|
163
|
+
Types: `AnalyticsServiceConfig`, `AnalyticsServicePluginOptions`, `AnalyticsStrategy`,
|
|
164
|
+
`StrategyContext`, `AnalyticsDriverCapabilities`, `CompiledDataset`,
|
|
165
|
+
`DatasetCompileOptions`, `DatasetSelection`, `CompareTo`, `DerivedMeasureSpec`,
|
|
166
|
+
`RelationshipResolver`, `RelationshipTarget`, `DimensionLabelDeps`, `FieldMetaLite`,
|
|
167
|
+
`OrderLabelResolver`.
|
|
298
168
|
|
|
299
|
-
|
|
300
|
-
// Compare current vs. previous period
|
|
301
|
-
const comparison = await analytics.compare({
|
|
302
|
-
object: 'order',
|
|
303
|
-
aggregation: 'sum',
|
|
304
|
-
field: 'amount',
|
|
305
|
-
currentPeriod: {
|
|
306
|
-
start: '2024-01-01',
|
|
307
|
-
end: '2024-01-31',
|
|
308
|
-
},
|
|
309
|
-
comparisonPeriod: {
|
|
310
|
-
start: '2023-12-01',
|
|
311
|
-
end: '2023-12-31',
|
|
312
|
-
},
|
|
313
|
-
});
|
|
169
|
+
## Advanced: constructing the service directly
|
|
314
170
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// percentChange: 13.64
|
|
321
|
-
// }
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
### Funnel Analysis
|
|
171
|
+
`AnalyticsService` is exported for hosts that wire their own kernel integration.
|
|
172
|
+
`AnalyticsServiceConfig` is the wider surface the plugin builds — it adds `logger`,
|
|
173
|
+
`strategies`, `fallbackService`, `coerceTemporalFilterValue`,
|
|
174
|
+
`coerceTemporalFilterColumn`, `isExternalObject`, `getObjectDatasource`,
|
|
175
|
+
`isRegisteredObject` and the dataset resolvers on top of the plugin options above.
|
|
325
176
|
|
|
326
177
|
```typescript
|
|
327
|
-
|
|
328
|
-
const funnel = await analytics.funnel({
|
|
329
|
-
steps: [
|
|
330
|
-
{ object: 'lead', stage: 'new' },
|
|
331
|
-
{ object: 'lead', stage: 'qualified' },
|
|
332
|
-
{ object: 'opportunity', stage: 'proposal' },
|
|
333
|
-
{ object: 'opportunity', stage: 'closed_won' },
|
|
334
|
-
],
|
|
335
|
-
dateRange: {
|
|
336
|
-
start: '2024-01-01',
|
|
337
|
-
end: '2024-01-31',
|
|
338
|
-
},
|
|
339
|
-
});
|
|
178
|
+
import { AnalyticsService, CubeRegistry } from '@objectstack/service-analytics';
|
|
340
179
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
// steps: [
|
|
344
|
-
// { stage: 'new', count: 1000, percentage: 100 },
|
|
345
|
-
// { stage: 'qualified', count: 450, percentage: 45 },
|
|
346
|
-
// { stage: 'proposal', count: 200, percentage: 20 },
|
|
347
|
-
// { stage: 'closed_won', count: 75, percentage: 7.5 },
|
|
348
|
-
// ],
|
|
349
|
-
// overallConversion: 0.075
|
|
350
|
-
// }
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
## Contract Implementation
|
|
180
|
+
const registry = new CubeRegistry();
|
|
181
|
+
registry.registerAll([ordersCube]);
|
|
354
182
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
```typescript
|
|
358
|
-
interface IAnalyticsService {
|
|
359
|
-
count(options: CountOptions): Promise<number>;
|
|
360
|
-
sum(options: AggregationOptions): Promise<number>;
|
|
361
|
-
avg(options: AggregationOptions): Promise<number>;
|
|
362
|
-
min(options: AggregationOptions): Promise<number>;
|
|
363
|
-
max(options: AggregationOptions): Promise<number>;
|
|
364
|
-
groupBy(options: GroupByOptions): Promise<AggregationResult[]>;
|
|
365
|
-
timeSeries(options: TimeSeriesOptions): Promise<TimeSeriesResult[]>;
|
|
366
|
-
defineMetric(metric: MetricDefinition): void;
|
|
367
|
-
getMetric(name: string): Promise<number | AggregationResult[]>;
|
|
368
|
-
}
|
|
183
|
+
const service = new AnalyticsService({ cubes: [ordersCube] });
|
|
369
184
|
```
|
|
370
185
|
|
|
371
|
-
## Performance Optimization
|
|
372
|
-
|
|
373
|
-
1. **Choose the Right Driver**: Use SQL for large datasets, InMemory for small
|
|
374
|
-
2. **Enable Caching**: Cache expensive queries with appropriate TTL
|
|
375
|
-
3. **Optimize Filters**: Filter early to reduce dataset size
|
|
376
|
-
4. **Use Indexes**: Ensure database indexes on frequently queried fields
|
|
377
|
-
5. **Batch Queries**: Execute multiple metrics in a single dashboard query
|
|
378
|
-
|
|
379
|
-
## Best Practices
|
|
380
|
-
|
|
381
|
-
1. **Driver Selection**: Start with ObjectQL, optimize to SQL if needed
|
|
382
|
-
2. **Metric Definitions**: Define reusable metrics for consistency
|
|
383
|
-
3. **Cache Strategy**: Cache expensive queries, invalidate on data changes
|
|
384
|
-
4. **Time Series**: Use appropriate intervals (hour/day/week/month)
|
|
385
|
-
5. **Group By**: Limit grouping dimensions to avoid explosion of result sets
|
|
386
|
-
|
|
387
186
|
## License
|
|
388
187
|
|
|
389
188
|
Apache-2.0. See [LICENSING.md](../../../LICENSING.md).
|
|
@@ -391,5 +190,5 @@ Apache-2.0. See [LICENSING.md](../../../LICENSING.md).
|
|
|
391
190
|
## See Also
|
|
392
191
|
|
|
393
192
|
- [@objectstack/objectql](../../objectql/)
|
|
394
|
-
- [@objectstack/
|
|
395
|
-
- [Analytics Guide](/
|
|
193
|
+
- [@objectstack/driver-memory](../../drivers/driver-memory/) — ships `InMemoryStrategy`
|
|
194
|
+
- [Analytics Guide](https://docs.objectstack.ai/docs/data-modeling/analytics)
|