@@ -126,95 +126,118 @@ export class FormulaPlugin implements RuntimePlugin {
126126 // Note: formulaEngine is already registered in install() method above
127127 }
128128
129+ /**
130+ * Evaluate formula fields on a set of records for a given object.
131+ * @private
132+ */
133+ private evaluateFormulas ( objectName : string , result : any , session ?: any ) : void {
134+ if ( ! result ) return ;
135+
136+ // Get schema from MetadataRegistry or kernel.getObject()
137+ const schemaItem = this . kernel . metadata ?. get ?.( 'object' , objectName )
138+ ?? ( typeof this . kernel . getObject === 'function' ? this . kernel . getObject ( objectName ) : undefined ) ;
139+ const schema = schemaItem ?. content || schemaItem ;
140+ if ( ! schema || ! schema . fields ) {
141+ if ( objectName ) {
142+ this . logger . debug ( 'No schema found for object, skipping formula evaluation' , { objectName } ) ;
143+ }
144+ return ;
145+ }
146+
147+ // Identify formula fields
148+ const formulaFields : [ string , any ] [ ] = [ ] ;
149+ for ( const [ key , field ] of Object . entries ( schema . fields ) as any [ ] ) {
150+ if ( field . type === 'formula' && field . expression ) {
151+ formulaFields . push ( [ key , field ] ) ;
152+ }
153+ }
154+ if ( formulaFields . length === 0 ) return ;
155+
156+ const results = Array . isArray ( result ) ? result : [ result ] ;
157+ const now = new Date ( ) ;
158+ const systemInfo = {
159+ today : new Date ( now . getFullYear ( ) , now . getMonth ( ) , now . getDate ( ) ) ,
160+ now : now ,
161+ year : now . getFullYear ( ) ,
162+ month : now . getMonth ( ) + 1 ,
163+ day : now . getDate ( ) ,
164+ hour : now . getHours ( ) ,
165+ minute : now . getMinutes ( ) ,
166+ second : now . getSeconds ( ) ,
167+ } ;
168+
169+ for ( const record of results ) {
170+ if ( ! record ) continue ;
171+
172+ const formulaContext : FormulaContext = {
173+ record,
174+ system : systemInfo ,
175+ current_user : {
176+ id : session ?. userId || session ?. id || '' ,
177+ name : session ?. name ,
178+ email : session ?. email ,
179+ role : session ?. roles ?. [ 0 ]
180+ } ,
181+ is_new : false ,
182+ record_id : record . _id || record . id
183+ } ;
184+
185+ for ( const [ fieldName , fieldConfig ] of formulaFields ) {
186+ const evalResult = this . engine . evaluate (
187+ fieldConfig . expression ,
188+ formulaContext ,
189+ fieldConfig . data_type || 'text' ,
190+ { strict : true }
191+ ) ;
192+
193+ if ( evalResult . success ) {
194+ record [ fieldName ] = evalResult . value ;
195+ } else {
196+ record [ fieldName ] = null ;
197+ this . logger . error ( '[ObjectQL][FormulaEngine] Formula evaluation failed' , undefined , {
198+ objectName,
199+ fieldName,
200+ recordId : formulaContext . record_id ,
201+ expression : fieldConfig . expression ,
202+ error : evalResult . error ,
203+ stack : evalResult . stack ,
204+ } ) ;
205+ }
206+ }
207+ }
208+ }
209+
129210 /**
130211 * Register formula evaluation middleware
131212 * @private
132213 */
133214 private registerFormulaMiddleware ( kernel : KernelWithFormulas , ctx : any ) : void {
215+ // Strategy 1: Register as engine middleware (covers both find and findOne)
216+ const engine = ctx . engine || kernel ;
217+ if ( typeof engine . registerMiddleware === 'function' ) {
218+ engine . registerMiddleware ( async ( opCtx : any , next : ( ) => Promise < void > ) => {
219+ await next ( ) ;
220+ if ( ( opCtx . operation === 'find' || opCtx . operation === 'findOne' ) && opCtx . result ) {
221+ this . evaluateFormulas ( opCtx . object , opCtx . result , opCtx . context ) ;
222+ }
223+ } ) ;
224+ return ;
225+ }
226+
227+ // Strategy 2: Register as afterFind hook (find only, no findOne coverage)
134228 const registerHook = ( name : string , handler : any ) => {
135229 if ( typeof ctx . hook === 'function' ) {
136230 ctx . hook ( name , handler ) ;
137231 } else if ( kernel . hooks && typeof kernel . hooks . register === 'function' ) {
138- // Register for all objects using wildcard
139232 kernel . hooks . register ( name , '*' , handler ) ;
140233 }
141234 } ;
142235
143236 registerHook ( 'afterFind' , async ( context : any ) => {
144- // context is RetrievalHookContext
145- const { objectName, result, user } = context ;
146- if ( ! result ) return ;
147-
148- // Get schema
149- const schemaItem = this . kernel . metadata . get ( 'object' , objectName ) ;
150- const schema = schemaItem ?. content || schemaItem ;
151- if ( ! schema || ! schema . fields ) {
152- return ;
153- }
154-
155- // Identify formula fields
156- const formulaFields : [ string , any ] [ ] = [ ] ;
157- for ( const [ key , field ] of Object . entries ( schema . fields ) as any [ ] ) {
158- if ( field . type === 'formula' && field . expression ) {
159- formulaFields . push ( [ key , field ] ) ;
160- }
161- }
162-
163- if ( formulaFields . length === 0 ) return ;
164-
165- const results = Array . isArray ( result ) ? result : [ result ] ;
166- const now = new Date ( ) ;
167- const systemInfo = {
168- today : new Date ( now . getFullYear ( ) , now . getMonth ( ) , now . getDate ( ) ) ,
169- now : now ,
170- year : now . getFullYear ( ) ,
171- month : now . getMonth ( ) + 1 ,
172- day : now . getDate ( ) ,
173- hour : now . getHours ( ) ,
174- minute : now . getMinutes ( ) ,
175- second : now . getSeconds ( ) ,
176- } ;
177-
178- for ( const record of results ) {
179- if ( ! record ) continue ;
180-
181- const formulaContext : FormulaContext = {
182- record,
183- system : systemInfo ,
184- current_user : {
185- id : user ?. id || '' ,
186- name : user ?. name ,
187- email : user ?. email ,
188- role : user ?. roles ?. [ 0 ]
189- } ,
190- is_new : false ,
191- record_id : record . _id || record . id
192- } ;
193-
194- for ( const [ fieldName , fieldConfig ] of formulaFields ) {
195- const evalResult = this . engine . evaluate (
196- fieldConfig . expression ,
197- formulaContext ,
198- fieldConfig . data_type || 'text' ,
199- { strict : true }
200- ) ;
201-
202- if ( evalResult . success ) {
203- record [ fieldName ] = evalResult . value ;
204- } else {
205- record [ fieldName ] = null ;
206- // Log specific error as seen in repository.ts
207- this . logger . error ( '[ObjectQL][FormulaEngine] Formula evaluation failed' , undefined , {
208- objectName : objectName ,
209- fieldName,
210- recordId : formulaContext . record_id ,
211- expression : fieldConfig . expression ,
212- error : evalResult . error ,
213- stack : evalResult . stack ,
214- } ) ;
215- }
216- }
217- }
237+ // Upstream hook context uses 'object' (not 'objectName') and 'session' (not 'user')
238+ const objectName = context . object || context . objectName ;
239+ const session = context . session || context . user ;
240+ this . evaluateFormulas ( objectName , context . result , session ) ;
218241 } ) ;
219242 }
220243
0 commit comments