@@ -15,6 +15,48 @@ import { CoreServiceName, StorageNameMapping } from '@objectstack/spec/system';
1515import { IRealtimeService , RealtimeEventPayload } from '@objectstack/spec/contracts' ;
1616import { pluralToSingular } from '@objectstack/spec/shared' ;
1717import { SchemaRegistry , computeFQN } from './registry.js' ;
18+ import { compileFormula , evaluateFormula } from './formula.js' ;
19+
20+ interface FormulaPlanEntry { name : string ; expression : string ; }
21+
22+ function planFormulaProjection (
23+ schema : any ,
24+ requestedFields : string [ ] | undefined
25+ ) : { plan : FormulaPlanEntry [ ] ; projected ?: string [ ] } {
26+ if ( ! schema ?. fields || ! Array . isArray ( requestedFields ) || requestedFields . length === 0 ) {
27+ return { plan : [ ] } ;
28+ }
29+ const plan : FormulaPlanEntry [ ] = [ ] ;
30+ const projected = new Set < string > ( ) ;
31+ for ( const f of requestedFields ) {
32+ const def = ( schema . fields as any ) [ f ] ;
33+ if ( def ?. type === 'formula' && def . expression ) {
34+ plan . push ( { name : f , expression : def . expression } ) ;
35+ try {
36+ for ( const dep of compileFormula ( def . expression ) . dependencies ) {
37+ if ( ( schema . fields as any ) [ dep ] ) projected . add ( dep ) ;
38+ }
39+ } catch {
40+ // ignore broken formulas at planning stage
41+ }
42+ } else {
43+ projected . add ( f ) ;
44+ }
45+ }
46+ if ( plan . length === 0 ) return { plan : [ ] } ;
47+ if ( ! projected . has ( 'id' ) ) projected . add ( 'id' ) ;
48+ return { plan, projected : Array . from ( projected ) } ;
49+ }
50+
51+ function applyFormulaPlan ( plan : FormulaPlanEntry [ ] , records : any [ ] ) : void {
52+ if ( ! plan . length ) return ;
53+ for ( const rec of records ) {
54+ if ( rec == null ) continue ;
55+ for ( const fp of plan ) {
56+ rec [ fp . name ] = evaluateFormula ( fp . expression , rec ) ;
57+ }
58+ }
59+ }
1860
1961export type HookHandler = ( context : HookContext ) => Promise < void > | void ;
2062
@@ -939,6 +981,13 @@ export class ObjectQL implements IDataEngine {
939981 }
940982 delete ( ast as any ) . top ;
941983
984+ // Plan formula projection: rewrite ast.fields to drop virtual formula
985+ // names and inject their dependencies, so the driver returns the raw
986+ // fields needed to compute the formulas after fetch.
987+ const _findSchema = this . _registry . getObject ( object ) ;
988+ const _findFormula = planFormulaProjection ( _findSchema , ast . fields as string [ ] | undefined ) ;
989+ if ( _findFormula . projected ) ast . fields = _findFormula . projected ;
990+
942991 const opCtx : OperationContext = {
943992 object,
944993 operation : 'find' ,
@@ -961,6 +1010,9 @@ export class ObjectQL implements IDataEngine {
9611010 try {
9621011 let result = await driver . find ( object , hookContext . input . ast as QueryAST , hookContext . input . options as any ) ;
9631012
1013+ // Post-process: evaluate formula virtual fields against the raw rows
1014+ if ( Array . isArray ( result ) ) applyFormulaPlan ( _findFormula . plan , result ) ;
1015+
9641016 // Post-process: expand related records if expand is requested
9651017 if ( ast . expand && Object . keys ( ast . expand ) . length > 0 && Array . isArray ( result ) ) {
9661018 result = await this . expandRelatedRecords ( object , result , ast . expand , 0 ) ;
@@ -989,6 +1041,12 @@ export class ObjectQL implements IDataEngine {
9891041 delete ( ast as any ) . context ;
9901042 delete ( ast as any ) . top ;
9911043
1044+ // Plan formula projection (same as find): rewrite ast.fields so the driver
1045+ // returns the raw dependency fields, then evaluate formulas after fetch.
1046+ const _findOneSchema = this . _registry . getObject ( objectName ) ;
1047+ const _findOneFormula = planFormulaProjection ( _findOneSchema , ast . fields as string [ ] | undefined ) ;
1048+ if ( _findOneFormula . projected ) ast . fields = _findOneFormula . projected ;
1049+
9921050 const opCtx : OperationContext = {
9931051 object : objectName ,
9941052 operation : 'findOne' ,
@@ -1000,6 +1058,9 @@ export class ObjectQL implements IDataEngine {
10001058 await this . executeWithMiddleware ( opCtx , async ( ) => {
10011059 let result = await driver . findOne ( objectName , opCtx . ast as QueryAST ) ;
10021060
1061+ // Post-process: evaluate formula virtual fields against the raw row
1062+ if ( result != null ) applyFormulaPlan ( _findOneFormula . plan , [ result ] ) ;
1063+
10031064 // Post-process: expand related records if expand is requested
10041065 if ( ast . expand && Object . keys ( ast . expand ) . length > 0 && result != null ) {
10051066 const expanded = await this . expandRelatedRecords ( objectName , [ result ] , ast . expand , 0 ) ;
0 commit comments