@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/README.md CHANGED
@@ -1,18 +1,10 @@
1
1
  # @objectstack/service-analytics
2
2
 
3
- Analytics Service for ObjectStack implements `IAnalyticsService` with multi-driver strategy pattern (NativeSQL, ObjectQL, InMemory).
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
- ## Features
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
- ## Basic Usage
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
- ## Configuration
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
- interface AnalyticsServiceConfig {
43
- /** Default execution driver */
44
- defaultDriver?: 'sql' | 'objectql' | 'memory';
45
-
46
- /** Enable query result caching */
47
- enableCaching?: boolean;
48
-
49
- /** Cache TTL in seconds (default: 300) */
50
- cacheTTL?: number;
51
-
52
- /** Maximum result set size for in-memory driver */
53
- maxMemoryResults?: number;
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
- ## Service API
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
- ### Basic Aggregations
47
+ `LiteKernel.use()` is synchronous; `ObjectKernel.use()` returns a promise — await it there.
65
48
 
66
- ```typescript
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
- // 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
- });
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
- // Calculate average
81
- const avgOrderValue = await analytics.avg({
82
- object: 'order',
83
- field: 'amount',
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
- // Find min/max
87
- const highestOrder = await analytics.max({
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
- ```typescript
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
- ### Time Series Analytics
71
+ `IAnalyticsService` (from `@objectstack/spec/contracts`) declares four members — two
72
+ required, two optional:
114
73
 
115
74
  ```typescript
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
- });
75
+ import type { IAnalyticsService } from '@objectstack/spec/contracts';
133
76
 
134
- // Result format:
135
- // [
136
- // { date: '2024-01-01', revenue: 12500, orders: 45 },
137
- // { date: '2024-01-02', revenue: 15200, orders: 52 },
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
- ### Custom Metrics
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
- ```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
- });
155
-
156
- // Query the metric
157
- const mrr = await analytics.getMetric('monthly_recurring_revenue');
158
- ```
87
+ ### AnalyticsQuery
159
88
 
160
- ## Multi-Driver Strategy
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
- ### When to Use Each Driver
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
- #### NativeSQL Driver
165
- **Best for**: Large datasets, complex joins, database-specific optimizations
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 result = await analytics.query({
169
- driver: 'sql',
170
- object: 'order',
171
- aggregations: [{ function: 'sum', field: 'amount' }],
172
- groupBy: ['customer_id'],
173
- having: [{ field: 'sum_amount', operator: 'gt', value: 10000 }],
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
- **Advantages:**
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
- **Limitations:**
183
- - Bypasses ObjectStack metadata layer
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
- #### ObjectQL Driver
188
- **Best for**: Metadata-aware analytics, cross-object aggregations
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
- ```typescript
191
- const result = await analytics.query({
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
- **Advantages:**
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
- **Limitations:**
208
- - Slightly slower than direct SQL
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/count # Count records
238
- POST /api/v1/analytics/sum # Sum field values
239
- POST /api/v1/analytics/avg # Calculate average
240
- POST /api/v1/analytics/min # Find minimum
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
- ## Dashboard Integration
148
+ `POST /analytics/sql` answers 404 when the slot's occupant does not implement the
149
+ optional `generateSql`.
249
150
 
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);
276
- ```
277
-
278
- ## Advanced Features
279
-
280
- ### Query Caching
151
+ ## Exports
281
152
 
282
153
  ```typescript
283
- // Enable caching for expensive queries
284
- const result = await analytics.query({
285
- object: 'order',
286
- aggregations: [{ function: 'sum', field: 'amount' }],
287
- cache: {
288
- enabled: true,
289
- ttl: 600, // 10 minutes
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
- ### Comparative Analytics
163
+ Types: `AnalyticsServiceConfig`, `AnalyticsServicePluginOptions`, `AnalyticsStrategy`,
164
+ `StrategyContext`, `AnalyticsDriverCapabilities`, `CompiledDataset`,
165
+ `DatasetCompileOptions`, `DatasetSelection`, `CompareTo`, `DerivedMeasureSpec`,
166
+ `RelationshipResolver`, `RelationshipTarget`, `DimensionLabelDeps`, `FieldMetaLite`,
167
+ `OrderLabelResolver`.
298
168
 
299
- ```typescript
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
- // Result:
316
- // {
317
- // current: 125000,
318
- // comparison: 110000,
319
- // change: 15000,
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
- // Define a conversion funnel
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
- // 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
- ```
352
-
353
- ## Contract Implementation
180
+ const registry = new CubeRegistry();
181
+ registry.registerAll([ordersCube]);
354
182
 
355
- Implements `IAnalyticsService` from `@objectstack/spec/contracts`:
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/spec/contracts](../../spec/src/contracts/)
395
- - [Analytics Guide](/content/docs/data-modeling/analytics.mdx)
193
+ - [@objectstack/driver-memory](../../drivers/driver-memory/) — ships `InMemoryStrategy`
194
+ - [Analytics Guide](https://docs.objectstack.ai/docs/data-modeling/analytics)