@objectstack/driver-memory 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.
- package/README.md +89 -8
- package/dist/index.d.mts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +178 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +178 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +31 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -617
- package/objectstack.config.ts +0 -260
- package/src/in-memory-strategy.ts +0 -47
- package/src/index.ts +0 -32
- package/src/memory-analytics.test.ts +0 -346
- package/src/memory-analytics.ts +0 -518
- package/src/memory-driver.test.ts +0 -722
- package/src/memory-driver.ts +0 -1206
- package/src/memory-matcher.ts +0 -177
- package/src/persistence/file-adapter.ts +0 -103
- package/src/persistence/index.ts +0 -4
- package/src/persistence/local-storage-adapter.ts +0 -60
- package/src/persistence/persistence.test.ts +0 -298
- package/tsconfig.json +0 -27
- package/vitest.config.ts +0 -22
package/src/memory-analytics.ts
DELETED
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type { IAnalyticsService, AnalyticsResult, CubeMeta } from '@objectstack/spec/contracts';
|
|
4
|
-
import type { Cube, AnalyticsQuery } from '@objectstack/spec/data';
|
|
5
|
-
import type { InMemoryDriver } from './memory-driver.js';
|
|
6
|
-
import { Logger, createLogger } from '@objectstack/core';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Configuration for MemoryAnalyticsService
|
|
10
|
-
*/
|
|
11
|
-
export interface MemoryAnalyticsConfig {
|
|
12
|
-
/** The data driver instance to use for queries */
|
|
13
|
-
driver: InMemoryDriver;
|
|
14
|
-
/** Cube definitions for the semantic layer */
|
|
15
|
-
cubes: Cube[];
|
|
16
|
-
/** Optional logger */
|
|
17
|
-
logger?: Logger;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Memory-Based Analytics Service
|
|
22
|
-
*
|
|
23
|
-
* Implements IAnalyticsService using InMemoryDriver's aggregation capabilities.
|
|
24
|
-
* Provides a semantic layer (Cubes, Metrics, Dimensions) on top of in-memory data.
|
|
25
|
-
*
|
|
26
|
-
* Features:
|
|
27
|
-
* - Cube-based semantic modeling
|
|
28
|
-
* - Measure calculations (count, sum, avg, min, max, count_distinct)
|
|
29
|
-
* - Dimension grouping
|
|
30
|
-
* - Filter support
|
|
31
|
-
* - Time dimension handling
|
|
32
|
-
* - SQL generation (for debugging/transparency)
|
|
33
|
-
*
|
|
34
|
-
* This implementation is suitable for:
|
|
35
|
-
* - Development and testing
|
|
36
|
-
* - Local-first analytics
|
|
37
|
-
* - Small to medium datasets
|
|
38
|
-
* - Prototyping BI applications
|
|
39
|
-
*/
|
|
40
|
-
export class MemoryAnalyticsService implements IAnalyticsService {
|
|
41
|
-
private driver: InMemoryDriver;
|
|
42
|
-
private cubes: Map<string, Cube>;
|
|
43
|
-
private logger: Logger;
|
|
44
|
-
|
|
45
|
-
constructor(config: MemoryAnalyticsConfig) {
|
|
46
|
-
this.driver = config.driver;
|
|
47
|
-
this.cubes = new Map(config.cubes.map(c => [c.name, c]));
|
|
48
|
-
this.logger = config.logger || createLogger({ level: 'info', format: 'pretty' });
|
|
49
|
-
this.logger.debug('MemoryAnalyticsService initialized', { cubeCount: this.cubes.size });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Execute an analytical query using the memory driver's aggregation pipeline
|
|
54
|
-
*/
|
|
55
|
-
async query(query: AnalyticsQuery): Promise<AnalyticsResult> {
|
|
56
|
-
this.logger.debug('Executing analytics query', { cube: query.cube, measures: query.measures });
|
|
57
|
-
|
|
58
|
-
// Get cube definition
|
|
59
|
-
if (!query.cube) {
|
|
60
|
-
throw new Error('Cube name is required');
|
|
61
|
-
}
|
|
62
|
-
const cube = this.cubes.get(query.cube);
|
|
63
|
-
if (!cube) {
|
|
64
|
-
throw new Error(`Cube not found: ${query.cube}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Build MongoDB aggregation pipeline
|
|
68
|
-
const pipeline: Record<string, any>[] = [];
|
|
69
|
-
|
|
70
|
-
// Stage 1: $match for filters
|
|
71
|
-
if (query.filters && query.filters.length > 0) {
|
|
72
|
-
const matchStage: Record<string, any> = {};
|
|
73
|
-
for (const filter of query.filters) {
|
|
74
|
-
const mongoOp = this.convertOperatorToMongo(filter.operator);
|
|
75
|
-
const fieldPath = this.resolveFieldPath(cube, filter.member);
|
|
76
|
-
|
|
77
|
-
if (filter.values && filter.values.length > 0) {
|
|
78
|
-
if (mongoOp === '$in') {
|
|
79
|
-
matchStage[fieldPath] = { $in: filter.values };
|
|
80
|
-
} else if (mongoOp === '$nin') {
|
|
81
|
-
matchStage[fieldPath] = { $nin: filter.values };
|
|
82
|
-
} else {
|
|
83
|
-
matchStage[fieldPath] = { [mongoOp]: filter.values[0] };
|
|
84
|
-
}
|
|
85
|
-
} else if (mongoOp === '$exists') {
|
|
86
|
-
matchStage[fieldPath] = { $exists: filter.operator === 'set' };
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (Object.keys(matchStage).length > 0) {
|
|
90
|
-
pipeline.push({ $match: matchStage });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Stage 2: Time dimension filters
|
|
95
|
-
if (query.timeDimensions && query.timeDimensions.length > 0) {
|
|
96
|
-
for (const timeDim of query.timeDimensions) {
|
|
97
|
-
const fieldPath = this.resolveFieldPath(cube, timeDim.dimension);
|
|
98
|
-
if (timeDim.dateRange) {
|
|
99
|
-
const range = Array.isArray(timeDim.dateRange)
|
|
100
|
-
? timeDim.dateRange
|
|
101
|
-
: this.parseDateRangeString(timeDim.dateRange);
|
|
102
|
-
|
|
103
|
-
if (range.length === 2) {
|
|
104
|
-
pipeline.push({
|
|
105
|
-
$match: {
|
|
106
|
-
[fieldPath]: {
|
|
107
|
-
$gte: new Date(range[0]),
|
|
108
|
-
$lte: new Date(range[1])
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Stage 3: $group for measures and dimensions
|
|
118
|
-
const groupStage: Record<string, any> = { _id: {} };
|
|
119
|
-
|
|
120
|
-
// Add dimensions to _id
|
|
121
|
-
if (query.dimensions && query.dimensions.length > 0) {
|
|
122
|
-
for (const dim of query.dimensions) {
|
|
123
|
-
const fieldPath = this.resolveFieldPath(cube, dim);
|
|
124
|
-
const dimName = this.getShortName(dim);
|
|
125
|
-
groupStage._id[dimName] = `$${fieldPath}`;
|
|
126
|
-
}
|
|
127
|
-
} else {
|
|
128
|
-
groupStage._id = null; // No grouping, aggregate all
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Add measures as computed fields
|
|
132
|
-
if (query.measures && query.measures.length > 0) {
|
|
133
|
-
for (const measure of query.measures) {
|
|
134
|
-
const measureDef = this.resolveMeasure(cube, measure);
|
|
135
|
-
const measureName = this.getShortName(measure);
|
|
136
|
-
|
|
137
|
-
if (measureDef) {
|
|
138
|
-
const aggregator = this.buildAggregator(measureDef);
|
|
139
|
-
groupStage[measureName] = aggregator;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
pipeline.push({ $group: groupStage });
|
|
145
|
-
|
|
146
|
-
// Stage 4: $project to reshape results (use short names, we'll fix them later)
|
|
147
|
-
const projectStage: Record<string, any> = { _id: 0 };
|
|
148
|
-
if (query.dimensions && query.dimensions.length > 0) {
|
|
149
|
-
for (const dim of query.dimensions) {
|
|
150
|
-
const dimName = this.getShortName(dim);
|
|
151
|
-
projectStage[dimName] = `$_id.${dimName}`;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (query.measures && query.measures.length > 0) {
|
|
155
|
-
for (const measure of query.measures) {
|
|
156
|
-
const measureName = this.getShortName(measure);
|
|
157
|
-
projectStage[measureName] = `$${measureName}`;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
pipeline.push({ $project: projectStage });
|
|
161
|
-
|
|
162
|
-
// Stage 5: $sort (use short names)
|
|
163
|
-
if (query.order && Object.keys(query.order).length > 0) {
|
|
164
|
-
const sortStage: Record<string, any> = {};
|
|
165
|
-
for (const [field, direction] of Object.entries(query.order)) {
|
|
166
|
-
const shortName = this.getShortName(field);
|
|
167
|
-
sortStage[shortName] = direction === 'asc' ? 1 : -1;
|
|
168
|
-
}
|
|
169
|
-
pipeline.push({ $sort: sortStage });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Stage 6: $limit and $skip
|
|
173
|
-
if (query.offset) {
|
|
174
|
-
pipeline.push({ $skip: query.offset });
|
|
175
|
-
}
|
|
176
|
-
if (query.limit) {
|
|
177
|
-
pipeline.push({ $limit: query.limit });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Execute the aggregation pipeline
|
|
181
|
-
const tableName = this.extractTableName(cube.sql);
|
|
182
|
-
const rawRows = await this.driver.aggregate(tableName, pipeline);
|
|
183
|
-
|
|
184
|
-
// Rename fields from short names to full cube.field names
|
|
185
|
-
const rows = rawRows.map(row => {
|
|
186
|
-
const renamedRow: Record<string, unknown> = {};
|
|
187
|
-
|
|
188
|
-
// Rename dimensions
|
|
189
|
-
if (query.dimensions) {
|
|
190
|
-
for (const dim of query.dimensions) {
|
|
191
|
-
const shortName = this.getShortName(dim);
|
|
192
|
-
if (shortName in row) {
|
|
193
|
-
renamedRow[dim] = row[shortName];
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Rename measures
|
|
199
|
-
if (query.measures) {
|
|
200
|
-
for (const measure of query.measures) {
|
|
201
|
-
const shortName = this.getShortName(measure);
|
|
202
|
-
if (shortName in row) {
|
|
203
|
-
renamedRow[measure] = row[shortName];
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return renamedRow;
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Build field metadata
|
|
212
|
-
const fields: Array<{ name: string; type: string }> = [];
|
|
213
|
-
|
|
214
|
-
if (query.dimensions) {
|
|
215
|
-
for (const dim of query.dimensions) {
|
|
216
|
-
const dimension = this.resolveDimension(cube, dim);
|
|
217
|
-
fields.push({
|
|
218
|
-
name: dim,
|
|
219
|
-
type: dimension?.type || 'string'
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (query.measures) {
|
|
225
|
-
for (const measure of query.measures) {
|
|
226
|
-
const measureDef = this.resolveMeasure(cube, measure);
|
|
227
|
-
fields.push({
|
|
228
|
-
name: measure,
|
|
229
|
-
type: this.measureTypeToFieldType(measureDef?.type || 'count')
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
this.logger.debug('Analytics query completed', { rowCount: rows.length });
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
rows,
|
|
238
|
-
fields,
|
|
239
|
-
sql: this.generateSqlFromPipeline(tableName, pipeline) // For debugging
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Get available cube metadata for discovery
|
|
245
|
-
*/
|
|
246
|
-
async getMeta(cubeName?: string): Promise<CubeMeta[]> {
|
|
247
|
-
const cubes = cubeName
|
|
248
|
-
? [this.cubes.get(cubeName)].filter(Boolean) as Cube[]
|
|
249
|
-
: Array.from(this.cubes.values());
|
|
250
|
-
|
|
251
|
-
return cubes.map(cube => ({
|
|
252
|
-
name: cube.name,
|
|
253
|
-
title: cube.title,
|
|
254
|
-
measures: Object.entries(cube.measures).map(([key, measure]) => ({
|
|
255
|
-
name: `${cube.name}.${key}`,
|
|
256
|
-
type: measure.type,
|
|
257
|
-
title: measure.label
|
|
258
|
-
})),
|
|
259
|
-
dimensions: Object.entries(cube.dimensions).map(([key, dimension]) => ({
|
|
260
|
-
name: `${cube.name}.${key}`,
|
|
261
|
-
type: dimension.type,
|
|
262
|
-
title: dimension.label
|
|
263
|
-
}))
|
|
264
|
-
}));
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Generate SQL representation for debugging/transparency
|
|
269
|
-
*/
|
|
270
|
-
async generateSql(query: AnalyticsQuery): Promise<{ sql: string; params: unknown[] }> {
|
|
271
|
-
if (!query.cube) {
|
|
272
|
-
throw new Error('Cube name is required');
|
|
273
|
-
}
|
|
274
|
-
const cube = this.cubes.get(query.cube);
|
|
275
|
-
if (!cube) {
|
|
276
|
-
throw new Error(`Cube not found: ${query.cube}`);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const tableName = this.extractTableName(cube.sql);
|
|
280
|
-
const selectClauses: string[] = [];
|
|
281
|
-
const groupByClauses: string[] = [];
|
|
282
|
-
|
|
283
|
-
// Build SELECT for dimensions
|
|
284
|
-
if (query.dimensions && query.dimensions.length > 0) {
|
|
285
|
-
for (const dim of query.dimensions) {
|
|
286
|
-
const fieldPath = this.resolveFieldPath(cube, dim);
|
|
287
|
-
selectClauses.push(`${fieldPath} AS "${dim}"`);
|
|
288
|
-
groupByClauses.push(fieldPath);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Build SELECT for measures
|
|
293
|
-
if (query.measures && query.measures.length > 0) {
|
|
294
|
-
for (const measure of query.measures) {
|
|
295
|
-
const measureDef = this.resolveMeasure(cube, measure);
|
|
296
|
-
if (measureDef) {
|
|
297
|
-
const aggSql = this.measureToSql(measureDef);
|
|
298
|
-
selectClauses.push(`${aggSql} AS "${measure}"`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Build WHERE clause
|
|
304
|
-
const whereClauses: string[] = [];
|
|
305
|
-
if (query.filters && query.filters.length > 0) {
|
|
306
|
-
for (const filter of query.filters) {
|
|
307
|
-
const fieldPath = this.resolveFieldPath(cube, filter.member);
|
|
308
|
-
const sqlOp = this.operatorToSql(filter.operator);
|
|
309
|
-
if (filter.values && filter.values.length > 0) {
|
|
310
|
-
whereClauses.push(`${fieldPath} ${sqlOp} '${filter.values[0]}'`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
let sql = `SELECT ${selectClauses.join(', ')} FROM ${tableName}`;
|
|
316
|
-
if (whereClauses.length > 0) {
|
|
317
|
-
sql += ` WHERE ${whereClauses.join(' AND ')}`;
|
|
318
|
-
}
|
|
319
|
-
if (groupByClauses.length > 0) {
|
|
320
|
-
sql += ` GROUP BY ${groupByClauses.join(', ')}`;
|
|
321
|
-
}
|
|
322
|
-
if (query.order) {
|
|
323
|
-
const orderClauses = Object.entries(query.order).map(([field, dir]) =>
|
|
324
|
-
`"${field}" ${dir.toUpperCase()}`
|
|
325
|
-
);
|
|
326
|
-
sql += ` ORDER BY ${orderClauses.join(', ')}`;
|
|
327
|
-
}
|
|
328
|
-
if (query.limit) {
|
|
329
|
-
sql += ` LIMIT ${query.limit}`;
|
|
330
|
-
}
|
|
331
|
-
if (query.offset) {
|
|
332
|
-
sql += ` OFFSET ${query.offset}`;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return { sql, params: [] };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ===================================
|
|
339
|
-
// Helper Methods
|
|
340
|
-
// ===================================
|
|
341
|
-
|
|
342
|
-
private resolveFieldPath(cube: Cube, member: string): string {
|
|
343
|
-
// Handle both "cube.field" and "field" formats
|
|
344
|
-
const parts = member.split('.');
|
|
345
|
-
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
346
|
-
|
|
347
|
-
// Check if it's a dimension
|
|
348
|
-
const dimension = cube.dimensions[fieldName];
|
|
349
|
-
if (dimension) {
|
|
350
|
-
// Extract field path from SQL expression
|
|
351
|
-
return dimension.sql.replace(/^\$/, ''); // Remove $ prefix if present
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Check if it's a measure (for filters)
|
|
355
|
-
const measure = cube.measures[fieldName];
|
|
356
|
-
if (measure) {
|
|
357
|
-
return measure.sql.replace(/^\$/, '');
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return fieldName;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
private resolveMeasure(cube: Cube, measureName: string) {
|
|
364
|
-
const parts = measureName.split('.');
|
|
365
|
-
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
366
|
-
return cube.measures[fieldName];
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
private resolveDimension(cube: Cube, dimensionName: string) {
|
|
370
|
-
const parts = dimensionName.split('.');
|
|
371
|
-
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
372
|
-
return cube.dimensions[fieldName];
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
private getShortName(fullName: string): string {
|
|
376
|
-
const parts = fullName.split('.');
|
|
377
|
-
return parts.length > 1 ? parts[1] : parts[0];
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
private buildAggregator(measure: { type: string; sql: string; filters?: any[] }): any {
|
|
381
|
-
const fieldPath = measure.sql.replace(/^\$/, '');
|
|
382
|
-
|
|
383
|
-
switch (measure.type) {
|
|
384
|
-
case 'count':
|
|
385
|
-
return { $sum: 1 };
|
|
386
|
-
case 'sum':
|
|
387
|
-
return { $sum: `$${fieldPath}` };
|
|
388
|
-
case 'avg':
|
|
389
|
-
return { $avg: `$${fieldPath}` };
|
|
390
|
-
case 'min':
|
|
391
|
-
return { $min: `$${fieldPath}` };
|
|
392
|
-
case 'max':
|
|
393
|
-
return { $max: `$${fieldPath}` };
|
|
394
|
-
case 'count_distinct':
|
|
395
|
-
return { $addToSet: `$${fieldPath}` }; // Will need post-processing for count
|
|
396
|
-
default:
|
|
397
|
-
return { $sum: 1 }; // Default to count
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
private measureTypeToFieldType(measureType: string): string {
|
|
402
|
-
switch (measureType) {
|
|
403
|
-
case 'count':
|
|
404
|
-
case 'sum':
|
|
405
|
-
case 'count_distinct':
|
|
406
|
-
return 'number';
|
|
407
|
-
case 'avg':
|
|
408
|
-
case 'min':
|
|
409
|
-
case 'max':
|
|
410
|
-
return 'number';
|
|
411
|
-
case 'string':
|
|
412
|
-
return 'string';
|
|
413
|
-
case 'boolean':
|
|
414
|
-
return 'boolean';
|
|
415
|
-
default:
|
|
416
|
-
return 'number';
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
private convertOperatorToMongo(operator: string): string {
|
|
421
|
-
const opMap: Record<string, string> = {
|
|
422
|
-
'equals': '$eq',
|
|
423
|
-
'notEquals': '$ne',
|
|
424
|
-
'contains': '$regex',
|
|
425
|
-
'notContains': '$not',
|
|
426
|
-
'gt': '$gt',
|
|
427
|
-
'gte': '$gte',
|
|
428
|
-
'lt': '$lt',
|
|
429
|
-
'lte': '$lte',
|
|
430
|
-
'set': '$exists',
|
|
431
|
-
'notSet': '$exists',
|
|
432
|
-
'inDateRange': '$gte', // Will need special handling
|
|
433
|
-
};
|
|
434
|
-
return opMap[operator] || '$eq';
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
private operatorToSql(operator: string): string {
|
|
438
|
-
const opMap: Record<string, string> = {
|
|
439
|
-
'equals': '=',
|
|
440
|
-
'notEquals': '!=',
|
|
441
|
-
'contains': 'LIKE',
|
|
442
|
-
'notContains': 'NOT LIKE',
|
|
443
|
-
'gt': '>',
|
|
444
|
-
'gte': '>=',
|
|
445
|
-
'lt': '<',
|
|
446
|
-
'lte': '<=',
|
|
447
|
-
};
|
|
448
|
-
return opMap[operator] || '=';
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
private measureToSql(measure: { type: string; sql: string }): string {
|
|
452
|
-
const fieldPath = measure.sql.replace(/^\$/, '');
|
|
453
|
-
|
|
454
|
-
switch (measure.type) {
|
|
455
|
-
case 'count':
|
|
456
|
-
return 'COUNT(*)';
|
|
457
|
-
case 'sum':
|
|
458
|
-
return `SUM(${fieldPath})`;
|
|
459
|
-
case 'avg':
|
|
460
|
-
return `AVG(${fieldPath})`;
|
|
461
|
-
case 'min':
|
|
462
|
-
return `MIN(${fieldPath})`;
|
|
463
|
-
case 'max':
|
|
464
|
-
return `MAX(${fieldPath})`;
|
|
465
|
-
case 'count_distinct':
|
|
466
|
-
return `COUNT(DISTINCT ${fieldPath})`;
|
|
467
|
-
default:
|
|
468
|
-
return 'COUNT(*)';
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
private extractTableName(sql: string): string {
|
|
473
|
-
// For simple table names, return as-is
|
|
474
|
-
// For complex SQL, this would need more sophisticated parsing
|
|
475
|
-
return sql.trim();
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
private parseDateRangeString(range: string): string[] {
|
|
479
|
-
// Simple parser for common date range strings
|
|
480
|
-
// In production, this would use a proper date range parser
|
|
481
|
-
const now = new Date();
|
|
482
|
-
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
483
|
-
|
|
484
|
-
if (range === 'today') {
|
|
485
|
-
return [today.toISOString(), new Date(today.getTime() + 86400000).toISOString()];
|
|
486
|
-
} else if (range.startsWith('last ')) {
|
|
487
|
-
const parts = range.split(' ');
|
|
488
|
-
const num = parseInt(parts[1]);
|
|
489
|
-
const unit = parts[2];
|
|
490
|
-
const start = new Date(today);
|
|
491
|
-
|
|
492
|
-
if (unit.startsWith('day')) {
|
|
493
|
-
start.setDate(start.getDate() - num);
|
|
494
|
-
} else if (unit.startsWith('week')) {
|
|
495
|
-
start.setDate(start.getDate() - num * 7);
|
|
496
|
-
} else if (unit.startsWith('month')) {
|
|
497
|
-
start.setMonth(start.getMonth() - num);
|
|
498
|
-
} else if (unit.startsWith('year')) {
|
|
499
|
-
start.setFullYear(start.getFullYear() - num);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
return [start.toISOString(), now.toISOString()];
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return [range, range]; // Fallback
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
private generateSqlFromPipeline(table: string, pipeline: Record<string, any>[]): string {
|
|
509
|
-
// Simplified SQL generation for debugging
|
|
510
|
-
// This is a basic representation of the aggregation pipeline
|
|
511
|
-
const stages = pipeline.map((stage, idx) => {
|
|
512
|
-
const op = Object.keys(stage)[0];
|
|
513
|
-
return `/* Stage ${idx + 1}: ${op} */ ${JSON.stringify(stage[op])}`;
|
|
514
|
-
}).join('\n');
|
|
515
|
-
|
|
516
|
-
return `-- MongoDB Aggregation Pipeline on table: ${table}\n${stages}`;
|
|
517
|
-
}
|
|
518
|
-
}
|