diff --git a/LICENSE b/LICENSE index 91d4584d3..62f71afa1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 ZenStack +Copyright (c) 2022-2025 ZenStack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/LICENSE b/packages/LICENSE index 91d4584d3..62f71afa1 100644 --- a/packages/LICENSE +++ b/packages/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 ZenStack +Copyright (c) 2022-2025 ZenStack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/plugins/openapi/src/generator-base.ts b/packages/plugins/openapi/src/generator-base.ts index f91423346..29f6aee6a 100644 --- a/packages/plugins/openapi/src/generator-base.ts +++ b/packages/plugins/openapi/src/generator-base.ts @@ -1,9 +1,9 @@ +import { getZodErrorMessage } from '@zenstackhq/runtime/local-helpers'; import { PluginError, getDataModels, hasAttribute, type PluginOptions, type PluginResult } from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; import type { DMMF } from '@zenstackhq/sdk/prisma'; import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; import semver from 'semver'; -import { fromZodError } from 'zod-validation-error/v3'; import { name } from '.'; import { SecuritySchemesSchema } from './schema'; @@ -94,7 +94,7 @@ export abstract class OpenAPIGeneratorBase { if (securitySchemes) { const parsed = SecuritySchemesSchema.safeParse(securitySchemes); if (!parsed.success) { - throw new PluginError(name, `"securitySchemes" option is invalid: ${fromZodError(parsed.error)}`); + throw new PluginError(name, `"securitySchemes" option is invalid: ${getZodErrorMessage(parsed.error)}`); } return parsed.data; } diff --git a/packages/runtime/src/enhancements/node/policy/handler.ts b/packages/runtime/src/enhancements/node/policy/handler.ts index 7aa22e8c4..a220fedd1 100644 --- a/packages/runtime/src/enhancements/node/policy/handler.ts +++ b/packages/runtime/src/enhancements/node/policy/handler.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import deepmerge from 'deepmerge'; -import { fromZodError } from 'zod-validation-error/v3'; import { CrudFailureReason } from '../../../constants'; import { ModelDataVisitor, @@ -15,7 +14,7 @@ import { type FieldInfo, type ModelMeta, } from '../../../cross'; -import { invariant, lowerCaseFirst, upperCaseFirst } from '../../../local-helpers'; +import { getZodErrorMessage, invariant, lowerCaseFirst, upperCaseFirst } from '../../../local-helpers'; import { EnhancementContext, PolicyOperationKind, type CrudContract, type DbClientContract } from '../../../types'; import type { InternalEnhancementOptions } from '../create-enhancement'; import { Logger } from '../logger'; @@ -420,7 +419,7 @@ export class PolicyProxyHandler implements Pr throw this.policyUtils.deniedByPolicy( model, 'create', - `input failed validation: ${fromZodError(err)}`, + `input failed validation: ${getZodErrorMessage(err)}`, CrudFailureReason.DATA_VALIDATION_VIOLATION, err ); @@ -1267,7 +1266,7 @@ export class PolicyProxyHandler implements Pr throw this.policyUtils.deniedByPolicy( model, 'update', - `input failed validation: ${fromZodError(err)}`, + `input failed validation: ${getZodErrorMessage(err)}`, CrudFailureReason.DATA_VALIDATION_VIOLATION, err ); diff --git a/packages/runtime/src/enhancements/node/policy/policy-utils.ts b/packages/runtime/src/enhancements/node/policy/policy-utils.ts index 08804dee1..d5a63e24f 100644 --- a/packages/runtime/src/enhancements/node/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/node/policy/policy-utils.ts @@ -2,7 +2,6 @@ import deepmerge from 'deepmerge'; import { z, type ZodError, type ZodObject, type ZodSchema } from 'zod'; -import { fromZodError } from 'zod-validation-error/v3'; import { CrudFailureReason, PrismaErrorCode } from '../../../constants'; import { clone, @@ -14,7 +13,13 @@ import { type FieldInfo, type ModelMeta, } from '../../../cross'; -import { isPlainObject, lowerCaseFirst, simpleTraverse, upperCaseFirst } from '../../../local-helpers'; +import { + getZodErrorMessage, + isPlainObject, + lowerCaseFirst, + simpleTraverse, + upperCaseFirst, +} from '../../../local-helpers'; import { AuthUser, CrudContract, @@ -935,7 +940,7 @@ export class PolicyUtil extends QueryUtils { throw this.deniedByPolicy( model, operation, - `entity ${formatObject(uniqueFilter, false)} failed validation: [${fromZodError(err)}]`, + `entity ${formatObject(uniqueFilter, false)} failed validation: [${getZodErrorMessage(err)}]`, CrudFailureReason.DATA_VALIDATION_VIOLATION, err ); @@ -1440,7 +1445,7 @@ export class PolicyUtil extends QueryUtils { if (!parseResult.success) { if (this.logger.enabled('info')) { this.logger.info( - `entity ${model} failed validation for operation ${kind}: ${fromZodError(parseResult.error)}` + `entity ${model} failed validation for operation ${kind}: ${getZodErrorMessage(parseResult.error)}` ); } onError(parseResult.error); diff --git a/packages/runtime/src/local-helpers/index.ts b/packages/runtime/src/local-helpers/index.ts index f9bb01009..a12c5b8b6 100644 --- a/packages/runtime/src/local-helpers/index.ts +++ b/packages/runtime/src/local-helpers/index.ts @@ -1,7 +1,8 @@ -export * from './simple-traverse'; -export * from './sleep'; export * from './is-plain-object'; export * from './lower-case-first'; -export * from './upper-case-first'; export * from './param-case'; +export * from './simple-traverse'; +export * from './sleep'; export * from './tiny-invariant'; +export * from './upper-case-first'; +export * from './zod-utils'; diff --git a/packages/runtime/src/local-helpers/zod-utils.ts b/packages/runtime/src/local-helpers/zod-utils.ts new file mode 100644 index 000000000..f463e5114 --- /dev/null +++ b/packages/runtime/src/local-helpers/zod-utils.ts @@ -0,0 +1,23 @@ +import { type ZodError } from 'zod'; +import { fromZodError as fromZodErrorV3 } from 'zod-validation-error/v3'; +import { fromZodError as fromZodErrorV4 } from 'zod-validation-error/v4'; +import { type ZodError as Zod4Error } from 'zod/v4'; + +/** + * Formats a Zod error message for better readability. Compatible with both Zod v3 and v4. + */ +export function getZodErrorMessage(err: unknown): string { + if (!(err instanceof Error)) { + return 'Unknown error'; + } + + try { + if ('_zod' in err) { + return fromZodErrorV4(err as Zod4Error).message; + } else { + return fromZodErrorV3(err as ZodError).message; + } + } catch { + return err.message; + } +} diff --git a/packages/runtime/src/validation.ts b/packages/runtime/src/validation.ts index a3c153c96..3cb606c1e 100644 --- a/packages/runtime/src/validation.ts +++ b/packages/runtime/src/validation.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { fromZodError } from 'zod-validation-error/v3'; +import { getZodErrorMessage } from './local-helpers'; /** * Error indicating violations of field-level constraints @@ -15,7 +15,7 @@ export function validate(validator: z.ZodType, data: unknown) { try { validator.parse(data); } catch (err) { - throw new ValidationError(fromZodError(err as z.ZodError).message); + throw new ValidationError(getZodErrorMessage(err as z.ZodError)); } } diff --git a/packages/schema/src/cli/config.ts b/packages/schema/src/cli/config.ts index d1fc2d0fb..87fdd8e2f 100644 --- a/packages/schema/src/cli/config.ts +++ b/packages/schema/src/cli/config.ts @@ -1,6 +1,5 @@ import fs from 'fs'; import z, { ZodError } from 'zod'; -import { fromZodError } from 'zod-validation-error/v3'; import { CliError } from './cli-error'; // TODO: future use @@ -28,7 +27,7 @@ export function loadConfig(filename: string) { throw new CliError(`Config is not a valid JSON file: ${filename}`); } if (err instanceof ZodError) { - throw new CliError(`Config file ${filename} is not valid: ${fromZodError(err)}`); + throw new CliError(`Config file ${filename} is not valid: ${err}`); } throw new CliError(`Error loading config: ${filename}`); } diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 866fdbe37..cfec9de47 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -7,16 +7,15 @@ import { PrismaErrorCode, clone, enumerate, - requireField, getIdFields, isPrismaClientKnownRequestError, + requireField, } from '@zenstackhq/runtime'; -import { lowerCaseFirst, upperCaseFirst, paramCase } from '@zenstackhq/runtime/local-helpers'; +import { getZodErrorMessage, lowerCaseFirst, paramCase, upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; import SuperJSON from 'superjson'; import { Linker, Paginator, Relator, Serializer, SerializerOptions } from 'ts-japi'; import UrlPattern from 'url-pattern'; import z, { ZodError } from 'zod'; -import { fromZodError } from 'zod-validation-error/v3'; import { LoggerConfig, Response } from '../../types'; import { APIHandlerBase, RequestContext } from '../base'; import { logWarning, registerCustomSerializers } from '../utils'; @@ -821,7 +820,7 @@ class RequestHandler extends APIHandlerBase { return { error: this.makeError( 'invalidPayload', - fromZodError(parsed.error).message, + getZodErrorMessage(parsed.error), 422, CrudFailureReason.DATA_VALIDATION_VIOLATION, parsed.error @@ -1022,7 +1021,7 @@ class RequestHandler extends APIHandlerBase { if (!parsed.success) { return this.makeError( 'invalidPayload', - fromZodError(parsed.error).message, + getZodErrorMessage(parsed.error), undefined, CrudFailureReason.DATA_VALIDATION_VIOLATION, parsed.error @@ -1053,7 +1052,7 @@ class RequestHandler extends APIHandlerBase { if (!parsed.success) { return this.makeError( 'invalidPayload', - fromZodError(parsed.error).message, + getZodErrorMessage(parsed.error), undefined, CrudFailureReason.DATA_VALIDATION_VIOLATION, parsed.error