11// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22
33import { ObjectQL } from './engine.js' ;
4+ import { MetadataFacade } from './metadata-facade.js' ;
45import { ObjectStackProtocolImplementation } from './protocol.js' ;
56import { Plugin , PluginContext } from '@objectstack/core' ;
67
@@ -33,7 +34,7 @@ export class ObjectQLPlugin implements Plugin {
3334 // Register as provider for Core Kernel Services
3435 ctx . registerService ( 'objectql' , this . ql ) ;
3536
36- // Respect existing metadata service (e.g. from MetadataPlugin )
37+ // Register MetadataFacade as metadata service (unless external service exists )
3738 let hasMetadata = false ;
3839 let metadataProvider = 'objectql' ;
3940 try {
@@ -47,8 +48,9 @@ export class ObjectQLPlugin implements Plugin {
4748
4849 if ( ! hasMetadata ) {
4950 try {
50- ctx . registerService ( 'metadata' , this . ql ) ;
51- ctx . logger . info ( 'ObjectQL providing metadata service (fallback mode)' , {
51+ const metadataFacade = new MetadataFacade ( ) ;
52+ ctx . registerService ( 'metadata' , metadataFacade ) ;
53+ ctx . logger . info ( 'MetadataFacade registered as metadata service' , {
5254 mode : 'in-memory' ,
5355 features : [ 'registry' , 'fast-lookup' ]
5456 } ) ;
@@ -88,7 +90,8 @@ export class ObjectQLPlugin implements Plugin {
8890 // Check if we should load from external metadata service
8991 try {
9092 const metadataService = ctx . getService ( 'metadata' ) as any ;
91- if ( metadataService && metadataService !== this . ql && this . ql ) {
93+ // Only sync if metadata service is external (not our own MetadataFacade)
94+ if ( metadataService && ! ( metadataService instanceof MetadataFacade ) && this . ql ) {
9295 await this . loadMetadataFromService ( metadataService , ctx ) ;
9396 }
9497 } catch ( e : any ) {
@@ -112,13 +115,119 @@ export class ObjectQLPlugin implements Plugin {
112115 }
113116 }
114117 }
118+
119+ // Register built-in audit hooks
120+ this . registerAuditHooks ( ctx ) ;
121+
122+ // Register tenant isolation middleware
123+ this . registerTenantMiddleware ( ctx ) ;
115124
116125 ctx . logger . info ( 'ObjectQL engine started' , {
117126 driversRegistered : this . ql ?. [ 'drivers' ] ?. size || 0 ,
118127 objectsRegistered : this . ql ?. registry ?. getAllObjects ?.( ) ?. length || 0
119128 } ) ;
120129 }
121130
131+ /**
132+ * Register built-in audit hooks for auto-stamping createdBy/modifiedBy
133+ * and fetching previousData for update/delete operations.
134+ */
135+ private registerAuditHooks ( ctx : PluginContext ) {
136+ if ( ! this . ql ) return ;
137+
138+ // Auto-stamp createdBy/modifiedBy on insert
139+ this . ql . registerHook ( 'beforeInsert' , async ( hookCtx ) => {
140+ if ( hookCtx . session ?. userId && hookCtx . input ?. data ) {
141+ const data = hookCtx . input . data as Record < string , any > ;
142+ if ( typeof data === 'object' && data !== null ) {
143+ data . created_by = data . created_by ?? hookCtx . session . userId ;
144+ data . modified_by = hookCtx . session . userId ;
145+ data . created_at = data . created_at ?? new Date ( ) . toISOString ( ) ;
146+ data . modified_at = new Date ( ) . toISOString ( ) ;
147+ if ( hookCtx . session . tenantId ) {
148+ data . space_id = data . space_id ?? hookCtx . session . tenantId ;
149+ }
150+ }
151+ }
152+ } , { object : '*' , priority : 10 } ) ;
153+
154+ // Auto-stamp modifiedBy on update
155+ this . ql . registerHook ( 'beforeUpdate' , async ( hookCtx ) => {
156+ if ( hookCtx . session ?. userId && hookCtx . input ?. data ) {
157+ const data = hookCtx . input . data as Record < string , any > ;
158+ if ( typeof data === 'object' && data !== null ) {
159+ data . modified_by = hookCtx . session . userId ;
160+ data . modified_at = new Date ( ) . toISOString ( ) ;
161+ }
162+ }
163+ } , { object : '*' , priority : 10 } ) ;
164+
165+ // Auto-fetch previousData for update hooks
166+ this . ql . registerHook ( 'beforeUpdate' , async ( hookCtx ) => {
167+ if ( hookCtx . input ?. id && ! hookCtx . previous ) {
168+ try {
169+ const existing = await this . ql ! . findOne ( hookCtx . object , {
170+ filter : { _id : hookCtx . input . id }
171+ } ) ;
172+ if ( existing ) {
173+ hookCtx . previous = existing ;
174+ }
175+ } catch ( _e ) {
176+ // Non-fatal: some objects may not support findOne
177+ }
178+ }
179+ } , { object : '*' , priority : 5 } ) ;
180+
181+ // Auto-fetch previousData for delete hooks
182+ this . ql . registerHook ( 'beforeDelete' , async ( hookCtx ) => {
183+ if ( hookCtx . input ?. id && ! hookCtx . previous ) {
184+ try {
185+ const existing = await this . ql ! . findOne ( hookCtx . object , {
186+ filter : { _id : hookCtx . input . id }
187+ } ) ;
188+ if ( existing ) {
189+ hookCtx . previous = existing ;
190+ }
191+ } catch ( _e ) {
192+ // Non-fatal
193+ }
194+ }
195+ } , { object : '*' , priority : 5 } ) ;
196+
197+ ctx . logger . debug ( 'Audit hooks registered (createdBy/modifiedBy, previousData)' ) ;
198+ }
199+
200+ /**
201+ * Register tenant isolation middleware that auto-injects space_id filter
202+ * for multi-tenant operations.
203+ */
204+ private registerTenantMiddleware ( ctx : PluginContext ) {
205+ if ( ! this . ql ) return ;
206+
207+ this . ql . registerMiddleware ( async ( opCtx , next ) => {
208+ // Only apply to operations with tenantId that are not system-level
209+ if ( ! opCtx . context ?. tenantId || opCtx . context ?. isSystem ) {
210+ return next ( ) ;
211+ }
212+
213+ // Read operations: inject space_id filter into AST
214+ if ( [ 'find' , 'findOne' , 'count' , 'aggregate' ] . includes ( opCtx . operation ) ) {
215+ if ( opCtx . ast ) {
216+ const tenantFilter = { space_id : opCtx . context . tenantId } ;
217+ if ( opCtx . ast . where ) {
218+ opCtx . ast . where = { $and : [ opCtx . ast . where , tenantFilter ] } ;
219+ } else {
220+ opCtx . ast . where = tenantFilter ;
221+ }
222+ }
223+ }
224+
225+ await next ( ) ;
226+ } ) ;
227+
228+ ctx . logger . debug ( 'Tenant isolation middleware registered' ) ;
229+ }
230+
122231 /**
123232 * Load metadata from external metadata service into ObjectQL registry
124233 * This enables ObjectQL to use file-based or remote metadata
0 commit comments