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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.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.10.0",
"version": "3.10.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
40 changes: 21 additions & 19 deletions packages/orm/src/client/client-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { FindOperationHandler } from './crud/operations/find';
import { GroupByOperationHandler } from './crud/operations/group-by';
import { UpdateOperationHandler } from './crud/operations/update';
import { InputValidator } from './crud/validator';
import { haveSameSchemaInputs } from './zod/factory';
import type { Diagnostics, QueryInfo } from './diagnostics';
import { createConfigError, createNotFoundError, createNotSupportedError } from './errors';
import { ZenStackDriver } from './executor/zenstack-driver';
Expand Down Expand Up @@ -518,11 +519,7 @@ export class ClientImpl {
plugins: newPlugins,
};
const newClient = new ClientImpl(this.schema, newOptions, this);
// create a new validator to have a fresh schema cache, because plugins may extend the
// query args schemas
newClient.inputValidator = new InputValidator(newClient as any, {
enabled: newOptions.validateInput !== false,
});
newClient.inputValidator = this.deriveInputValidator(newClient);
return newClient;
}

Expand All @@ -539,11 +536,7 @@ export class ClientImpl {
plugins: newPlugins,
};
const newClient = new ClientImpl(this.schema, newOptions, this);
// create a new validator to have a fresh schema cache, because plugins may
// extend the query args schemas
newClient.inputValidator = new InputValidator(newClient as any, {
enabled: newClient.$options.validateInput !== false,
});
newClient.inputValidator = this.deriveInputValidator(newClient);
return newClient;
}

Expand All @@ -554,14 +547,26 @@ export class ClientImpl {
plugins: [] as AnyPlugin[],
};
const newClient = new ClientImpl(this.schema, newOptions, this);
// create a new validator to have a fresh schema cache, because plugins may
// extend the query args schemas
newClient.inputValidator = new InputValidator(newClient as any, {
enabled: newOptions.validateInput !== false,
});
newClient.inputValidator = this.deriveInputValidator(newClient);
return newClient;
}

/**
* Validator for a client derived from this one. The derived client reuses this client's Zod
* schema factory, and so its schema cache, whenever its options shape the same query-args
* schemas; it only gets a fresh factory when a plugin that extends query args or results was
* added or removed, or `omit`/`slicing` changed. The validator itself is always new because
* `validateInput` may differ.
*/
private deriveInputValidator(newClient: ClientImpl) {
return new InputValidator(newClient as any, {
enabled: newClient.$options.validateInput !== false,
zodFactory: haveSameSchemaInputs(this.$options, newClient.$options)
? this.inputValidator.zodFactory
: undefined,
});
}

$setAuth(auth: AuthType<SchemaDef> | undefined) {
if (auth !== undefined && (typeof auth !== 'object' || auth === null || Array.isArray(auth))) {
throw new Error('Invalid auth object');
Expand All @@ -588,10 +593,7 @@ export class ClientImpl {

$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
newClient.inputValidator = new InputValidator(newClient as any, {
enabled: newClient.$options.validateInput !== false,
});
newClient.inputValidator = this.deriveInputValidator(newClient);
return newClient as unknown as ClientContract<SchemaDef, Options>;
}

Expand Down
8 changes: 8 additions & 0 deletions packages/orm/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
const r = await kysely.getExecutor().executeQuery(compiled);
result = r.rows as { $exists: number | boolean }[];
} catch (err) {
if (err instanceof ORMError) {
// already a structured error (e.g., raised by a plugin or wrapped by the executor)
throw err;
}
throw createDBQueryError(`Failed to execute query: ${err}`, err, compiled.sql, compiled.parameters);
}

Expand Down Expand Up @@ -212,6 +216,10 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
const r = await kysely.getExecutor().executeQuery(compiled);
result = r.rows;
} catch (err) {
if (err instanceof ORMError) {
// already a structured error (e.g., raised by a plugin or wrapped by the executor)
throw err;
}
throw createDBQueryError(`Failed to execute query: ${err}`, err, compiled.sql, compiled.parameters);
}

Expand Down
12 changes: 9 additions & 3 deletions packages/orm/src/client/crud/validator/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ import { ZodSchemaFactory } from '../../zod/factory';

type GetSchemaFunc<Schema extends SchemaDef> = (model: GetModels<Schema>) => ZodType;

