Description
Calling $unuseAll() or $unuse() on a transaction client returns a new client that runs queries outside the active transaction.
Reproduction
const client = new ZenStackClient(schema, {
dialect: new PostgresDialect({ pool }),
plugins: [new PolicyPlugin()],
});
await client.$transaction(async (tx) => {
// Get txid inside the transaction
const [{ txid: txTxid }] = await tx.$unuseAll().$queryRawUnsafe<{ txid: string }>(
'SELECT txid_current() AS txid'
);
// Get txid via $unuseAll()
const [{ txid: unuseTxid }] = await tx.$unuseAll().$queryRawUnsafe<{ txid: string }>(
'SELECT txid_current() AS txid'
);
console.log('tx txid:', txTxid);
console.log('$unuseAll() txid:', unuseTxid);
// These are DIFFERENT — $unuseAll() escaped the transaction
});
Output:
tx txid: 17474959
$unuseAll() txid: 17474960
Root cause
In client-impl.ts, $unuseAll() creates a new ClientImpl via new ClientImpl(this.schema, newOptions, this). The constructor always runs this.kysely = new Kysely(this.kyselyProps) (line 131), which creates a fresh Kysely instance from kyselyProps.
However, when inside a transaction, the transaction state lives in txClient.kysely (set to the Kysely Transaction object on line 257 of interactiveTransaction). The kyselyProps don't carry the transaction context, so the new client gets a non-transactional connection.
The same issue applies to $unuse() and $use().
Suggested fix
After constructing the new client, propagate the transaction Kysely instance:
$unuseAll() {
const newOptions = { ...this.options, plugins: [] };
const newClient = new ClientImpl(this.schema, newOptions, this);
newClient.inputValidator = new InputValidator(newClient, {
enabled: newOptions.validateInput !== false,
});
// Preserve transaction state
if (this.kysely.isTransaction) {
newClient.kysely = this.kysely;
}
return newClient;
}
Same pattern for $unuse(), $use(), and $setAuth().
Version
@zenstackhq/orm 3.4.6
Description
Calling
$unuseAll()or$unuse()on a transaction client returns a new client that runs queries outside the active transaction.Reproduction
Output:
Root cause
In
client-impl.ts,$unuseAll()creates a newClientImplvianew ClientImpl(this.schema, newOptions, this). The constructor always runsthis.kysely = new Kysely(this.kyselyProps)(line 131), which creates a fresh Kysely instance fromkyselyProps.However, when inside a transaction, the transaction state lives in
txClient.kysely(set to the KyselyTransactionobject on line 257 ofinteractiveTransaction). ThekyselyPropsdon't carry the transaction context, so the new client gets a non-transactional connection.The same issue applies to
$unuse()and$use().Suggested fix
After constructing the new client, propagate the transaction Kysely instance:
Same pattern for
$unuse(),$use(), and$setAuth().Version
@zenstackhq/orm3.4.6