@objectstack/service-analytics 17.0.0 → 17.2.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 +444 -0
- package/README.md +136 -335
- package/dist/index.cjs +307 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -13
- package/dist/index.d.ts +113 -13
- package/dist/index.js +278 -30
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
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,179 @@ 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
|
-
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
// Count records
|
|
68
|
-
const totalOrders = await analytics.count({
|
|
69
|
-
object: 'order',
|
|
70
|
-
filters: [{ field: 'status', operator: 'eq', value: 'completed' }],
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Sum field values
|
|
74
|
-
const totalRevenue = await analytics.sum({
|
|
75
|
-
object: 'order',
|
|
76
|
-
field: 'amount',
|
|
77
|
-
filters: [{ field: 'created_at', operator: 'gte', value: '2024-01-01' }],
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Calculate average
|
|
81
|
-
const avgOrderValue = await analytics.avg({
|
|
82
|
-
object: 'order',
|
|
83
|
-
field: 'amount',
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Find min/max
|
|
87
|
-
const highestOrder = await analytics.max({
|
|
88
|
-
object: 'order',
|
|
89
|
-
field: 'amount',
|
|
90
|
-
});
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Group By Aggregations
|
|
47
|
+
`LiteKernel.use()` is synchronous; `ObjectKernel.use()` returns a promise — await it there.
|
|
94
48
|
|
|
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
|
-
```
|
|
49
|
+
## Plugin options
|
|
112
50
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Daily revenue for the past 30 days
|
|
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
|
-
});
|
|
133
|
-
|
|
134
|
-
// Result format:
|
|
135
|
-
// [
|
|
136
|
-
// { date: '2024-01-01', revenue: 12500, orders: 45 },
|
|
137
|
-
// { date: '2024-01-02', revenue: 15200, orders: 52 },
|
|
138
|
-
// ]
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### Custom Metrics
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
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
|
-
});
|
|
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.
|
|
155
54
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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`. |
|
|
159
65
|
|
|
160
|
-
|
|
66
|
+
`debug` and `debugSql` are deliberately separate: raising log verbosity must never
|
|
67
|
+
widen what travels to a tenant.
|
|
161
68
|
|
|
162
|
-
|
|
69
|
+
## Service API
|
|
163
70
|
|
|
164
|
-
|
|
165
|
-
|
|
71
|
+
`IAnalyticsService` (from `@objectstack/spec/contracts`) declares four members — two
|
|
72
|
+
required, two optional:
|
|
166
73
|
|
|
167
74
|
```typescript
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
**
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
75
|
+
import type { IAnalyticsService } from '@objectstack/spec/contracts';
|
|
76
|
+
|
|
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)
|
|
81
|
+
```
|
|
82
|
+
|
|
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.
|
|
86
|
+
|
|
87
|
+
### AnalyticsQuery
|
|
88
|
+
|
|
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.
|
|
92
|
+
|
|
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. |
|
|
104
|
+
|
|
105
|
+
There is no `filters` key and no `aggregations` key. `filters` is rejected at the REST
|
|
106
|
+
door with a 400 naming `where`. There is no per-metric filter key either — the cube
|
|
107
|
+
metric's `filters` was removed (#10414: no strategy ever read it); fold a per-metric
|
|
108
|
+
condition into the metric's own `sql` expression, or use an ADR-0021 dataset measure's
|
|
109
|
+
structured `filter`.
|
|
189
110
|
|
|
190
111
|
```typescript
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
groupBy: ['account.industry'],
|
|
112
|
+
const revenueByStatus = await analytics.query({
|
|
113
|
+
cube: 'orders',
|
|
114
|
+
measures: ['orders.total_amount'],
|
|
115
|
+
dimensions: ['orders.status'],
|
|
116
|
+
where: { is_active: true },
|
|
117
|
+
order: { 'orders.total_amount': 'desc' },
|
|
118
|
+
limit: 10,
|
|
199
119
|
});
|
|
120
|
+
// result.rows — Record<string, unknown>[]
|
|
121
|
+
// result.fields — column metadata (name, type, label?, format?, currency?, percentScale?)
|
|
200
122
|
```
|
|
201
123
|
|
|
202
|
-
|
|
203
|
-
- Respects object/field metadata and permissions
|
|
204
|
-
- Handles formula fields and computed values
|
|
205
|
-
- Consistent with ObjectQL query behavior
|
|
124
|
+
## Strategy chain
|
|
206
125
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
- Additional abstraction layer
|
|
126
|
+
`AnalyticsService` delegates to a priority-ordered chain; the first strategy whose
|
|
127
|
+
`canHandle` returns true serves the query.
|
|
210
128
|
|
|
211
|
-
|
|
212
|
-
|
|
129
|
+
| Priority | Strategy | Condition |
|
|
130
|
+
|:---:|:---|:---|
|
|
131
|
+
| 10 | `NativeSQLStrategy` | driver supports raw SQL (`executeRawSql`) |
|
|
132
|
+
| 20 | `ObjectQLStrategy` | driver supports aggregate AST (`executeAggregate`) |
|
|
133
|
+
| 30 | custom strategies, or the internal delegate added when `fallbackService` is set | injected by the host |
|
|
213
134
|
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
135
|
+
`InMemoryStrategy` is **not** built in — it ships from `@objectstack/driver-memory` and
|
|
136
|
+
is injected through `AnalyticsServiceConfig.strategies` (or `fallbackService`).
|
|
231
137
|
|
|
232
|
-
## REST API
|
|
138
|
+
## REST API
|
|
233
139
|
|
|
234
|
-
|
|
140
|
+
Served by the runtime dispatcher's `/analytics` domain when this service occupies the
|
|
141
|
+
slot. These four routes are the whole surface:
|
|
235
142
|
|
|
236
143
|
```
|
|
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
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
## Dashboard Integration
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
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);
|
|
144
|
+
POST /api/v1/analytics/query # execute an AnalyticsQuery
|
|
145
|
+
GET /api/v1/analytics/meta[?cube=] # cube metadata for discovery
|
|
146
|
+
POST /api/v1/analytics/sql # generate SQL without executing (dry-run)
|
|
147
|
+
POST /api/v1/analytics/dataset/query # run a dataset selection (ADR-0021)
|
|
276
148
|
```
|
|
277
149
|
|
|
278
|
-
|
|
150
|
+
`POST /analytics/sql` answers 404 when the slot's occupant does not implement the
|
|
151
|
+
optional `generateSql`.
|
|
279
152
|
|
|
280
|
-
|
|
153
|
+
## Exports
|
|
281
154
|
|
|
282
155
|
```typescript
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// Invalidate cache when data changes
|
|
294
|
-
analytics.invalidateCache('order');
|
|
156
|
+
import {
|
|
157
|
+
AnalyticsService, AnalyticsServicePlugin, CubeRegistry, DatasetExecutor,
|
|
158
|
+
NativeSQLStrategy, ObjectQLStrategy,
|
|
159
|
+
compileDataset, compileScopedFilterToSql,
|
|
160
|
+
combineFilters, evaluateDerivedMeasures, fillEmptyGroups, mergeByDimensions, shiftRange,
|
|
161
|
+
createOrderLabelResolver, pickDisplayField, resolveDimensionLabels, withLabelFetchCache,
|
|
162
|
+
} from '@objectstack/service-analytics';
|
|
295
163
|
```
|
|
296
164
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
});
|
|
165
|
+
Types: `AnalyticsServiceConfig`, `AnalyticsServicePluginOptions`, `AnalyticsStrategy`,
|
|
166
|
+
`StrategyContext`, `AnalyticsDriverCapabilities`, `CompiledDataset`,
|
|
167
|
+
`DatasetCompileOptions`, `DatasetSelection`, `CompareTo`, `DerivedMeasureSpec`,
|
|
168
|
+
`RelationshipResolver`, `RelationshipTarget`, `DimensionLabelDeps`, `FieldMetaLite`,
|
|
169
|
+
`OrderLabelResolver`.
|
|
314
170
|
|
|
315
|
-
|
|
316
|
-
// {
|
|
317
|
-
// current: 125000,
|
|
318
|
-
// comparison: 110000,
|
|
319
|
-
// change: 15000,
|
|
320
|
-
// percentChange: 13.64
|
|
321
|
-
// }
|
|
322
|
-
```
|
|
171
|
+
## Advanced: constructing the service directly
|
|
323
172
|
|
|
324
|
-
|
|
173
|
+
`AnalyticsService` is exported for hosts that wire their own kernel integration.
|
|
174
|
+
`AnalyticsServiceConfig` is the wider surface the plugin builds — it adds `logger`,
|
|
175
|
+
`strategies`, `fallbackService`, `coerceTemporalFilterValue`,
|
|
176
|
+
`coerceTemporalFilterColumn`, `isExternalObject`, `getObjectDatasource`,
|
|
177
|
+
`isRegisteredObject` and the dataset resolvers on top of the plugin options above.
|
|
325
178
|
|
|
326
179
|
```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
|
-
});
|
|
340
|
-
|
|
341
|
-
// Result:
|
|
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
|
-
```
|
|
180
|
+
import { AnalyticsService, CubeRegistry } from '@objectstack/service-analytics';
|
|
352
181
|
|
|
353
|
-
|
|
182
|
+
const registry = new CubeRegistry();
|
|
183
|
+
registry.registerAll([ordersCube]);
|
|
354
184
|
|
|
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
|
-
}
|
|
185
|
+
const service = new AnalyticsService({ cubes: [ordersCube] });
|
|
369
186
|
```
|
|
370
187
|
|
|
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
188
|
## License
|
|
388
189
|
|
|
389
190
|
Apache-2.0. See [LICENSING.md](../../../LICENSING.md).
|
|
@@ -391,5 +192,5 @@ Apache-2.0. See [LICENSING.md](../../../LICENSING.md).
|
|
|
391
192
|
## See Also
|
|
392
193
|
|
|
393
194
|
- [@objectstack/objectql](../../objectql/)
|
|
394
|
-
- [@objectstack/
|
|
395
|
-
- [Analytics Guide](/
|
|
195
|
+
- [@objectstack/driver-memory](../../drivers/driver-memory/) — ships `InMemoryStrategy`
|
|
196
|
+
- [Analytics Guide](https://objectstack.ai/docs/data-modeling/analytics)
|