export type InputValidatorOptions = {
export type InputValidatorOptions<Schema extends SchemaDef> = {
/**
* Whether validation is enabled. Defaults to `true`.
*/
enabled?: boolean;

/**
* Schema factory to validate with. Defaults to a new factory (and so a new schema cache) bound
* to the client.
*/
zodFactory?: ZodSchemaFactory<Schema>;
};

export class InputValidator<Schema extends SchemaDef> {
Expand All @@ -38,9 +44,9 @@ export class InputValidator<Schema extends SchemaDef> {

constructor(
private readonly client: ClientContract<Schema>,
options?: InputValidatorOptions,
options?: InputValidatorOptions<Schema>,
) {
this.zodFactory = new ZodSchemaFactory(client);
this.zodFactory = options?.zodFactory ?? new ZodSchemaFactory(client);
this.enabled = options?.enabled !== false;
}

Expand Down
7 changes: 7 additions & 0 deletions packages/orm/src/client/executor/zenstack-query-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,13 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor {
? this.getInsertIds(mutationInfo.model, queryNode, queryResult)
: preUpdateIds;

if (mutatedIds && mutatedIds.length === 0) {
// zero rows matched (e.g. a policy-denied or genuinely empty
// updateMany/deleteMany) — nothing to reload, and NOT the "can't
// determine ids at all" case below, so don't warn.
return [];
}

if (mutatedIds) {
const idFields = requireIdFields(this.client.$schema, mutationInfo.model);
const eb = expressionBuilder<any, any>();
Expand Down
39 changes: 32 additions & 7 deletions packages/orm/src/client/zod/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,26 @@ export function createQuerySchemaFactory(clientOrSchema: any, options?: any) {
return new ZodSchemaFactory(clientOrSchema, options);
}

/**
* Whether two client options produce the same query-args schemas, i.e. a `ZodSchemaFactory`
* built for one can serve the other. Only `omit`, `slicing`, `allowQueryTimeOmitOverride` and
* the plugins that extend query args or results shape the schemas; dialect, auth, hooks and
* validation flags do not.
*/
export function haveSameSchemaInputs(a: ClientOptions<SchemaDef>, b: ClientOptions<SchemaDef>): boolean {
if (a === b) {
return true;
}
if (a.omit !== b.omit || a.slicing !== b.slicing || a.allowQueryTimeOmitOverride !== b.allowQueryTimeOmitOverride) {
return false;
}
const shaping = (options: ClientOptions<SchemaDef>) =>
(options.plugins ?? []).filter((plugin) => plugin.queryArgs || plugin.result);
const pa = shaping(a);
const pb = shaping(b);
return pa.length === pb.length && pa.every((plugin, index) => plugin === pb[index]);
}

/**
* Builds a `DateTime` value schema that accepts a `Date` object or any string
* the JS `Date` constructor parses, and coerces it to a `Date`. ISO datetime,
Expand Down Expand Up @@ -483,16 +503,18 @@ export class ZodSchemaFactory<
return this.extraValidationsEnabled ? ZodUtils.addCustomValidation(schema, typeDef.attributes) : schema;
}
const func = typeDef.strict ? z.strictObject : z.looseObject;
const schema = func(
const objectSchema = func(
Object.fromEntries(
Object.entries(typeDef.fields).map(([field, def]) => {
// Wrap nested typedef references in z.lazy() so cyclic or self-referencing
// typedefs don't recurse infinitely while building schemas.
let fieldSchema: ZodType = isTypeDef(this.schema, def.type)
? z.lazy(() => this.makeTypeDefSchema(def.type))
: this.makeScalarSchema(def.type);
// Wrap nested object typedef references in z.lazy() so cyclic or self-referencing
// typedefs don't recurse infinitely while building schemas. All other fields get
// their validation attributes applied, the same as model fields.
let fieldSchema: ZodType =
isTypeDef(this.schema, def.type) && !isPrimitiveTypeDef(this.schema, def.type)
? z.lazy(() => this.makeTypeDefSchema(def.type))
: this.makeScalarSchema(def.type, this.getItemAttributes(def));
if (def.array) {
fieldSchema = fieldSchema.array();
fieldSchema = ZodUtils.addListValidation(fieldSchema.array(), def.attributes);
}
if (def.optional) {
fieldSchema = fieldSchema.nullish();
Expand All @@ -501,6 +523,9 @@ export class ZodSchemaFactory<
}),
),
);
const schema = this.extraValidationsEnabled
? ZodUtils.addCustomValidation(objectSchema, typeDef.attributes)
: objectSchema;

// zod doesn't preserve object field order after parsing, here we use a
// validation-only custom schema and use the original data if parsing
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.10.0",
"version": "3.10.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
Loading
Loading