@objectstack/service-analytics 4.0.4 → 4.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.
@@ -1,147 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { Cube } from '@objectstack/spec/data';
4
-
5
- /**
6
- * CubeRegistry — Central registry for analytics cube definitions.
7
- *
8
- * Cubes can be registered from two sources:
9
- * 1. **Manifest definitions** — Explicit cube definitions in `objectstack.config.ts`.
10
- * 2. **Object schema inference** — Auto-generated cubes from ObjectQL object schemas.
11
- *
12
- * The registry is the single source of truth for cube metadata discovery
13
- * (used by `getMeta()` and the strategy chain).
14
- */
15
- export class CubeRegistry {
16
- private cubes = new Map<string, Cube>();
17
-
18
- /** Register a single cube definition. Overwrites if name already exists. */
19
- register(cube: Cube): void {
20
- this.cubes.set(cube.name, cube);
21
- }
22
-
23
- /** Register multiple cube definitions at once. */
24
- registerAll(cubes: Cube[]): void {
25
- for (const cube of cubes) {
26
- this.register(cube);
27
- }
28
- }
29
-
30
- /** Get a cube definition by name. */
31
- get(name: string): Cube | undefined {
32
- return this.cubes.get(name);
33
- }
34
-
35
- /** Check if a cube is registered. */
36
- has(name: string): boolean {
37
- return this.cubes.has(name);
38
- }
39
-
40
- /** Return all registered cubes. */
41
- getAll(): Cube[] {
42
- return Array.from(this.cubes.values());
43
- }
44
-
45
- /** Return all cube names. */
46
- names(): string[] {
47
- return Array.from(this.cubes.keys());
48
- }
49
-
50
- /** Number of registered cubes. */
51
- get size(): number {
52
- return this.cubes.size;
53
- }
54
-
55
- /** Remove all cubes. */
56
- clear(): void {
57
- this.cubes.clear();
58
- }
59
-
60
- /**
61
- * Auto-generate a cube definition from an object schema.
62
- *
63
- * Heuristic rules:
64
- * - `number` fields → `sum`, `avg`, `min`, `max` measures
65
- * - `boolean` fields → `count` measure (count where true)
66
- * - All non-computed fields → dimensions
67
- * - `date`/`datetime` fields → time dimensions with standard granularities
68
- * - A default `count` measure is always added
69
- *
70
- * @param objectName - The snake_case object name (used as table/cube name)
71
- * @param fields - Array of field descriptors `{ name, type, label? }`
72
- */
73
- inferFromObject(
74
- objectName: string,
75
- fields: Array<{ name: string; type: string; label?: string }>,
76
- ): Cube {
77
- const measures: Record<string, any> = {
78
- count: {
79
- name: 'count',
80
- label: 'Count',
81
- type: 'count',
82
- sql: '*',
83
- },
84
- };
85
- const dimensions: Record<string, any> = {};
86
-
87
- for (const field of fields) {
88
- const label = field.label || field.name;
89
-
90
- // All fields become dimensions
91
- const dimType = this.fieldTypeToDimensionType(field.type);
92
- dimensions[field.name] = {
93
- name: field.name,
94
- label,
95
- type: dimType,
96
- sql: field.name,
97
- ...(dimType === 'time'
98
- ? { granularities: ['day', 'week', 'month', 'quarter', 'year'] }
99
- : {}),
100
- };
101
-
102
- // Numeric fields also become aggregation measures
103
- if (field.type === 'number' || field.type === 'currency' || field.type === 'percent') {
104
- measures[`${field.name}_sum`] = {
105
- name: `${field.name}_sum`,
106
- label: `${label} (Sum)`,
107
- type: 'sum',
108
- sql: field.name,
109
- };
110
- measures[`${field.name}_avg`] = {
111
- name: `${field.name}_avg`,
112
- label: `${label} (Avg)`,
113
- type: 'avg',
114
- sql: field.name,
115
- };
116
- }
117
- }
118
-
119
- const cube: Cube = {
120
- name: objectName,
121
- title: objectName,
122
- sql: objectName,
123
- measures,
124
- dimensions,
125
- public: false,
126
- };
127
-
128
- this.register(cube);
129
- return cube;
130
- }
131
-
132
- private fieldTypeToDimensionType(fieldType: string): string {
133
- switch (fieldType) {
134
- case 'number':
135
- case 'currency':
136
- case 'percent':
137
- return 'number';
138
- case 'boolean':
139
- return 'boolean';
140
- case 'date':
141
- case 'datetime':
142
- return 'time';
143
- default:
144
- return 'string';
145
- }
146
- }
147
- }
package/src/index.ts DELETED
@@ -1,19 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- // Core service
4
- export { AnalyticsService } from './analytics-service.js';
5
- export type { AnalyticsServiceConfig } from './analytics-service.js';
6
-
7
- // Kernel plugin
8
- export { AnalyticsServicePlugin } from './plugin.js';
9
- export type { AnalyticsServicePluginOptions } from './plugin.js';
10
-
11
- // Cube registry
12
- export { CubeRegistry } from './cube-registry.js';
13
-
14
- // Strategies
15
- export { NativeSQLStrategy } from './strategies/native-sql-strategy.js';
16
- export { ObjectQLStrategy } from './strategies/objectql-strategy.js';
17
- export type { AnalyticsStrategy, StrategyContext, DriverCapabilities } from './strategies/types.js';
18
-
19
- // Note: InMemoryStrategy is exported from @objectstack/driver-memory
package/src/plugin.ts DELETED
@@ -1,133 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { Plugin, PluginContext } from '@objectstack/core';
4
- import type { Cube } from '@objectstack/spec/data';
5
- import type { IAnalyticsService } from '@objectstack/spec/contracts';
6
- import { AnalyticsService } from './analytics-service.js';
7
- import type { AnalyticsServiceConfig } from './analytics-service.js';
8
- import type { DriverCapabilities } from './strategies/types.js';
9
-
10
- /**
11
- * Configuration for AnalyticsServicePlugin.
12
- */
13
- export interface AnalyticsServicePluginOptions {
14
- /** Pre-defined cube definitions (from manifest). */
15
- cubes?: Cube[];
16
- /**
17
- * Probe driver capabilities for a given cube.
18
- * When omitted, defaults to in-memory only.
19
- */
20
- queryCapabilities?: (cubeName: string) => DriverCapabilities;
21
- /**
22
- * Execute raw SQL on a driver. Enables NativeSQLStrategy.
23
- */
24
- executeRawSql?: (objectName: string, sql: string, params: unknown[]) => Promise<Record<string, unknown>[]>;
25
- /**
26
- * Execute ObjectQL aggregate. Enables ObjectQLStrategy.
27
- */
28
- executeAggregate?: (objectName: string, options: {
29
- groupBy?: string[];
30
- aggregations?: Array<{ field: string; method: string; alias: string }>;
31
- filter?: Record<string, unknown>;
32
- }) => Promise<Record<string, unknown>[]>;
33
- /** Enable debug logging. */
34
- debug?: boolean;
35
- }
36
-
37
- /**
38
- * AnalyticsServicePlugin — Kernel plugin for multi-driver analytics.
39
- *
40
- * Lifecycle:
41
- * 1. **init** — Creates `AnalyticsService`, registers as `'analytics'` service.
42
- * If an existing analytics service is already registered (e.g. MemoryAnalyticsService
43
- * from dev-plugin), it is captured as the `fallbackService`.
44
- * 2. **start** — Triggers `'analytics:ready'` hook so other plugins can
45
- * register cubes or extend the service.
46
- * 3. **destroy** — Cleans up references.
47
- *
48
- * @example
49
- * ```ts
50
- * import { LiteKernel } from '@objectstack/core';
51
- * import { AnalyticsServicePlugin } from '@objectstack/service-analytics';
52
- *
53
- * const kernel = new LiteKernel();
54
- * kernel.use(new AnalyticsServicePlugin({
55
- * cubes: [ordersCube],
56
- * queryCapabilities: (cube) => ({ nativeSql: true, objectqlAggregate: true, inMemory: false }),
57
- * executeRawSql: async (obj, sql, params) => pgPool.query(sql, params).then(r => r.rows),
58
- * }));
59
- * await kernel.bootstrap();
60
- *
61
- * const analytics = kernel.getService<IAnalyticsService>('analytics');
62
- * const result = await analytics.query({ cube: 'orders', measures: ['orders.count'] });
63
- * ```
64
- */
65
- export class AnalyticsServicePlugin implements Plugin {
66
- name = 'com.objectstack.service-analytics';
67
- version = '1.0.0';
68
- type = 'standard' as const;
69
- dependencies: string[] = [];
70
-
71
- private service?: AnalyticsService;
72
- private readonly options: AnalyticsServicePluginOptions;
73
-
74
- constructor(options: AnalyticsServicePluginOptions = {}) {
75
- this.options = options;
76
- }
77
-
78
- async init(ctx: PluginContext): Promise<void> {
79
- // Check if there is an existing analytics service (e.g. from dev-plugin)
80
- let fallbackService: IAnalyticsService | undefined;
81
- try {
82
- const existing = ctx.getService<IAnalyticsService>('analytics');
83
- if (existing && typeof existing.query === 'function') {
84
- fallbackService = existing;
85
- ctx.logger.debug('[Analytics] Found existing analytics service, using as fallback');
86
- }
87
- } catch {
88
- // No existing service — that's fine
89
- }
90
-
91
- const config: AnalyticsServiceConfig = {
92
- cubes: this.options.cubes,
93
- logger: ctx.logger,
94
- queryCapabilities: this.options.queryCapabilities,
95
- executeRawSql: this.options.executeRawSql,
96
- executeAggregate: this.options.executeAggregate,
97
- fallbackService,
98
- };
99
-
100
- this.service = new AnalyticsService(config);
101
-
102
- // Register or replace the analytics service
103
- if (fallbackService) {
104
- ctx.replaceService('analytics', this.service);
105
- } else {
106
- ctx.registerService('analytics', this.service);
107
- }
108
-
109
- if (this.options.debug) {
110
- ctx.hook('analytics:beforeQuery', async (query: unknown) => {
111
- ctx.logger.debug('[Analytics] Before query', { query });
112
- });
113
- }
114
-
115
- ctx.logger.info('[Analytics] Service initialized');
116
- }
117
-
118
- async start(ctx: PluginContext): Promise<void> {
119
- if (!this.service) return;
120
-
121
- // Notify other plugins that analytics is ready
122
- await ctx.trigger('analytics:ready', this.service);
123
-
124
- ctx.logger.info(
125
- `[Analytics] Service started with ${this.service.cubeRegistry.size} cubes: ` +
126
- `${this.service.cubeRegistry.names().join(', ') || '(none)'}`,
127
- );
128
- }
129
-
130
- async destroy(): Promise<void> {
131
- this.service = undefined;
132
- }
133
- }
@@ -1,184 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';
4
- import type { Cube } from '@objectstack/spec/data';
5
- import type { AnalyticsStrategy, StrategyContext } from './types.js';
6
-
7
- /**
8
- * NativeSQLStrategy — Priority 1
9
- *
10
- * Pushes the analytics query down to the database as a native SQL statement.
11
- * This is the most efficient path and is preferred whenever the backing driver
12
- * supports raw SQL execution (e.g. Postgres, MySQL, SQLite).
13
- */
14
- export class NativeSQLStrategy implements AnalyticsStrategy {
15
- readonly name = 'NativeSQLStrategy';
16
- readonly priority = 10;
17
-
18
- canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {
19
- if (!query.cube) return false;
20
- const caps = ctx.queryCapabilities(query.cube);
21
- return caps.nativeSql && typeof ctx.executeRawSql === 'function';
22
- }
23
-
24
- async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {
25
- const { sql, params } = await this.generateSql(query, ctx);
26
- const cube = ctx.getCube(query.cube!)!;
27
- const objectName = this.extractObjectName(cube);
28
-
29
- const rows = await ctx.executeRawSql!(objectName, sql, params);
30
-
31
- // Build field metadata
32
- const fields = this.buildFieldMeta(query, cube);
33
-
34
- return { rows, fields, sql };
35
- }
36
-
37
- async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {
38
- const cube = ctx.getCube(query.cube!);
39
- if (!cube) {
40
- throw new Error(`Cube not found: ${query.cube}`);
41
- }
42
-
43
- const params: unknown[] = [];
44
- const selectClauses: string[] = [];
45
- const groupByClauses: string[] = [];
46
-
47
- // Build SELECT for dimensions
48
- if (query.dimensions && query.dimensions.length > 0) {
49
- for (const dim of query.dimensions) {
50
- const colExpr = this.resolveDimensionSql(cube, dim);
51
- selectClauses.push(`${colExpr} AS "${dim}"`);
52
- groupByClauses.push(colExpr);
53
- }
54
- }
55
-
56
- // Build SELECT for measures
57
- if (query.measures && query.measures.length > 0) {
58
- for (const measure of query.measures) {
59
- const aggExpr = this.resolveMeasureSql(cube, measure);
60
- selectClauses.push(`${aggExpr} AS "${measure}"`);
61
- }
62
- }
63
-
64
- // Build WHERE clause
65
- const whereClauses: string[] = [];
66
- if (query.filters && query.filters.length > 0) {
67
- for (const filter of query.filters) {
68
- const colExpr = this.resolveFieldSql(cube, filter.member);
69
- const clause = this.buildFilterClause(colExpr, filter.operator, filter.values, params);
70
- if (clause) whereClauses.push(clause);
71
- }
72
- }
73
-
74
- // Build time dimension filters
75
- if (query.timeDimensions && query.timeDimensions.length > 0) {
76
- for (const td of query.timeDimensions) {
77
- const colExpr = this.resolveFieldSql(cube, td.dimension);
78
- if (td.dateRange) {
79
- const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
80
- if (range.length === 2) {
81
- params.push(range[0], range[1]);
82
- whereClauses.push(`${colExpr} BETWEEN $${params.length - 1} AND $${params.length}`);
83
- }
84
- }
85
- }
86
- }
87
-
88
- const tableName = this.extractObjectName(cube);
89
- let sql = `SELECT ${selectClauses.join(', ')} FROM "${tableName}"`;
90
- if (whereClauses.length > 0) {
91
- sql += ` WHERE ${whereClauses.join(' AND ')}`;
92
- }
93
- if (groupByClauses.length > 0) {
94
- sql += ` GROUP BY ${groupByClauses.join(', ')}`;
95
- }
96
- if (query.order && Object.keys(query.order).length > 0) {
97
- const orderClauses = Object.entries(query.order).map(([f, d]) => `"${f}" ${d.toUpperCase()}`);
98
- sql += ` ORDER BY ${orderClauses.join(', ')}`;
99
- }
100
- if (query.limit != null) {
101
- sql += ` LIMIT ${query.limit}`;
102
- }
103
- if (query.offset != null) {
104
- sql += ` OFFSET ${query.offset}`;
105
- }
106
-
107
- return { sql, params };
108
- }
109
-
110
- // ── Helpers ──────────────────────────────────────────────────────
111
-
112
- private resolveDimensionSql(cube: Cube, member: string): string {
113
- const fieldName = member.includes('.') ? member.split('.')[1] : member;
114
- const dim = cube.dimensions[fieldName];
115
- return dim ? dim.sql : fieldName;
116
- }
117
-
118
- private resolveMeasureSql(cube: Cube, member: string): string {
119
- const fieldName = member.includes('.') ? member.split('.')[1] : member;
120
- const measure = cube.measures[fieldName];
121
- if (!measure) return `COUNT(*)`;
122
-
123
- const col = measure.sql;
124
- switch (measure.type) {
125
- case 'count': return 'COUNT(*)';
126
- case 'sum': return `SUM(${col})`;
127
- case 'avg': return `AVG(${col})`;
128
- case 'min': return `MIN(${col})`;
129
- case 'max': return `MAX(${col})`;
130
- case 'count_distinct': return `COUNT(DISTINCT ${col})`;
131
- default: return `COUNT(*)`;
132
- }
133
- }
134
-
135
- private resolveFieldSql(cube: Cube, member: string): string {
136
- const fieldName = member.includes('.') ? member.split('.')[1] : member;
137
- const dim = cube.dimensions[fieldName];
138
- if (dim) return dim.sql;
139
- const measure = cube.measures[fieldName];
140
- if (measure) return measure.sql;
141
- return fieldName;
142
- }
143
-
144
- private buildFilterClause(col: string, operator: string, values: string[] | undefined, params: unknown[]): string | null {
145
- const opMap: Record<string, string> = {
146
- equals: '=', notEquals: '!=', gt: '>', gte: '>=', lt: '<', lte: '<=',
147
- contains: 'LIKE', notContains: 'NOT LIKE',
148
- };
149
-
150
- if (operator === 'set') return `${col} IS NOT NULL`;
151
- if (operator === 'notSet') return `${col} IS NULL`;
152
-
153
- const sqlOp = opMap[operator];
154
- if (!sqlOp || !values || values.length === 0) return null;
155
-
156
- if (operator === 'contains' || operator === 'notContains') {
157
- params.push(`%${values[0]}%`);
158
- } else {
159
- params.push(values[0]);
160
- }
161
- return `${col} ${sqlOp} $${params.length}`;
162
- }
163
-
164
- private extractObjectName(cube: Cube): string {
165
- return cube.sql.trim();
166
- }
167
-
168
- private buildFieldMeta(query: AnalyticsQuery, cube: Cube): Array<{ name: string; type: string }> {
169
- const fields: Array<{ name: string; type: string }> = [];
170
- if (query.dimensions) {
171
- for (const dim of query.dimensions) {
172
- const fieldName = dim.includes('.') ? dim.split('.')[1] : dim;
173
- const d = cube.dimensions[fieldName];
174
- fields.push({ name: dim, type: d?.type || 'string' });
175
- }
176
- }
177
- if (query.measures) {
178
- for (const m of query.measures) {
179
- fields.push({ name: m, type: 'number' });
180
- }
181
- }
182
- return fields;
183
- }
184
- }
@@ -1,178 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';
4
- import type { Cube } from '@objectstack/spec/data';
5
- import type { AnalyticsStrategy, StrategyContext } from './types.js';
6
-
7
- /**
8
- * ObjectQLStrategy — Priority 2
9
- *
10
- * Translates an analytics query into an ObjectQL `engine.aggregate()` call.
11
- * This path works with any driver that supports the ObjectQL aggregate AST
12
- * (Postgres, Mongo, SQLite, etc.) without requiring raw SQL access.
13
- */
14
- export class ObjectQLStrategy implements AnalyticsStrategy {
15
- readonly name = 'ObjectQLStrategy';
16
- readonly priority = 20;
17
-
18
- canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {
19
- if (!query.cube) return false;
20
- const caps = ctx.queryCapabilities(query.cube);
21
- return caps.objectqlAggregate && typeof ctx.executeAggregate === 'function';
22
- }
23
-
24
- async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {
25
- const cube = ctx.getCube(query.cube!)!;
26
- const objectName = this.extractObjectName(cube);
27
-
28
- // Build groupBy from dimensions
29
- const groupBy: string[] = [];
30
- if (query.dimensions && query.dimensions.length > 0) {
31
- for (const dim of query.dimensions) {
32
- groupBy.push(this.resolveFieldName(cube, dim, 'dimension'));
33
- }
34
- }
35
-
36
- // Build aggregations from measures
37
- const aggregations: Array<{ field: string; method: string; alias: string }> = [];
38
- if (query.measures && query.measures.length > 0) {
39
- for (const measure of query.measures) {
40
- const { field, method } = this.resolveMeasureAggregation(cube, measure);
41
- aggregations.push({ field, method, alias: measure });
42
- }
43
- }
44
-
45
- // Build filter from query filters
46
- const filter: Record<string, unknown> = {};
47
- if (query.filters && query.filters.length > 0) {
48
- for (const f of query.filters) {
49
- const fieldName = this.resolveFieldName(cube, f.member, 'any');
50
- filter[fieldName] = this.convertFilter(f.operator, f.values);
51
- }
52
- }
53
-
54
- const rows = await ctx.executeAggregate!(objectName, {
55
- groupBy: groupBy.length > 0 ? groupBy : undefined,
56
- aggregations: aggregations.length > 0 ? aggregations : undefined,
57
- filter: Object.keys(filter).length > 0 ? filter : undefined,
58
- });
59
-
60
- // Remap short field names back to cube-qualified names
61
- const mappedRows = rows.map(row => {
62
- const mapped: Record<string, unknown> = {};
63
- if (query.dimensions) {
64
- for (const dim of query.dimensions) {
65
- const shortName = this.resolveFieldName(cube, dim, 'dimension');
66
- if (shortName in row) mapped[dim] = row[shortName];
67
- }
68
- }
69
- if (query.measures) {
70
- for (const m of query.measures) {
71
- // Alias was set to the full measure name
72
- if (m in row) mapped[m] = row[m];
73
- }
74
- }
75
- return mapped;
76
- });
77
-
78
- const fields = this.buildFieldMeta(query, cube);
79
- return { rows: mappedRows, fields };
80
- }
81
-
82
- async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {
83
- const cube = ctx.getCube(query.cube!);
84
- if (!cube) {
85
- throw new Error(`Cube not found: ${query.cube}`);
86
- }
87
-
88
- // Generate a representative SQL even though ObjectQL uses AST internally
89
- const selectParts: string[] = [];
90
- const groupByParts: string[] = [];
91
-
92
- if (query.dimensions) {
93
- for (const dim of query.dimensions) {
94
- const col = this.resolveFieldName(cube, dim, 'dimension');
95
- selectParts.push(`${col} AS "${dim}"`);
96
- groupByParts.push(col);
97
- }
98
- }
99
- if (query.measures) {
100
- for (const m of query.measures) {
101
- const { field, method } = this.resolveMeasureAggregation(cube, m);
102
- const aggSql = method === 'count' ? 'COUNT(*)' : `${method.toUpperCase()}(${field})`;
103
- selectParts.push(`${aggSql} AS "${m}"`);
104
- }
105
- }
106
-
107
- const tableName = this.extractObjectName(cube);
108
- let sql = `SELECT ${selectParts.join(', ')} FROM "${tableName}"`;
109
- if (groupByParts.length > 0) {
110
- sql += ` GROUP BY ${groupByParts.join(', ')}`;
111
- }
112
-
113
- return { sql, params: [] };
114
- }
115
-
116
- // ── Helpers ──────────────────────────────────────────────────────
117
-
118
- private resolveFieldName(cube: Cube, member: string, kind: 'dimension' | 'measure' | 'any'): string {
119
- const fieldName = member.includes('.') ? member.split('.')[1] : member;
120
- if (kind === 'dimension' || kind === 'any') {
121
- const dim = cube.dimensions[fieldName];
122
- if (dim) return dim.sql.replace(/^\$/, '');
123
- }
124
- if (kind === 'measure' || kind === 'any') {
125
- const measure = cube.measures[fieldName];
126
- if (measure) return measure.sql.replace(/^\$/, '');
127
- }
128
- return fieldName;
129
- }
130
-
131
- private resolveMeasureAggregation(cube: Cube, measureName: string): { field: string; method: string } {
132
- const fieldName = measureName.includes('.') ? measureName.split('.')[1] : measureName;
133
- const measure = cube.measures[fieldName];
134
- if (!measure) return { field: '*', method: 'count' };
135
- return {
136
- field: measure.sql.replace(/^\$/, ''),
137
- method: measure.type === 'count_distinct' ? 'count_distinct' : measure.type,
138
- };
139
- }
140
-
141
- private convertFilter(operator: string, values?: string[]): unknown {
142
- if (operator === 'set') return { $ne: null };
143
- if (operator === 'notSet') return null;
144
- if (!values || values.length === 0) return undefined;
145
-
146
- switch (operator) {
147
- case 'equals': return values[0];
148
- case 'notEquals': return { $ne: values[0] };
149
- case 'gt': return { $gt: values[0] };
150
- case 'gte': return { $gte: values[0] };
151
- case 'lt': return { $lt: values[0] };
152
- case 'lte': return { $lte: values[0] };
153
- case 'contains': return { $regex: values[0] };
154
- default: return values[0];
155
- }
156
- }
157
-
158
- private extractObjectName(cube: Cube): string {
159
- return cube.sql.trim();
160
- }
161
-
162
- private buildFieldMeta(query: AnalyticsQuery, cube: Cube): Array<{ name: string; type: string }> {
163
- const fields: Array<{ name: string; type: string }> = [];
164
- if (query.dimensions) {
165
- for (const dim of query.dimensions) {
166
- const fieldName = dim.includes('.') ? dim.split('.')[1] : dim;
167
- const d = cube.dimensions[fieldName];
168
- fields.push({ name: dim, type: d?.type || 'string' });
169
- }
170
- }
171
- if (query.measures) {
172
- for (const m of query.measures) {
173
- fields.push({ name: m, type: 'number' });
174
- }
175
- }
176
- return fields;
177
- }
178
- }
@@ -1,11 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- /**
4
- * Strategy pattern types — re-exported from @objectstack/spec/contracts
5
- * for convenience. The canonical definitions live in the spec package.
6
- */
7
- export type {
8
- AnalyticsStrategy,
9
- StrategyContext,
10
- DriverCapabilities,
11
- } from '@objectstack/spec/contracts';
package/tsconfig.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src",
6
- "types": [
7
- "node"
8
- ]
9
- },
10
- "include": [
11
- "src"
12
- ],
13
- "exclude": [
14
- "node_modules",
15
- "dist"
16
- ]
17
- }