Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "zenstack-v3",
"displayName": "ZenStack",
"description": "ZenStack",
"version": "3.9.0",
"version": "3.9.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/auth-adapters/better-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/better-auth",
"displayName": "ZenStack Better Auth Adapter",
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
"version": "3.9.0",
"version": "3.9.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/cli",
"displayName": "ZenStack CLI",
"description": "FullStack database toolkit with built-in access control and automatic API generation.",
"version": "3.9.0",
"version": "3.9.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/clients/client-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/client-helpers",
"displayName": "ZenStack Client Helpers",
"description": "Helpers for implementing clients that consume ZenStack's CRUD service",
"version": "3.9.0",
"version": "3.9.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/clients/fetch-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/fetch-client",
"displayName": "ZenStack Fetch Client",
"description": "Simple fetch-based client for consuming ZenStack's RPC-style CRUD API",
"version": "3.9.0",
"version": "3.9.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/clients/tanstack-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/tanstack-query",
"displayName": "ZenStack TanStack Query Integration",
"description": "TanStack Query Client for consuming ZenStack v3's CRUD service",
"version": "3.9.0",
"version": "3.9.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/common-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/common-helpers",
"displayName": "ZenStack Common Helpers",
"description": "ZenStack Common Helpers",
"version": "3.9.0",
"version": "3.9.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/config/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/eslint-config",
"version": "3.9.0",
"version": "3.9.1",
"type": "module",
"private": true,
"license": "MIT"
Expand Down
2 changes: 1 addition & 1 deletion packages/config/tsdown-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/tsdown-config",
"version": "3.9.0",
"version": "3.9.1",
"private": true,
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/config/typescript-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/typescript-config",
"version": "3.9.0",
"version": "3.9.1",
"private": true,
"license": "MIT"
}
2 changes: 1 addition & 1 deletion packages/config/vitest-config/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/vitest-config",
"type": "module",
"version": "3.9.0",
"version": "3.9.1",
"private": true,
"license": "MIT",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-zenstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "create-zenstack",
"displayName": "Create ZenStack",
"description": "Create a new ZenStack project",
"version": "3.9.0",
"version": "3.9.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/ide/vscode/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zenstack-v3",
"publisher": "zenstack",
"version": "3.9.0",
"version": "3.9.1",
"displayName": "ZenStack V3 Language Tools",
"description": "VSCode extension for ZenStack (v3) ZModel language",
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/language",
"displayName": "ZenStack Language Tooling",
"description": "ZenStack ZModel language specification",
"version": "3.9.0",
"version": "3.9.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/orm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/orm",
"displayName": "ZenStack ORM",
"description": "ZenStack ORM",
"version": "3.9.0",
"version": "3.9.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
34 changes: 31 additions & 3 deletions packages/orm/src/client/client-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ type ExtResultFieldDef = {
compute: (data: Record<string, any>) => unknown;
};

/**
* Returns the name mapper held by a ZenStack executor, or undefined for a plain kysely one.
*/
function getExecutorNameMapper(executor: QueryExecutor | undefined) {
return executor instanceof ZenStackQueryExecutor ? executor.getNameMapper() : undefined;
}

/**
* ZenStack ORM client.
*/
Expand Down Expand Up @@ -99,6 +106,16 @@ export class ClientImpl {
baseClient.kyselyProps.dialect.createQueryCompiler(),
baseClient.kyselyProps.dialect.createAdapter(),
new DefaultConnectionProvider(baseClient.kyselyProps.driver),
[],
false,
// A name mapper is derived purely from `$schema` and `$options`, so it can be
// reused when neither changed - which is the case for derived clients like the
// one `$transaction` creates. Rebuilding it is O(models x fields). See #2773.
// Deliberately an identity check: `$use`/`$setOptions` and friends pass a new
// options object, and the mapper's dialect is built from those options.
baseClient.$schema === schema && baseClient.$options === options
? getExecutorNameMapper(baseClient.kyselyProps.executor)
: undefined,
),
};
this.kyselyRaw = baseClient.kyselyRaw;
Expand Down Expand Up @@ -253,7 +270,7 @@ export class ClientImpl {
): Promise<any> {
if (this.kysely.isTransaction) {
// proceed directly if already in a transaction
return callback(this as unknown as ClientContract<SchemaDef>);
return callback(this.$contract);
} else {
// otherwise, create a new transaction, clone the client, and execute the callback
let txBuilder = this.kysely.transaction();
Expand All @@ -263,7 +280,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<SchemaDef>);
return callback(txClient.$contract);
});
}
}
Expand All @@ -285,7 +302,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<SchemaDef>));
result.push(await cb(txClient.$contract));
}
return result;
};
Expand Down Expand Up @@ -446,6 +463,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<SchemaDef> {
return this as unknown as ClientContract<SchemaDef>;
}

