@@ -126,95 +126,113 @@ 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 ) return ;
141+
142+ // Identify formula fields
143+ const formulaFields : [ string , any ] [ ] = [ ] ;
144+ for ( const [ key , field ] of Object . entries ( schema . fields ) as any [ ] ) {
145+ if ( field . type === 'formula' && field . expression ) {
146+ formulaFields . push ( [ key , field ] ) ;
147+ }
148+ }
149+ if ( formulaFields . length === 0 ) return ;
150+
151+ const results = Array . isArray ( result ) ? result : [ result ] ;
152+ const now = new Date ( ) ;
153+ const systemInfo = {
154+ today : new Date ( now . getFullYear ( ) , now . getMonth ( ) , now . getDate ( ) ) ,
155+ now : now ,
156+ year : now . getFullYear ( ) ,
157+ month : now . getMonth ( ) + 1 ,
158+ day : now . getDate ( ) ,
159+ hour : now . getHours ( ) ,
160+ minute : now . getMinutes ( ) ,
161+ second : now . getSeconds ( ) ,
162+ } ;
163+
164+ for ( const record of results ) {
165+ if ( ! record ) continue ;
166+
167+ const formulaContext : FormulaContext = {
168+ record,
169+ system : systemInfo ,
170+ current_user : {
171+ id : session ?. userId || '' ,
172+ name : session ?. name ,
173+ email : session ?. email ,
174+ role : session ?. roles ?. [ 0 ]
175+ } ,
176+ is_new : false ,
177+ record_id : record . _id || record . id
178+ } ;
179+
180+ for ( const [ fieldName , fieldConfig ] of formulaFields ) {
181+ const evalResult = this . engine . evaluate (
182+ fieldConfig . expression ,
183+ formulaContext ,
184+ fieldConfig . data_type || 'text' ,
185+ { strict : true }
186+ ) ;
187+
188+ if ( evalResult . success ) {
189+ record [ fieldName ] = evalResult . value ;
190+ } else {
191+ record [ fieldName ] = null ;
192+ this . logger . error ( '[ObjectQL][FormulaEngine] Formula evaluation failed' , undefined , {
193+ objectName,
194+ fieldName,
195+ recordId : formulaContext . record_id ,
196+ expression : fieldConfig . expression ,
197+ error : evalResult . error ,
198+ stack : evalResult . stack ,
199+ } ) ;
200+ }
201+ }
202+ }
203+ }
204+
129205 /**
130206 * Register formula evaluation middleware
131207 * @private
132208 */
133209 private registerFormulaMiddleware ( kernel : KernelWithFormulas , ctx : any ) : void {
210+ // Strategy 1: Register as engine middleware (covers both find and findOne)
211+ const engine = ctx . engine || kernel ;
212+ if ( typeof engine . registerMiddleware === 'function' ) {
213+ engine . registerMiddleware ( async ( opCtx : any , next : ( ) => Promise < void > ) => {
214+ await next ( ) ;
215+ if ( ( opCtx . operation === 'find' || opCtx . operation === 'findOne' ) && opCtx . result ) {
216+ this . evaluateFormulas ( opCtx . object , opCtx . result , opCtx . context ) ;
217+ }
218+ } ) ;
219+ return ;
220+ }
221+
222+ // Strategy 2: Register as afterFind hook (find only, no findOne coverage)
134223 const registerHook = ( name : string , handler : any ) => {
135224 if ( typeof ctx . hook === 'function' ) {
136225 ctx . hook ( name , handler ) ;
137226 } else if ( kernel . hooks && typeof kernel . hooks . register === 'function' ) {
138- // Register for all objects using wildcard
139227 kernel . hooks . register ( name , '*' , handler ) ;
140228 }
141229 } ;
142230
143231 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- }
232+ // Upstream hook context uses 'object' (not 'objectName') and 'session' (not 'user')
233+ const objectName = context . object || context . objectName ;
234+ const session = context . session || context . user ;
235+ this . evaluateFormulas ( objectName , context . result , session ) ;
218236 } ) ;
219237 }
220238
0 commit comments