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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"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.6",
"version": "3.9.7",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
13 changes: 9 additions & 4 deletions packages/orm/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2137,11 +2137,16 @@ type UpdateRelationFieldPayload<
? ToManyRelationUpdateInput<Schema, Model, Field, Options>
: ToOneRelationUpdateInput<Schema, Model, Field, Options>;

// Variance-annotated for the same reason as `ToOneRelationUpdateInput` below: without the
// annotations, comparing two instantiations with different `Options` (e.g. a generated
// `XxxUpdateArgs`/`XxxUpsertArgs` type passed to a client method) makes TypeScript measure
// variance structurally through the recursive nested update/upsert types, which blows up into
// "Type instantiation is excessively deep and possibly infinite" (#2778).
type ToManyRelationUpdateInput<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Field extends RelationFields<Schema, Model>,
Options extends QueryOptions<Schema>,
in out Schema extends SchemaDef,
in out Model extends GetModels<Schema>,
in out Field extends RelationFields<Schema, Model>,
in out Options extends QueryOptions<Schema>,
> = Omit<
{
/**
Expand Down
24 changes: 17 additions & 7 deletions packages/orm/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1431,11 +1431,22 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
query: SelectQueryBuilder<any, any, any>,
omit: Record<string, boolean | undefined> | undefined | null,
modelAlias: string,
options?: {
/**
* When false, query-level, client-level, and schema-level omit settings are all
* ignored for the model's own fields and every field is selected. Omission is
* still applied to the JSON packed from delegate descendants, since those fields
* are never referenced by joins or ordering and the packed JSON is copied to
* the final result as-is.
*/
applyOmit?: boolean;
},
) {
let result = query;
const applyOmit = options?.applyOmit ?? true;

for (const fieldDef of getModelFields(this.schema, model, { inherited: true, computed: true })) {
if (this.shouldOmitField(omit, model, fieldDef.name)) {
if (applyOmit && this.shouldOmitField(omit, model, fieldDef.name)) {
continue;
}
// parameterized computed fields can't be auto-selected — they require
Expand Down Expand Up @@ -1498,12 +1509,11 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
let subQuery = this.buildSelectModel(model, subQueryAlias);

if (selectAllFields) {
subQuery = this.buildSelectAllFields(
model,
subQuery,
typeof payload === 'object' ? payload?.omit : undefined,
subQueryAlias,
);
// omission (query-level, client-level, or schema-level) is intentionally not
// applied here: this select feeds a derived subquery whose columns are needed by
// nested relation joins (PK/FK fields) and by ordering of the aggregated result,
// and the outer JSON object projection handles omission on its own
subQuery = this.buildSelectAllFields(model, subQuery, undefined, subQueryAlias, { applyOmit: false });
}

if (payload && typeof payload === 'object') {
Expand Down
72 changes: 72 additions & 0 deletions packages/orm/src/client/crud/dialects/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { invariant } from '@zenstackhq/common-helpers';
import type { BuiltinType, FieldDef, SchemaDef } from '@zenstackhq/schema';
import Decimal from 'decimal.js';
import {
ValueNode,
type OperationNode,
expressionBuilder,
sql,
type AliasableExpression,
Expand Down Expand Up @@ -531,6 +533,17 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends LateralJoinDi
) {
const leftResolved = this.resolveFieldSqlType(leftFieldDef);
const rightResolved = this.resolveFieldSqlType(rightFieldDef);

// Fast path for comparing a column with a @db.* native type against a bound value (e.g.
// `userId == auth().id`). Casting the column would defeat index usage, so the value side is
// handled instead: PostgreSQL infers the parameter type from the column, and for uuid the
// value is validated up front so a malformed one yields a constant result rather than an
// "invalid input syntax for type uuid" error.
const valueResult = this.tryBuildNativeTypeValueComparison(left, leftResolved, op, right, rightResolved);
if (valueResult) {
return valueResult;
}

// If the resolved SQL types differ and at least one side carries a @db.* native type override,
// cast that side back to its base ZModel SQL type so PostgreSQL doesn't reject the comparison
// (e.g. "operator does not exist: uuid = text").
Expand All @@ -548,6 +561,65 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends LateralJoinDi
return super.buildComparison(left, leftFieldDef, op, right, rightFieldDef);
}

// native SQL types that accept any text input, so a bound string value never fails to parse
private static readonly textLikeSqlTypes = new Set(['text', 'varchar', 'bpchar', 'citext']);
Comment thread
ymc9 marked this conversation as resolved.

// uuid input formats accepted by PostgreSQL (canonical 8-4-4-4-12 or 32 hex digits). This is a
// pure format check: unlike RFC 4122 validators it doesn't require specific version/variant bits,
// since PostgreSQL stores any 128-bit value (e.g. `00000000-0000-0000-0000-000000000001`).
private static readonly uuidFormatRegex =
/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32})$/i;

private tryBuildNativeTypeValueComparison(
left: Expression<unknown>,
leftResolved: ReturnType<typeof this.resolveFieldSqlType>,
op: string,
right: Expression<unknown>,
rightResolved: ReturnType<typeof this.resolveFieldSqlType>,
): Expression<SqlBool> | undefined {
let valueNode: OperationNode;
let columnResolved: typeof leftResolved;
const leftNode = left.toOperationNode();
const rightNode = right.toOperationNode();
if (ValueNode.is(rightNode) && !ValueNode.is(leftNode)) {
valueNode = rightNode;
columnResolved = leftResolved;
} else if (ValueNode.is(leftNode) && !ValueNode.is(rightNode)) {
valueNode = leftNode;
columnResolved = rightResolved;
} else {
return undefined;
}

if (!columnResolved.hasDbOverride || !columnResolved.sqlType) {
return undefined;
}

if (PostgresCrudDialect.textLikeSqlTypes.has(columnResolved.sqlType)) {
// text-like column, any string value is valid input
return this.eb(left, op as any, right) as Expression<SqlBool>;
}

if (columnResolved.sqlType === 'uuid' && (op === '=' || op === '!=')) {
const value = (valueNode as ValueNode).value;
if (typeof value === 'string' && PostgresCrudDialect.uuidFormatRegex.test(value)) {
// well-formed uuid, compare natively without casting the column
return this.eb(left, op as any, right) as Expression<SqlBool>;
} else if (op === '=') {
// malformed uuid can never equal a uuid column
return this.eb.lit(false) as unknown as Expression<SqlBool>;
} else {
// malformed uuid differs from every non-null uuid; a null column must not match, matching
// the SQL semantics of `col != value` (null when col is null)
const column = valueNode === rightNode ? left : right;
return this.eb(column, 'is not', null) as Expression<SqlBool>;
}
}

// other native types fall back to casting the column
return undefined;
}

override getStringCasingBehavior() {
// Postgres `LIKE` is case-sensitive, `ILIKE` is case-insensitive
return { supportsILike: true, likeCaseSensitive: true };
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/policy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/plugin-policy",
"displayName": "ZenStack Access Policy Plugin",
"description": "ZenStack plugin that enforces access control policies defined in the schema",
"version": "3.9.6",
"version": "3.9.7",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
Loading
Loading