$setOptions<Options extends ClientOptions<SchemaDef>>(options: Options): ClientContract<SchemaDef, Options> {
const newClient = new ClientImpl(this.schema, options as ClientOptions<SchemaDef>, this);
// create a new validator to have a fresh schema cache, because options may change validation settings
Expand Down
44 changes: 37 additions & 7 deletions packages/orm/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -36,13 +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<Schema extends SchemaDef> =
| [client: ClientContract<Schema>]
| [schema: Schema, options: ClientOptions<Schema>];

export abstract class BaseCrudDialect<Schema extends SchemaDef> {
protected eb = expressionBuilder<any, any>();

constructor(
protected readonly schema: Schema,
protected readonly options: ClientOptions<Schema>,
) {}
protected readonly schema: Schema;
protected readonly options: ClientOptions<Schema>;

/**
* 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<Schema> | undefined;

constructor(...args: CrudDialectArgs<Schema>) {
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

Expand Down Expand Up @@ -1660,9 +1686,13 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
if (!computer) {
throw createConfigError(`Computed field "${field}" implementation not provided for model "${model}"`);
}
// `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);
// 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 of a parameterized computed field (undefined
// otherwise), forwarded as the implementation's 3rd argument. The result is parenthesized
// as it gets embedded into larger expressions: `where: { isMine: true }` → `(<expr>) = $n`.
return this.eb.parens(computer(this.eb, { modelAlias, client: this.client }, computedArgs));
}
}

Expand Down
25 changes: 20 additions & 5 deletions packages/orm/src/client/crud/dialects/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
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<Schema extends SchemaDef>(client: ClientContract<Schema>): BaseCrudDialect<Schema>;

/**
* 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 extends SchemaDef>(
schema: Schema,
options: ClientOptions<Schema>,
): BaseCrudDialect<Schema> {
): BaseCrudDialect<Schema>;

export function getCrudDialect<Schema extends SchemaDef>(...args: CrudDialectArgs<Schema>): BaseCrudDialect<Schema> {
const schema = args.length === 1 ? args[0].$schema : args[0];
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(...args))
.with('postgresql', () => new PostgresCrudDialect(...args))
.with('mysql', () => new MySqlCrudDialect(...args))
.exhaustive();
}
5 changes: 0 additions & 5 deletions packages/orm/src/client/crud/dialects/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,11 @@ import {
import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types';
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<Schema extends SchemaDef> extends LateralJoinDialectBase<Schema> {
constructor(schema: Schema, options: ClientOptions<Schema>) {
super(schema, options);
}

override get provider() {
return 'mysql' as const;
}
Expand Down
7 changes: 3 additions & 4 deletions packages/orm/src/client/crud/dialects/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ import { parse as parsePostgresArray } from 'postgres-array';
import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types';
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';

/**
Expand Down Expand Up @@ -73,8 +72,8 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends LateralJoinDi
'@db.Boolean': 'boolean',
};

constructor(schema: Schema, options: ClientOptions<Schema>) {
super(schema, options);
constructor(...args: CrudDialectArgs<Schema>) {
super(...args);
this.overrideTypeParsers();
}

Expand Down
2 changes: 1 addition & 1 deletion packages/orm/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
protected readonly model: GetModels<Schema>,
protected readonly inputValidator: InputValidator<Schema>,
) {
this.dialect = getCrudDialect(this.schema, this.client.$options);
this.dialect = getCrudDialect(this.client);
}

protected get schema() {
Expand Down
2 changes: 1 addition & 1 deletion packages/orm/src/client/executor/name-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class QueryNameMapper extends OperationNodeTransformer {

constructor(private readonly client: ClientContract<SchemaDef>) {
super();
this.dialect = getCrudDialect(client.$schema, client.$options);
this.dialect = getCrudDialect(client);
for (const [modelName, modelDef] of Object.entries(client.$schema.models)) {
const mappedName = this.getMappedName(modelDef);
if (mappedName) {
Expand Down
Loading
Loading