1414
1515import { ObjectKernel , DriverPlugin , AppPlugin } from '@objectstack/runtime' ;
1616import { ObjectQLPlugin } from '@objectstack/objectql' ;
17- import { InMemoryDriver } from '@objectstack/driver-memory' ;
17+ import { InMemoryDriver , MemoryAnalyticsService } from '@objectstack/driver-memory' ;
1818import { MSWPlugin } from '@objectstack/plugin-msw' ;
1919import type { MSWPluginOptions } from '@objectstack/plugin-msw' ;
20+ import type { Cube } from '@objectstack/spec/data' ;
2021
2122export 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