@@ -292,6 +292,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
292292 kysely : AnyKysely ,
293293 model : string ,
294294 args : FindArgs < Schema , GetModels < Schema > , any , true > | undefined ,
295+ direct = false ,
295296 ) : Promise < any [ ] > {
296297 // table
297298 let query = this . dialect . buildSelectModel ( model , model ) ;
@@ -317,11 +318,23 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
317318
318319 query = query . modifyEnd ( this . makeContextComment ( { model, operation : 'read' } ) ) ;
319320
320- let result : any [ ] = [ ] ;
321321 const compiled = kysely . getExecutor ( ) . compileQuery ( query . toOperationNode ( ) , createQueryId ( ) ) ;
322+
323+ let result : any [ ] = [ ] ;
322324 try {
323- const r = await kysely . getExecutor ( ) . executeQuery ( compiled ) ;
324- result = r . rows ;
325+ if ( direct ) {
326+ // Bypass onKyselyQuery interceptors (e.g. policy plugin) so read-denied rows
327+ // are still reachable. Uses the outer executor for connection acquisition so
328+ // the query runs within an active transaction when applicable.
329+ const zenExecutor = ( this . client as any ) . kyselyProps . executor as ZenStackQueryExecutor ;
330+ const r = await kysely
331+ . getExecutor ( )
332+ . provideConnection ( ( connection ) => zenExecutor . executeQueryDirect ( compiled , connection ) ) ;
333+ result = r . rows ;
334+ } else {
335+ const r = await kysely . getExecutor ( ) . executeQuery ( compiled ) ;
336+ result = r . rows ;
337+ }
325338 } catch ( err ) {
326339 // Re-throw ORMErrors (e.g. policy violations with custom error codes) as-is
327340 // to avoid wrapping them in a generic DBQueryError and losing their type/code.
@@ -332,8 +345,13 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
332345 return result ;
333346 }
334347
335- protected async readUnique ( kysely : AnyKysely , model : string , args : FindArgs < Schema , GetModels < Schema > , any , true > ) {
336- const result = await this . read ( kysely , model , { ...args , take : 1 } ) ;
348+ protected async readUnique (
349+ kysely : AnyKysely ,
350+ model : string ,
351+ args : FindArgs < Schema , GetModels < Schema > , any , true > ,
352+ direct = false ,
353+ ) {
354+ const result = await this . read ( kysely , model , { ...args , take : 1 } , direct ) ;
337355 return result [ 0 ] ?? null ;
338356 }
339357
@@ -1199,19 +1217,13 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
11991217 // For non-RETURNING dialects that require it (e.g. MySQL), the pre-load SELECT must
12001218 // bypass the read policy so that read-denied rows are still reachable and the UPDATE
12011219 // can run, allowing its own policy error codes to be surfaced.
1202- const bypassReadPolicyForPreload =
1203- ! this . dialect . supportsReturning && ! fromRelation && this . dialect . requiresUpdatePreloadBypassReadPolicy ;
1220+ const bypassReadPolicyForPreload = ! this . dialect . supportsReturning && ! fromRelation ;
12041221
12051222 // lazily load the entity to be updated
12061223 let thisEntity : any ;
12071224 const loadThisEntity = async ( ) => {
12081225 if ( thisEntity === undefined ) {
1209- thisEntity = bypassReadPolicyForPreload
1210- ? await this . readUniqueDirect ( kysely , model , {
1211- where : origWhere ,
1212- select : this . makeIdSelect ( model ) ,
1213- } as any )
1214- : ( ( await this . getEntityIds ( kysely , model , origWhere ) ) ?? null ) ;
1226+ thisEntity = ( await this . getEntityIds ( kysely , model , origWhere , bypassReadPolicyForPreload ) ) ?? null ;
12151227 if ( ! thisEntity && throwIfNotFound ) {
12161228 throw createNotFoundError ( model ) ;
12171229 }
@@ -2542,38 +2554,16 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
25422554 }
25432555
25442556 // Given a unique filter of a model, load the entity and return its id fields
2545- private getEntityIds ( kysely : AnyKysely , model : string , uniqueFilter : any ) {
2546- return this . readUnique ( kysely , model , {
2547- where : uniqueFilter ,
2548- select : this . makeIdSelect ( model ) ,
2549- } ) ;
2550- }
2551-
2552- // Like readUnique but bypasses onKyselyQuery interceptors (e.g. policy plugin).
2553- // Used for the MySQL update pre-load so read-denied rows are still reachable.
2554- private async readUniqueDirect (
2555- kysely : AnyKysely ,
2556- model : string ,
2557- args : FindArgs < Schema , GetModels < Schema > , any , true > ,
2558- ) : Promise < any | null > {
2559- let query = this . dialect . buildSelectModel ( model , model ) ;
2560- const argsWithTake = { ...args , take : 1 } ;
2561- query = this . dialect . buildFilterSortTake ( model , argsWithTake , query , model ) ;
2562- if ( 'select' in args && args . select ) {
2563- query = this . buildFieldSelection ( model , query , args . select , model ) ;
2564- } else {
2565- query = this . dialect . buildSelectAllFields ( model , query , ( args as any ) ?. omit , model ) ;
2566- }
2567- const queryNode = query . toOperationNode ( ) ;
2568- // In a transaction, kysely.getExecutor() is Kysely's wrapper — not ZenStackQueryExecutor.
2569- // Route connection acquisition through the outer executor; compile and execute on the base one.
2570- const outerExecutor = kysely . getExecutor ( ) ;
2571- const zenExecutor = ( this . client as any ) . kyselyProps . executor as ZenStackQueryExecutor ;
2572- const compiled = zenExecutor . compileQuery ( queryNode , createQueryId ( ) ) ;
2573- const r = await outerExecutor . provideConnection ( ( connection ) =>
2574- zenExecutor . executeQueryDirect ( compiled , connection ) ,
2557+ private getEntityIds ( kysely : AnyKysely , model : string , uniqueFilter : any , direct = false ) {
2558+ return this . readUnique (
2559+ kysely ,
2560+ model ,
2561+ {
2562+ where : uniqueFilter ,
2563+ select : this . makeIdSelect ( model ) ,
2564+ } ,
2565+ direct ,
25752566 ) ;
2576- return r . rows [ 0 ] ?? null ;
25772567 }
25782568
25792569 // Given multiple unique filters, load all matching entities and return their id fields in one query
0 commit comments