From eaafe95c123d47ca59bcb07d1f274668242cc8dc Mon Sep 17 00:00:00 2001 From: evgenovalov Date: Tue, 4 Aug 2026 15:19:41 +0200 Subject: [PATCH 1/3] feat: provide the client in the computed field context Computed field implementations receive (eb, { modelAlias }, args) and cannot read per-client state such as the auth context set via $setAuth. Pass the executing client in the context, mirroring what custom function implementations already get through ZModelFunctionContext. Also replaces the scattered 'as unknown as ClientContract' casts with a single ClientImpl.$contract accessor. Co-Authored-By: Claude Fable 5 --- packages/orm/src/client/client-impl.ts | 17 ++++++++-- .../src/client/crud/dialects/base-dialect.ts | 6 +++- .../orm/src/client/crud/dialects/index.ts | 8 +++-- .../orm/src/client/crud/dialects/mysql.ts | 5 +-- .../src/client/crud/dialects/postgresql.ts | 5 +-- .../orm/src/client/crud/operations/base.ts | 2 +- .../orm/src/client/executor/name-mapper.ts | 2 +- .../executor/zenstack-query-executor.ts | 8 ++--- packages/orm/src/client/options.ts | 26 ++++++++++++-- .../orm/client-api/computed-fields.test.ts | 34 +++++++++++++++++++ 10 files changed, 94 insertions(+), 19 deletions(-) diff --git a/packages/orm/src/client/client-impl.ts b/packages/orm/src/client/client-impl.ts index 0fb4e4dbd..be256f1c5 100644 --- a/packages/orm/src/client/client-impl.ts +++ b/packages/orm/src/client/client-impl.ts @@ -253,7 +253,7 @@ export class ClientImpl { ): Promise { if (this.kysely.isTransaction) { // proceed directly if already in a transaction - return callback(this as unknown as ClientContract); + return callback(this.$contract); } else { // otherwise, create a new transaction, clone the client, and execute the callback let txBuilder = this.kysely.transaction(); @@ -263,7 +263,7 @@ export class ClientImpl { return txBuilder.execute((tx) => { const txClient = new ClientImpl(this.schema, this.$options, this); txClient.kysely = tx; - return callback(txClient as unknown as ClientContract); + return callback(txClient.$contract); }); } } @@ -285,7 +285,7 @@ export class ClientImpl { const result: any[] = []; for (const promise of arg) { const cb = this.getPromiseCallback(promise); - result.push(await cb(txClient as unknown as ClientContract)); + result.push(await cb(txClient.$contract)); } return result; }; @@ -446,6 +446,17 @@ export class ClientImpl { return this.auth; } + /** + * This client viewed through its public typed contract. `ClientImpl` is intentionally + * untyped internally — the model accessors are added by the runtime proxy — so this + * getter is the single sanctioned bridge to `ClientContract`. The proxy invokes it + * with the proxy as `this` (`Reflect.get` with receiver), so the returned reference + * keeps the model accessors. + */ + get $contract(): ClientContract { + return this as unknown as ClientContract; + } + $setOptions>(options: Options): ClientContract { const newClient = new ClientImpl(this.schema, options as ClientOptions, this); // create a new validator to have a fresh schema cache, because options may change validation settings diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 07abff83f..9eb497fb4 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -15,6 +15,7 @@ import type { SortOrder, StringFilter, } from '../../crud-types'; +import type { ClientContract } from '../../contract'; import { createConfigError, createInvalidInputError, createNotSupportedError } from '../../errors'; import type { ClientOptions } from '../../options'; import { @@ -42,6 +43,9 @@ export abstract class BaseCrudDialect { constructor( protected readonly schema: Schema, protected readonly options: ClientOptions, + // the client executing the query; optional so the dialect can still be + // constructed standalone (e.g. for output transformation only) + protected readonly client?: ClientContract, ) {} // #region capability flags1 @@ -1662,7 +1666,7 @@ export abstract class BaseCrudDialect { } // `computedArgs` is the query-time args object for a parameterized computed // field (undefined otherwise); forwarded as the implementation's 3rd argument. - return computer(this.eb, { modelAlias }, computedArgs); + return computer(this.eb, { modelAlias, client: this.client }, computedArgs); } } diff --git a/packages/orm/src/client/crud/dialects/index.ts b/packages/orm/src/client/crud/dialects/index.ts index 8dd8d25ea..eea929206 100644 --- a/packages/orm/src/client/crud/dialects/index.ts +++ b/packages/orm/src/client/crud/dialects/index.ts @@ -1,5 +1,6 @@ import type { SchemaDef } from '@zenstackhq/schema'; import { match } from 'ts-pattern'; +import type { ClientContract } from '../../contract'; import type { ClientOptions } from '../../options'; import type { BaseCrudDialect } from './base-dialect'; import { MySqlCrudDialect } from './mysql'; @@ -9,10 +10,11 @@ import { SqliteCrudDialect } from './sqlite'; export function getCrudDialect( schema: Schema, options: ClientOptions, + client?: ClientContract, ): BaseCrudDialect { return match(schema.provider.type) - .with('sqlite', () => new SqliteCrudDialect(schema, options)) - .with('postgresql', () => new PostgresCrudDialect(schema, options)) - .with('mysql', () => new MySqlCrudDialect(schema, options)) + .with('sqlite', () => new SqliteCrudDialect(schema, options, client)) + .with('postgresql', () => new PostgresCrudDialect(schema, options, client)) + .with('mysql', () => new MySqlCrudDialect(schema, options, client)) .exhaustive(); } diff --git a/packages/orm/src/client/crud/dialects/mysql.ts b/packages/orm/src/client/crud/dialects/mysql.ts index 2af95e2cd..9388f4b18 100644 --- a/packages/orm/src/client/crud/dialects/mysql.ts +++ b/packages/orm/src/client/crud/dialects/mysql.ts @@ -12,6 +12,7 @@ import { type SqlBool, } from 'kysely'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; +import type { ClientContract } from '../../contract'; import type { NullsOrder, SortOrder } from '../../crud-types'; import { createInvalidInputError, createNotSupportedError } from '../../errors'; import type { ClientOptions } from '../../options'; @@ -20,8 +21,8 @@ import type { FuzzyFilterOptions } from './base-dialect'; import { LateralJoinDialectBase } from './lateral-join-dialect-base'; export class MySqlCrudDialect extends LateralJoinDialectBase { - constructor(schema: Schema, options: ClientOptions) { - super(schema, options); + constructor(schema: Schema, options: ClientOptions, client?: ClientContract) { + super(schema, options, client); } override get provider() { diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index 67887729d..6461893ea 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -11,6 +11,7 @@ import { } from 'kysely'; import { parse as parsePostgresArray } from 'postgres-array'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; +import type { ClientContract } from '../../contract'; import type { NullsOrder, SortOrder } from '../../crud-types'; import { createInvalidInputError } from '../../errors'; import type { ClientOptions } from '../../options'; @@ -73,8 +74,8 @@ export class PostgresCrudDialect extends LateralJoinDi '@db.Boolean': 'boolean', }; - constructor(schema: Schema, options: ClientOptions) { - super(schema, options); + constructor(schema: Schema, options: ClientOptions, client?: ClientContract) { + super(schema, options, client); this.overrideTypeParsers(); } diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 2ef4ca043..6e9e707e8 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -199,7 +199,7 @@ export abstract class BaseOperationHandler { protected readonly model: GetModels, protected readonly inputValidator: InputValidator, ) { - this.dialect = getCrudDialect(this.schema, this.client.$options); + this.dialect = getCrudDialect(this.schema, this.client.$options, this.client); } protected get schema() { diff --git a/packages/orm/src/client/executor/name-mapper.ts b/packages/orm/src/client/executor/name-mapper.ts index e37a946d1..667e677f5 100644 --- a/packages/orm/src/client/executor/name-mapper.ts +++ b/packages/orm/src/client/executor/name-mapper.ts @@ -62,7 +62,7 @@ export class QueryNameMapper extends OperationNodeTransformer { constructor(private readonly client: ClientContract) { super(); - this.dialect = getCrudDialect(client.$schema, client.$options); + this.dialect = getCrudDialect(client.$schema, client.$options, client); for (const [modelName, modelDef] of Object.entries(client.$schema.models)) { const mappedName = this.getMappedName(modelDef); if (mappedName) { diff --git a/packages/orm/src/client/executor/zenstack-query-executor.ts b/packages/orm/src/client/executor/zenstack-query-executor.ts index ed4f6f6b1..d1723ac24 100644 --- a/packages/orm/src/client/executor/zenstack-query-executor.ts +++ b/packages/orm/src/client/executor/zenstack-query-executor.ts @@ -93,10 +93,10 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { client.$schema.provider.type === 'postgresql' || // postgres queries need to be schema-qualified this.schemaHasMappedNames(client.$schema) ) { - this.nameMapper = new QueryNameMapper(client as unknown as ClientContract); + this.nameMapper = new QueryNameMapper(client.$contract); } - this.dialect = getCrudDialect(client.$schema, client.$options); + this.dialect = getCrudDialect(client.$schema, client.$options, client.$contract); } private schemaHasMappedNames(schema: SchemaDef) { @@ -210,7 +210,7 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { proceed = async (query: RootOperationNode) => { const _p = (q: RootOperationNode) => _proceed(q); const hookResult = await hook!({ - client: this.client as unknown as ClientContract, + client: this.client.$contract, schema: this.client.$schema, query, proceed: _p, @@ -660,7 +660,7 @@ In such cases, ZenStack cannot reliably determine the IDs of the mutated entitie if (inTx) { innerClient.forceTransaction(); } - return innerClient as unknown as ClientContract; + return innerClient.$contract; } private andNodes(condition1: WhereNode | undefined, condition2: WhereNode | undefined) { diff --git a/packages/orm/src/client/options.ts b/packages/orm/src/client/options.ts index 4ab02fc1a..158392e29 100644 --- a/packages/orm/src/client/options.ts +++ b/packages/orm/src/client/options.ts @@ -283,16 +283,38 @@ export type OmitConfig = { }; }; +/** + * Context object passed to computed field implementations. + */ +export type ComputedFieldContext = { + /** + * The alias name that can be used to refer to the containing model + */ + modelAlias: string; + + /** + * The ZenStack client executing the query. Useful for reading per-client state, + * e.g. the auth context set via `$setAuth`. Undefined only when the CRUD dialect + * is constructed standalone rather than by the ORM runtime — queries issued + * through the client API always have it. + */ + client?: ClientContract; +}; + export type ComputedFieldsOptions = { [Model in GetModels as 'computedFields' extends keyof GetModel ? Uncapitalize : never]: { [Field in keyof Schema['models'][Model]['computedFields']]: Schema['models'][Model]['computedFields'][Field] extends infer Func - ? Func extends (...args: any[]) => infer R + ? Func extends (...args: infer Params) => infer R ? ( // inject a first parameter for expression builder p: ExpressionBuilder, Model>, - ...args: Parameters + // runtime-provided context (the generated stub only declares + // `modelAlias`; the runtime passes the full context) + context: ComputedFieldContext, + // query-time args of a parameterized field, from the stub + ...args: Params extends [any, ...infer Rest] ? Rest : [] ) => OperandExpression // wrap the return type with Kysely `OperandExpression` : never : never; diff --git a/tests/e2e/orm/client-api/computed-fields.test.ts b/tests/e2e/orm/client-api/computed-fields.test.ts index 4b1b6ac28..1f93f318a 100644 --- a/tests/e2e/orm/client-api/computed-fields.test.ts +++ b/tests/e2e/orm/client-api/computed-fields.test.ts @@ -989,4 +989,38 @@ model User { }), ).toBeRejectedByValidation(['upperName']); }); + + it('provides the client in the computed field context', async () => { + const db = await createTestClient( + ` +model Post { + id Int @id @default(autoincrement()) + authorId Int + isMine Boolean @computed +} +`, + { + computedFields: { + Post: { + isMine: (eb: any, { client }: any) => eb('authorId', '=', client?.$auth?.id ?? -1), + }, + }, + } as any, + ); + + await db.post.create({ data: { id: 1, authorId: 1 } }); + await db.post.create({ data: { id: 2, authorId: 2 } }); + + // no auth set: nothing is mine + await expect(db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ isMine: false }); + + // the client derived with $setAuth carries its auth into the computed field + const authedDb = db.$setAuth({ id: 1 }); + await expect(authedDb.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ isMine: true }); + await expect(authedDb.post.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ isMine: false }); + await expect(authedDb.post.findMany({ where: { isMine: true } })).resolves.toHaveLength(1); + + // the original client is unaffected + await expect(db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ isMine: false }); + }); }); From 771e2161beff6493aeafa444c364373a473aa8ec Mon Sep 17 00:00:00 2001 From: evgenovalov Date: Tue, 4 Aug 2026 18:18:54 +0200 Subject: [PATCH 2/3] test: parenthesize the computed field expression in the client-context test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bare binary comparison embedded by the boolean where-filter renders as a chained '=' — a syntax error on postgres (sqlite tolerates it). Co-Authored-By: Claude Fable 5 --- tests/e2e/orm/client-api/computed-fields.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/e2e/orm/client-api/computed-fields.test.ts b/tests/e2e/orm/client-api/computed-fields.test.ts index 1f93f318a..fc9202b18 100644 --- a/tests/e2e/orm/client-api/computed-fields.test.ts +++ b/tests/e2e/orm/client-api/computed-fields.test.ts @@ -1002,7 +1002,10 @@ model Post { { computedFields: { Post: { - isMine: (eb: any, { client }: any) => eb('authorId', '=', client?.$auth?.id ?? -1), + // parenthesized: the expression gets embedded into larger ones + // (e.g. `where: { isMine: true }` wraps it with `= true`), and an + // unparenthesized chained comparison is a syntax error on postgres + isMine: (eb: any, { client }: any) => eb.parens(eb('authorId', '=', client?.$auth?.id ?? -1)), }, }, } as any, From c4ec402211214a29f353919a47f842e40eea1c1e Mon Sep 17 00:00:00 2001 From: evgenovalov Date: Tue, 11 Aug 2026 10:06:33 +0200 Subject: [PATCH 3/3] refactor: address review on dialect construction and computed field context - `BaseCrudDialect` / `getCrudDialect` now take either the client executing the queries (schema and options derived from it) or a standalone schema/options pair, instead of an extra optional `client` parameter - pass the client at the remaining construction sites (policy handler, policy expression transformer, soft-delete plugin); `ResultProcessor` stays on the standalone form as it has no client and only transforms output values - assert the client when evaluating a computed field and make `ComputedFieldContext.client` non-optional Co-Authored-By: Claude Opus 5 (1M context) --- .../src/client/crud/dialects/base-dialect.ts | 41 +++++++++++++++---- .../orm/src/client/crud/dialects/index.ts | 25 ++++++++--- .../orm/src/client/crud/dialects/mysql.ts | 6 --- .../src/client/crud/dialects/postgresql.ts | 8 ++-- .../orm/src/client/crud/operations/base.ts | 2 +- .../orm/src/client/executor/name-mapper.ts | 2 +- .../executor/zenstack-query-executor.ts | 2 +- packages/orm/src/client/options.ts | 6 +-- .../policy/src/expression-transformer.ts | 2 +- packages/plugins/policy/src/policy-handler.ts | 2 +- packages/plugins/soft-delete/src/plugin.ts | 2 +- .../orm/client-api/computed-fields.test.ts | 2 +- 12 files changed, 64 insertions(+), 36 deletions(-) diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 9eb497fb4..4d97b685e 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -6,6 +6,7 @@ import { match, P } from 'ts-pattern'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; import type { OrArray } from '../../../utils/type-utils'; import { AggregateOperators, DELEGATE_JOINED_FIELD_PREFIX, LOGICAL_COMBINATORS } from '../../constants'; +import type { ClientContract } from '../../contract'; import type { BooleanFilter, BytesFilter, @@ -15,7 +16,6 @@ import type { SortOrder, StringFilter, } from '../../crud-types'; -import type { ClientContract } from '../../contract'; import { createConfigError, createInvalidInputError, createNotSupportedError } from '../../errors'; import type { ClientOptions } from '../../options'; import { @@ -37,16 +37,38 @@ import { tmpAlias, } from '../../query-utils'; +/** + * Arguments for constructing a CRUD dialect: either the client executing the queries — schema + * and options are derived from it, and it's handed to computed field implementations — or a + * standalone schema/options pair for uses that have no client (e.g. output transformation). + */ +export type CrudDialectArgs = + | [client: ClientContract] + | [schema: Schema, options: ClientOptions]; + export abstract class BaseCrudDialect { protected eb = expressionBuilder(); - constructor( - protected readonly schema: Schema, - protected readonly options: ClientOptions, - // the client executing the query; optional so the dialect can still be - // constructed standalone (e.g. for output transformation only) - protected readonly client?: ClientContract, - ) {} + protected readonly schema: Schema; + protected readonly options: ClientOptions; + + /** + * The client executing the query. Unset only when the dialect was constructed from a + * standalone schema/options pair, in which case it cannot evaluate computed fields. + */ + protected readonly client: ClientContract | undefined; + + constructor(...args: CrudDialectArgs) { + if (args.length === 1) { + const [client] = args; + this.client = client; + this.schema = client.$schema; + this.options = client.$options; + } else { + [this.schema, this.options] = args; + this.client = undefined; + } + } // #region capability flags1 @@ -1664,6 +1686,9 @@ export abstract class BaseCrudDialect { if (!computer) { throw createConfigError(`Computed field "${field}" implementation not provided for model "${model}"`); } + // every query issued through the ORM builds the dialect from a client, and a dialect + // built from a standalone schema/options pair never inlines computed fields + invariant(this.client, `computed field "${field}" of model "${model}" needs a client to be evaluated`); // `computedArgs` is the query-time args object for a parameterized computed // field (undefined otherwise); forwarded as the implementation's 3rd argument. return computer(this.eb, { modelAlias, client: this.client }, computedArgs); diff --git a/packages/orm/src/client/crud/dialects/index.ts b/packages/orm/src/client/crud/dialects/index.ts index eea929206..f2e4aff1a 100644 --- a/packages/orm/src/client/crud/dialects/index.ts +++ b/packages/orm/src/client/crud/dialects/index.ts @@ -2,19 +2,32 @@ import type { SchemaDef } from '@zenstackhq/schema'; import { match } from 'ts-pattern'; import type { ClientContract } from '../../contract'; import type { ClientOptions } from '../../options'; -import type { BaseCrudDialect } from './base-dialect'; +import type { BaseCrudDialect, CrudDialectArgs } from './base-dialect'; import { MySqlCrudDialect } from './mysql'; import { PostgresCrudDialect } from './postgresql'; import { SqliteCrudDialect } from './sqlite'; +/** + * Creates a CRUD dialect for the client's provider. Schema and options are taken from the + * client, which is also handed to computed field implementations, so prefer this overload + * whenever a client is available. + */ +export function getCrudDialect(client: ClientContract): BaseCrudDialect; + +/** + * Creates a CRUD dialect from a standalone schema/options pair, for uses that have no client + * (e.g. output transformation). Such a dialect cannot evaluate computed fields. + */ export function getCrudDialect( schema: Schema, options: ClientOptions, - client?: ClientContract, -): BaseCrudDialect { +): BaseCrudDialect; + +export function getCrudDialect(...args: CrudDialectArgs): BaseCrudDialect { + const schema = args.length === 1 ? args[0].$schema : args[0]; return match(schema.provider.type) - .with('sqlite', () => new SqliteCrudDialect(schema, options, client)) - .with('postgresql', () => new PostgresCrudDialect(schema, options, client)) - .with('mysql', () => new MySqlCrudDialect(schema, options, client)) + .with('sqlite', () => new SqliteCrudDialect(...args)) + .with('postgresql', () => new PostgresCrudDialect(...args)) + .with('mysql', () => new MySqlCrudDialect(...args)) .exhaustive(); } diff --git a/packages/orm/src/client/crud/dialects/mysql.ts b/packages/orm/src/client/crud/dialects/mysql.ts index 9388f4b18..498a431fc 100644 --- a/packages/orm/src/client/crud/dialects/mysql.ts +++ b/packages/orm/src/client/crud/dialects/mysql.ts @@ -12,19 +12,13 @@ import { type SqlBool, } from 'kysely'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; -import type { ClientContract } from '../../contract'; import type { NullsOrder, SortOrder } from '../../crud-types'; import { createInvalidInputError, createNotSupportedError } from '../../errors'; -import type { ClientOptions } from '../../options'; import { isTypeDef } from '../../query-utils'; import type { FuzzyFilterOptions } from './base-dialect'; import { LateralJoinDialectBase } from './lateral-join-dialect-base'; export class MySqlCrudDialect extends LateralJoinDialectBase { - constructor(schema: Schema, options: ClientOptions, client?: ClientContract) { - super(schema, options, client); - } - override get provider() { return 'mysql' as const; } diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index 6461893ea..75ee4f35e 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -11,12 +11,10 @@ import { } from 'kysely'; import { parse as parsePostgresArray } from 'postgres-array'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; -import type { ClientContract } from '../../contract'; import type { NullsOrder, SortOrder } from '../../crud-types'; import { createInvalidInputError } from '../../errors'; -import type { ClientOptions } from '../../options'; import { isEnum, isTypeDef } from '../../query-utils'; -import type { FuzzyFilterOptions } from './base-dialect'; +import type { CrudDialectArgs, FuzzyFilterOptions } from './base-dialect'; import { LateralJoinDialectBase } from './lateral-join-dialect-base'; /** @@ -74,8 +72,8 @@ export class PostgresCrudDialect extends LateralJoinDi '@db.Boolean': 'boolean', }; - constructor(schema: Schema, options: ClientOptions, client?: ClientContract) { - super(schema, options, client); + constructor(...args: CrudDialectArgs) { + super(...args); this.overrideTypeParsers(); } diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 6e9e707e8..a9b9e572e 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -199,7 +199,7 @@ export abstract class BaseOperationHandler { protected readonly model: GetModels, protected readonly inputValidator: InputValidator, ) { - this.dialect = getCrudDialect(this.schema, this.client.$options, this.client); + this.dialect = getCrudDialect(this.client); } protected get schema() { diff --git a/packages/orm/src/client/executor/name-mapper.ts b/packages/orm/src/client/executor/name-mapper.ts index 667e677f5..b4ac42d9c 100644 --- a/packages/orm/src/client/executor/name-mapper.ts +++ b/packages/orm/src/client/executor/name-mapper.ts @@ -62,7 +62,7 @@ export class QueryNameMapper extends OperationNodeTransformer { constructor(private readonly client: ClientContract) { super(); - this.dialect = getCrudDialect(client.$schema, client.$options, client); + this.dialect = getCrudDialect(client); for (const [modelName, modelDef] of Object.entries(client.$schema.models)) { const mappedName = this.getMappedName(modelDef); if (mappedName) { diff --git a/packages/orm/src/client/executor/zenstack-query-executor.ts b/packages/orm/src/client/executor/zenstack-query-executor.ts index d1723ac24..b6638291b 100644 --- a/packages/orm/src/client/executor/zenstack-query-executor.ts +++ b/packages/orm/src/client/executor/zenstack-query-executor.ts @@ -96,7 +96,7 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { this.nameMapper = new QueryNameMapper(client.$contract); } - this.dialect = getCrudDialect(client.$schema, client.$options, client.$contract); + this.dialect = getCrudDialect(client.$contract); } private schemaHasMappedNames(schema: SchemaDef) { diff --git a/packages/orm/src/client/options.ts b/packages/orm/src/client/options.ts index 158392e29..29f60281f 100644 --- a/packages/orm/src/client/options.ts +++ b/packages/orm/src/client/options.ts @@ -294,11 +294,9 @@ export type ComputedFieldContext = { /** * The ZenStack client executing the query. Useful for reading per-client state, - * e.g. the auth context set via `$setAuth`. Undefined only when the CRUD dialect - * is constructed standalone rather than by the ORM runtime — queries issued - * through the client API always have it. + * e.g. the auth context set via `$setAuth`. */ - client?: ClientContract; + client: ClientContract; }; export type ComputedFieldsOptions = { diff --git a/packages/plugins/policy/src/expression-transformer.ts b/packages/plugins/policy/src/expression-transformer.ts index 2beb5be73..b0037e040 100644 --- a/packages/plugins/policy/src/expression-transformer.ts +++ b/packages/plugins/policy/src/expression-transformer.ts @@ -133,7 +133,7 @@ export class ExpressionTransformer { private readonly eb = expressionBuilder(); constructor(private readonly client: ClientContract) { - this.dialect = getCrudDialect(this.schema, this.clientOptions); + this.dialect = getCrudDialect(this.client); } get schema() { diff --git a/packages/plugins/policy/src/policy-handler.ts b/packages/plugins/policy/src/policy-handler.ts index 7ebc3d669..b84c39cd4 100644 --- a/packages/plugins/policy/src/policy-handler.ts +++ b/packages/plugins/policy/src/policy-handler.ts @@ -75,7 +75,7 @@ export class PolicyHandler extends OperationNodeTransf private readonly options: PolicyPluginOptions = {}, ) { super(); - this.dialect = getCrudDialect(this.client.$schema, this.client.$options); + this.dialect = getCrudDialect(this.client); } // #region main entry point diff --git a/packages/plugins/soft-delete/src/plugin.ts b/packages/plugins/soft-delete/src/plugin.ts index a20cac974..5ba8347b4 100644 --- a/packages/plugins/soft-delete/src/plugin.ts +++ b/packages/plugins/soft-delete/src/plugin.ts @@ -62,7 +62,7 @@ class SoftDeleteHandler extends OperationNodeTransform constructor(private readonly client: ClientContract) { super(); - this.dialect = getCrudDialect(client.$schema, client.$options); + this.dialect = getCrudDialect(client); } async handle(node: RootOperationNode, proceed: ProceedKyselyQueryFunction) { diff --git a/tests/e2e/orm/client-api/computed-fields.test.ts b/tests/e2e/orm/client-api/computed-fields.test.ts index fc9202b18..2be62ad94 100644 --- a/tests/e2e/orm/client-api/computed-fields.test.ts +++ b/tests/e2e/orm/client-api/computed-fields.test.ts @@ -1005,7 +1005,7 @@ model Post { // parenthesized: the expression gets embedded into larger ones // (e.g. `where: { isMine: true }` wraps it with `= true`), and an // unparenthesized chained comparison is a syntax error on postgres - isMine: (eb: any, { client }: any) => eb.parens(eb('authorId', '=', client?.$auth?.id ?? -1)), + isMine: (eb: any, { client }: any) => eb.parens(eb('authorId', '=', client.$auth?.id ?? -1)), }, }, } as any,