@@ -82,6 +82,9 @@ export class ObjectQL implements IDataEngine {
8282 fn : EngineMiddleware ;
8383 object ?: string ;
8484 } > = [ ] ;
85+
86+ // Action registry: key = "objectName:actionName"
87+ private actions = new Map < string , { handler : ( ctx : any ) => Promise < any > | any ; package ?: string } > ( ) ;
8588
8689 // Host provided context additions (e.g. Server router)
8790 private hostContext : Record < string , any > = { } ;
@@ -195,6 +198,47 @@ export class ObjectQL implements IDataEngine {
195198 }
196199 }
197200
201+ // ========================================
202+ // Action System
203+ // ========================================
204+
205+ /**
206+ * Register a named action on an object.
207+ * Actions are custom business logic callable via `repo.execute(actionName, params)`.
208+ *
209+ * @param objectName Target object
210+ * @param actionName Unique action name within the object
211+ * @param handler Handler function
212+ * @param packageName Optional package owner (for cleanup)
213+ */
214+ registerAction ( objectName : string , actionName : string , handler : ( ctx : any ) => Promise < any > | any , packageName ?: string ) : void {
215+ const key = `${ objectName } :${ actionName } ` ;
216+ this . actions . set ( key , { handler, package : packageName } ) ;
217+ this . logger . debug ( 'Registered action' , { objectName, actionName, package : packageName } ) ;
218+ }
219+
220+ /**
221+ * Execute a named action on an object.
222+ */
223+ async executeAction ( objectName : string , actionName : string , ctx : any ) : Promise < any > {
224+ const entry = this . actions . get ( `${ objectName } :${ actionName } ` ) ;
225+ if ( ! entry ) {
226+ throw new Error ( `Action '${ actionName } ' on object '${ objectName } ' not found` ) ;
227+ }
228+ return entry . handler ( ctx ) ;
229+ }
230+
231+ /**
232+ * Remove all actions registered by a specific package.
233+ */
234+ removeActionsByPackage ( packageName : string ) : void {
235+ for ( const [ key , entry ] of this . actions . entries ( ) ) {
236+ if ( entry . package === packageName ) {
237+ this . actions . delete ( key ) ;
238+ }
239+ }
240+ }
241+
198242 /**
199243 * Register a middleware function
200244 * Middlewares execute in onion model around every data operation.
@@ -930,12 +974,16 @@ export class ObjectQL implements IDataEngine {
930974
931975/**
932976 * Repository scoped to a single object, bound to an execution context.
977+ *
978+ * Provides both IDataEngine-style methods (find, insert, update, delete)
979+ * and convenience aliases (create, updateById, deleteById) matching
980+ * the @objectql/core ObjectRepository API.
933981 */
934982export class ObjectRepository {
935983 constructor (
936984 private objectName : string ,
937985 private context : ExecutionContext ,
938- private engine : IDataEngine
986+ private engine : IDataEngine & { executeAction ?: ( o : string , a : string , c : any ) => Promise < any > }
939987 ) { }
940988
941989 async find ( query : any = { } ) : Promise < any [ ] > {
@@ -958,30 +1006,76 @@ export class ObjectRepository {
9581006 } ) ;
9591007 }
9601008
1009+ /** Alias for insert() — matches @objectql/core convention */
1010+ async create ( data : any ) : Promise < any > {
1011+ return this . insert ( data ) ;
1012+ }
1013+
9611014 async update ( data : any , options : any = { } ) : Promise < any > {
9621015 return this . engine . update ( this . objectName , data , {
9631016 ...options ,
9641017 context : this . context ,
9651018 } ) ;
9661019 }
9671020
1021+ /** Update a single record by ID */
1022+ async updateById ( id : string | number , data : any ) : Promise < any > {
1023+ return this . engine . update ( this . objectName , { ...data , _id : id } , {
1024+ filter : { _id : id } ,
1025+ context : this . context ,
1026+ } ) ;
1027+ }
1028+
9681029 async delete ( options : any = { } ) : Promise < any > {
9691030 return this . engine . delete ( this . objectName , {
9701031 ...options ,
9711032 context : this . context ,
9721033 } ) ;
9731034 }
9741035
1036+ /** Delete a single record by ID */
1037+ async deleteById ( id : string | number ) : Promise < any > {
1038+ return this . engine . delete ( this . objectName , {
1039+ filter : { _id : id } ,
1040+ context : this . context ,
1041+ } ) ;
1042+ }
1043+
9751044 async count ( query : any = { } ) : Promise < number > {
9761045 return this . engine . count ( this . objectName , {
9771046 ...query ,
9781047 context : this . context ,
9791048 } ) ;
9801049 }
1050+
1051+ /** Aggregate query */
1052+ async aggregate ( query : any = { } ) : Promise < any [ ] > {
1053+ return this . engine . aggregate ( this . objectName , {
1054+ ...query ,
1055+ context : this . context ,
1056+ } ) ;
1057+ }
1058+
1059+ /** Execute a named action registered on this object */
1060+ async execute ( actionName : string , params ?: any ) : Promise < any > {
1061+ if ( this . engine . executeAction ) {
1062+ return this . engine . executeAction ( this . objectName , actionName , {
1063+ ...params ,
1064+ userId : this . context . userId ,
1065+ tenantId : this . context . tenantId ,
1066+ roles : this . context . roles ,
1067+ } ) ;
1068+ }
1069+ throw new Error ( `Actions not supported by engine` ) ;
1070+ }
9811071}
9821072
9831073/**
9841074 * Scoped execution context with object() accessor.
1075+ *
1076+ * Provides identity (userId, tenantId/spaceId, roles),
1077+ * repository access via object(), privilege escalation via sudo(),
1078+ * and transactional execution via transaction().
9851079 */
9861080export class ScopedContext {
9871081 constructor (
@@ -991,7 +1085,7 @@ export class ScopedContext {
9911085
9921086 /** Get a repository scoped to this context */
9931087 object ( name : string ) : ObjectRepository {
994- return new ObjectRepository ( name , this . executionContext , this . engine ) ;
1088+ return new ObjectRepository ( name , this . executionContext , this . engine as any ) ;
9951089 }
9961090
9971091 /** Create an elevated (system) context */
@@ -1002,7 +1096,54 @@ export class ScopedContext {
10021096 ) ;
10031097 }
10041098
1099+ /**
1100+ * Execute a callback within a database transaction.
1101+ *
1102+ * The callback receives a new ScopedContext whose operations
1103+ * share the same transaction handle. If the callback throws,
1104+ * the transaction is rolled back; otherwise it is committed.
1105+ *
1106+ * Falls back to non-transactional execution if the driver
1107+ * does not support transactions.
1108+ */
1109+ async transaction ( callback : ( trxCtx : ScopedContext ) => Promise < any > ) : Promise < any > {
1110+ const engine = this . engine as any ;
1111+
1112+ // Find the default driver for transaction support
1113+ const driver = engine . defaultDriver
1114+ ? engine . drivers ?. get ( engine . defaultDriver )
1115+ : undefined ;
1116+
1117+ if ( ! driver ?. beginTransaction ) {
1118+ // No transaction support — execute directly
1119+ return callback ( this ) ;
1120+ }
1121+
1122+ const trx = await driver . beginTransaction ( ) ;
1123+ const trxCtx = new ScopedContext (
1124+ { ...this . executionContext , transaction : trx } ,
1125+ this . engine
1126+ ) ;
1127+
1128+ try {
1129+ const result = await callback ( trxCtx ) ;
1130+ if ( driver . commit ) await driver . commit ( trx ) ;
1131+ else if ( driver . commitTransaction ) await driver . commitTransaction ( trx ) ;
1132+ return result ;
1133+ } catch ( error ) {
1134+ if ( driver . rollback ) await driver . rollback ( trx ) ;
1135+ else if ( driver . rollbackTransaction ) await driver . rollbackTransaction ( trx ) ;
1136+ throw error ;
1137+ }
1138+ }
1139+
10051140 get userId ( ) { return this . executionContext . userId ; }
10061141 get tenantId ( ) { return this . executionContext . tenantId ; }
1142+ /** Alias for tenantId — matches ObjectQLContext.spaceId convention */
1143+ get spaceId ( ) { return this . executionContext . tenantId ; }
10071144 get roles ( ) { return this . executionContext . roles ; }
1145+ get isSystem ( ) { return this . executionContext . isSystem ; }
1146+
1147+ /** Internal: expose the transaction handle for driver-level access */
1148+ get transactionHandle ( ) { return this . executionContext . transaction ; }
10081149}
0 commit comments