Skip to content

Commit 295af97

Browse files
Copilothotlong
andcommitted
feat: register MemoryAnalyticsService in createKernel() for demo/MSW environments
- Import MemoryAnalyticsService from @objectstack/driver-memory - Add buildCubesFromConfig() to auto-generate cube definitions from app objects - Register analytics service on kernel before MSWPlugin loads - Add analytics routes to broker shim for broker-based calls - Adapt getMetadata() → getMeta() method name mismatch between HttpDispatcher and IAnalyticsService Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent f4f7784 commit 295af97

File tree

1 file changed

+110
-1
lines changed

1 file changed

+110
-1
lines changed

apps/console/src/mocks/createKernel.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414

1515
import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
1616
import { ObjectQLPlugin } from '@objectstack/objectql';
17-
import { InMemoryDriver } from '@objectstack/driver-memory';
17+
import { InMemoryDriver, MemoryAnalyticsService } from '@objectstack/driver-memory';
1818
import { MSWPlugin } from '@objectstack/plugin-msw';
1919
import type { MSWPluginOptions } from '@objectstack/plugin-msw';
20+
import type { Cube } from '@objectstack/spec/data';
2021

2122
export interface KernelOptions {
2223
/** Application configuration (defineStack output) */
@@ -99,6 +100,17 @@ async function installBrokerShim(kernel: ObjectKernel): Promise<void> {
99100
return protocol.getMetaItems({ type: method, packageId: params.packageId });
100101
}
101102

103+
// Analytics service calls (e.g. analytics.query, analytics.meta)
104+
if (service === 'analytics') {
105+
let analytics: any;
106+
try { analytics = await kernel.getService('analytics'); } catch { /* noop */ }
107+
if (analytics) {
108+
if (method === 'query') return analytics.query(params);
109+
if (method === 'meta') return analytics.getMeta(params?.cubeName);
110+
if (method === 'sql') return analytics.generateSql(params);
111+
}
112+
}
113+
102114
throw new Error(`[BrokerShim] Unhandled action: ${action}`);
103115
},
104116
};
@@ -157,6 +169,89 @@ function patchDriverCreate(driver: InMemoryDriver): void {
157169
};
158170
}
159171

172+
/**
173+
* Build cube definitions from the appConfig objects.
174+
*
175+
* Scans objects for numeric/currency fields and generates a Cube per object
176+
* with sensible default measures (count + sum/avg for each numeric field)
177+
* and dimensions (all non-numeric scalar fields).
178+
*
179+
* Only used in demo/MSW/dev environments to provide out-of-the-box
180+
* analytics without requiring explicit cube configuration.
181+
*/
182+
function buildCubesFromConfig(appConfig: any): Cube[] {
183+
const objects: any[] = appConfig?.objects ?? [];
184+
const cubes: Cube[] = [];
185+
186+
for (const obj of objects) {
187+
if (!obj?.name || !obj?.fields) continue;
188+
189+
const measures: Record<string, any> = {
190+
count: {
191+
name: 'count',
192+
label: 'Count',
193+
type: 'count' as const,
194+
sql: 'id',
195+
},
196+
};
197+
198+
const dimensions: Record<string, any> = {};
199+
200+
for (const [fieldName, fieldDef] of Object.entries<any>(obj.fields)) {
201+
if (!fieldDef) continue;
202+
const fType = fieldDef.type;
203+
204+
// Numeric / currency / percent fields → aggregate measures
205+
if (fType === 'currency' || fType === 'number' || fType === 'percent') {
206+
measures[`${fieldName}_sum`] = {
207+
name: `${fieldName}_sum`,
208+
label: `${fieldDef.label ?? fieldName} (Sum)`,
209+
type: 'sum' as const,
210+
sql: fieldName,
211+
};
212+
measures[`${fieldName}_avg`] = {
213+
name: `${fieldName}_avg`,
214+
label: `${fieldDef.label ?? fieldName} (Avg)`,
215+
type: 'avg' as const,
216+
sql: fieldName,
217+
};
218+
}
219+
220+
// Scalar fields → dimensions for grouping
221+
if (fType === 'text' || fType === 'select' || fType === 'boolean') {
222+
dimensions[fieldName] = {
223+
name: fieldName,
224+
label: fieldDef.label ?? fieldName,
225+
type: fType === 'boolean' ? ('boolean' as const) : ('string' as const),
226+
sql: fieldName,
227+
};
228+
}
229+
230+
// Date/datetime fields → time dimensions
231+
if (fType === 'date' || fType === 'datetime') {
232+
dimensions[fieldName] = {
233+
name: fieldName,
234+
label: fieldDef.label ?? fieldName,
235+
type: 'time' as const,
236+
sql: fieldName,
237+
granularities: ['day', 'week', 'month', 'quarter', 'year'],
238+
};
239+
}
240+
}
241+
242+
cubes.push({
243+
name: String(obj.name),
244+
title: obj.label ? String(obj.label) : undefined,
245+
description: obj.description ? String(obj.description) : undefined,
246+
sql: String(obj.name), // table name matches object name in InMemoryDriver
247+
measures,
248+
dimensions,
249+
} as Cube);
250+
}
251+
252+
return cubes;
253+
}
254+
160255
/**
161256
* Create and bootstrap an ObjectStack kernel with in-memory driver.
162257
*
@@ -178,6 +273,20 @@ export async function createKernel(options: KernelOptions): Promise<KernelResult
178273
await kernel.use(new DriverPlugin(driver, 'memory'));
179274
await kernel.use(new AppPlugin(appConfig));
180275

276+
// Register MemoryAnalyticsService so that HttpDispatcher can serve
277+
// /api/v1/analytics/* endpoints in demo/MSW/dev environments.
278+
// Without this, analytics routes return 405 because the kernel has
279+
// no 'analytics' service and the dispatcher skips the handler.
280+
const cubes = buildCubesFromConfig(appConfig);
281+
const memoryAnalytics = new MemoryAnalyticsService({ driver, cubes });
282+
kernel.registerService('analytics', {
283+
query: (query: any) => memoryAnalytics.query(query),
284+
getMeta: (cubeName?: string) => memoryAnalytics.getMeta(cubeName),
285+
// HttpDispatcher calls getMetadata(); adapt to MemoryAnalyticsService.getMeta()
286+
getMetadata: () => memoryAnalytics.getMeta(),
287+
generateSql: (query: any) => memoryAnalytics.generateSql(query),
288+
});
289+
181290
let mswPlugin: MSWPlugin | undefined;
182291
if (mswOptions) {
183292
// Install a protocol-based broker shim BEFORE MSWPlugin's start phase

0 commit comments

Comments
 (0)