@@ -10,7 +10,7 @@ import {
1010 EngineAggregateOptions ,
1111 EngineCountOptions
1212} from '@objectstack/spec/data' ;
13- import { parseAutonumberFormat , renderAutonumber , missingFieldValues } from '@objectstack/spec/data' ;
13+ import { parseAutonumberFormat , renderAutonumber , missingFieldValues , isTenancyDisabled } from '@objectstack/spec/data' ;
1414import { ExecutionContext , ExecutionContextSchema } from '@objectstack/spec/kernel' ;
1515import { IDataDriver , IDataEngine , Logger , createLogger , withTransientRetry , type RetryOptions } from '@objectstack/core' ;
1616import { SummaryRecomputeError , type SummaryRecomputeFailure } from './summary-errors.js' ;
@@ -781,25 +781,35 @@ export class ObjectQL implements IDataEngine {
781781 /**
782782 * Build the DriverOptions blob passed to every IDataDriver call.
783783 *
784- * Always carries `tenantId` from the active ExecutionContext so the
785- * driver can enforce per-tenant isolation (SQL driver auto-scopes reads
786- * and auto-injects the tenant column on writes). Existing user-supplied
787- * shapes (transactions, AST extras) are preserved by spreading them
788- * first.
784+ * Carries `tenantId` from the active ExecutionContext so the driver can
785+ * enforce per-tenant isolation (SQL driver auto-scopes reads and
786+ * auto-injects the tenant column on writes) — EXCEPT for objects that
787+ * declare `tenancy.enabled: false` (ADR-0066 platform-global posture,
788+ * e.g. `sys_license`): stamping the caller's active-org tenantId there
789+ * would org-scope a global catalog at the driver, and its NULL-org rows
790+ * would vanish for authenticated org-context reads while anonymous
791+ * reads still see them (#3249). The SQL driver has its own opt-out
792+ * (sticky tenant-field cache), but withholding tenantId here protects
793+ * every driver at the source. Existing user-supplied shapes
794+ * (transactions, AST extras) are preserved by spreading them first — an
795+ * explicitly-passed `base.tenantId` is deliberate caller intent and
796+ * still wins.
789797 *
790798 * System / isSystem callers may still cross tenants by clearing
791799 * `tenantId` themselves on the resulting object; this helper does not
792800 * mask the system path.
793801 */
794- private buildDriverOptions ( execCtx ?: ExecutionContext , base ?: any ) : any {
802+ private buildDriverOptions ( object : string , execCtx ?: ExecutionContext , base ?: any ) : any {
795803 // The open transaction may arrive explicitly via the context, or ambiently
796804 // via txStore when an internal query runs during a transactional write
797805 // (ADR-0034). Explicit wins; ambient is the safety net.
798806 const tx = execCtx ?. transaction !== undefined
799807 ? execCtx . transaction
800808 : this . txStore . getStore ( ) ?. transaction ;
801809 const hasTx = tx !== undefined ;
802- const hasTenant = execCtx ?. tenantId !== undefined ;
810+ const hasTenant =
811+ execCtx ?. tenantId !== undefined &&
812+ ! isTenancyDisabled ( this . _registry . getObject ( object ) ) ;
803813 const hasTz = execCtx ?. timezone !== undefined ;
804814 const isSystem = execCtx ?. isSystem === true ;
805815 if ( ! hasTx && ! hasTenant && ! isSystem && ! hasTz ) return base ;
@@ -2183,7 +2193,7 @@ export class ObjectQL implements IDataEngine {
21832193 ql : this
21842194 } ;
21852195 await this . triggerHooks ( 'beforeFind' , hookContext ) ;
2186- hookContext . input . options = this . buildDriverOptions ( opCtx . context , hookContext . input . options as any ) ;
2196+ hookContext . input . options = this . buildDriverOptions ( object , opCtx . context , hookContext . input . options as any ) ;
21872197
21882198 try {
21892199 let result = await driver . find ( object , hookContext . input . ast as QueryAST , hookContext . input . options as any ) ;
@@ -2265,7 +2275,7 @@ export class ObjectQL implements IDataEngine {
22652275 ql : this
22662276 } ;
22672277 await this . triggerHooks ( 'beforeFind' , hookContext ) ;
2268- hookContext . input . options = this . buildDriverOptions ( opCtx . context , hookContext . input . options as any ) ;
2278+ hookContext . input . options = this . buildDriverOptions ( objectName , opCtx . context , hookContext . input . options as any ) ;
22692279
22702280 let result = await driver . findOne ( objectName , hookContext . input . ast as QueryAST , hookContext . input . options as any ) ;
22712281
@@ -2362,7 +2372,7 @@ export class ObjectQL implements IDataEngine {
23622372 // Base the merge on the first row context's options: hooks share the
23632373 // same underlying options object (in-place mutations are visible), and
23642374 // for single inserts this is exactly the pre-#2922 behaviour.
2365- const driverOptions = this . buildDriverOptions ( opCtx . context , rowHookContexts [ 0 ] ?. input . options as any ) ;
2375+ const driverOptions = this . buildDriverOptions ( object , opCtx . context , rowHookContexts [ 0 ] ?. input . options as any ) ;
23662376 for ( const rowCtx of rowHookContexts ) {
23672377 rowCtx . input . options = driverOptions ;
23682378 }
@@ -2620,7 +2630,7 @@ export class ObjectQL implements IDataEngine {
26202630 ql : this
26212631 } ;
26222632 await this . triggerHooks ( 'beforeUpdate' , hookContext ) ;
2623- hookContext . input . options = this . buildDriverOptions ( opCtx . context , hookContext . input . options as any ) ;
2633+ hookContext . input . options = this . buildDriverOptions ( object , opCtx . context , hookContext . input . options as any ) ;
26242634
26252635 try {
26262636 let result ;
@@ -2945,7 +2955,7 @@ export class ObjectQL implements IDataEngine {
29452955 ql : this
29462956 } ;
29472957 await this . triggerHooks ( 'beforeDelete' , hookContext ) ;
2948- hookContext . input . options = this . buildDriverOptions ( opCtx . context , hookContext . input . options as any ) ;
2958+ hookContext . input . options = this . buildDriverOptions ( object , opCtx . context , hookContext . input . options as any ) ;
29492959
29502960 try {
29512961 let result ;
@@ -3041,7 +3051,7 @@ export class ObjectQL implements IDataEngine {
30413051 } ;
30423052
30433053 await this . executeWithMiddleware ( opCtx , async ( ) => {
3044- const countOpts = this . buildDriverOptions ( opCtx . context ) ;
3054+ const countOpts = this . buildDriverOptions ( object , opCtx . context ) ;
30453055 if ( driver . count ) {
30463056 return driver . count ( object , opCtx . ast as QueryAST , countOpts ) ;
30473057 }
@@ -3149,15 +3159,15 @@ export class ObjectQL implements IDataEngine {
31493159 const hasDateBucket = structuredItems . some ( ( g : any ) => ! ! g ?. dateGranularity ) ;
31503160 const tzRequiresInMemory = ! ! tz && tz !== 'UTC' && hasDateBucket ;
31513161 if ( typeof drv . aggregate === 'function' && allStructuredSupported && ! tzRequiresInMemory ) {
3152- return drv . aggregate ( object , ast , this . buildDriverOptions ( opCtx . context ) ) ;
3162+ return drv . aggregate ( object , ast , this . buildDriverOptions ( object , opCtx . context ) ) ;
31533163 }
31543164 // In-memory fallback path: ask the driver for raw rows, then bucket +
31553165 // aggregate here. This guarantees `groupBy` (incl. structured items
31563166 // carrying `dateGranularity`) and `aggregations` always work even on
31573167 // drivers that have no native aggregation support (driver-rest,
31583168 // driver-memory, partial SQL drivers), and is the path that honours a
31593169 // non-UTC reference timezone.
3160- const raw = await driver . find ( object , ast , this . buildDriverOptions ( opCtx . context ) ) ;
3170+ const raw = await driver . find ( object , ast , this . buildDriverOptions ( object , opCtx . context ) ) ;
31613171 return applyInMemoryAggregation ( raw , ast , tz ) ;
31623172 } ) ;
31633173
0 commit comments