zenstack
zenstack copied to clipboard
typescript types broken on zod schema plugin on delegated tables in zenstack v2.5.1
zenstack v2.5.1 introduces new error.
for example for this schema:
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
plugin zod {
provider = '@core/zod'
}
model Animal {
id String @id @default(uuid())
animalType String @default("")
@@delegate(animalType)
}
model Dog extends Animal {
name String
}
you get the following error:
~/development/projects/tmp/zenstack-sample-issue ············································································································································································································································ 6s 19:12:41 ─╮
❯ zenstack generate ─╯
⌛️ ZenStack CLI v2.5.1, running plugins
✔ Generating Prisma schema
✔ Generating PrismaClient enhancer
✔ Generating Zod schemas
Error compiling generated code:
node_modules/.pnpm/@[email protected]_@[email protected][email protected]_/node_modules/.zenstack/zod/models/Animal.schema.ts:41:23 - error TS2322: Type 'boolean' is not assignable to type 'never'.
41 id: true, animalType: true
~~~~~~~~~~
node_modules/.pnpm/@[email protected]_@[email protected][email protected]_/node_modules/.zenstack/zod/models/Animal.schema.ts:49:23 - error TS2322: Type 'boolean' is not assignable to type 'never'.
49 id: true, animalType: true
~~~~~~~~~~
node_modules/.pnpm/@[email protected]_@[email protected][email protected]_/node_modules/.zenstack/zod/models/Dog.schema.ts:43:23 - error TS2322: Type 'boolean' is not assignable to type 'never'.
43 id: true, animalType: true
~~~~~~~~~~
node_modules/.pnpm/@[email protected]_@[email protected][email protected]_/node_modules/.zenstack/zod/models/Dog.schema.ts:51:23 - error TS2322: Type 'boolean' is not assignable to type 'never'.
51 id: true, animalType: true
~~~~~~~~~~
: Error compiling generated code
removing zod plugin or @@delegate field would relax this issue. currently we decided not to upgrade because we are using zod schemas in our app.
Environment (please complete the following information):
- ZenStack version: 2.5.1
- Prisma version: 5.15.1
- Database type: Postgresql
Thanks for reporting this @tmax22 . I'll look into it.
I've encountered an issue with custom Zod plugins in ZenStack's CLI. The generation order is incorrect, causing Zod type checks to fail.
✔ Generating Prisma schema
✔ Generating PrismaClient enhancer
✔ Generating Zod schemas
✔ Generating Zod schemas
After analyzing the plugin-runner.ts file, I found that ZenStack executes corePlugins before userPlugins during generation. The corePlugins execution order is prismaPlugin, enhancerPlugin, and zodPlugin. The zodPlugin here is ZenStack's built-in Zod plugin (default configuration).
At the same time, userPlugins includes user-defined plugins, which may also include a zodPlugin (custom configuration).
This leads to two issues:
- When
enhancerPluginexecutes, it depends on Zod types generated byzodPlugin, butzodPluginhasn't run yet, causing type check failures. zodPluginis executed twice.
I propose a simple code modification (without breaking the calculateAllPlugins logic):
let { corePlugins, userPlugins } = this.calculateAllPlugins(runnerOptions, plugins);
// filter prisma plugin
const prismaPlugin = corePlugins.find((p) => p.provider === CorePlugins.Prisma);
// filter core zod plugin
const coreZodPlugin = corePlugins.find((p) => p.provider === CorePlugins.Zod);
// filter enhancer plugin
const enhancerPlugin = corePlugins.find((p) => p.provider === CorePlugins.Enhancer);
// filter zod plugin in userPlugins
const userZodPlugin = userPlugins.find((p) => p.provider === CorePlugins.Zod);
const zodPlugin = userZodPlugin ?? coreZodPlugin;
// filter not zod plugin in userPlugins
const notZodPlugin = userPlugins.filter((p) => p.provider !== CorePlugins.Zod);
corePlugins = [prismaPlugin, zodPlugin, enhancerPlugin] as PluginInfo[];
userPlugins = notZodPlugin;
This execution order ensures that zodPlugin runs before enhancerPlugin. Alternatively, type checking could be removed from enhancerPlugin before generation.
Note: When I linked ZenStack locally and ran generate, it worked normally. I suspect this might be related to tsconfig.json.
I've encountered an issue with custom Zod plugins in ZenStack's CLI. The generation order is incorrect, causing Zod type checks to fail.
✔ Generating Prisma schema ✔ Generating PrismaClient enhancer ✔ Generating Zod schemas ✔ Generating Zod schemasAfter analyzing the
plugin-runner.tsfile, I found that ZenStack executescorePluginsbeforeuserPluginsduring generation. ThecorePluginsexecution order isprismaPlugin,enhancerPlugin, andzodPlugin. ThezodPluginhere is ZenStack's built-in Zod plugin (default configuration).At the same time,
userPluginsincludes user-defined plugins, which may also include azodPlugin(custom configuration).This leads to two issues:
- When
enhancerPluginexecutes, it depends on Zod types generated byzodPlugin, butzodPluginhasn't run yet, causing type check failures.zodPluginis executed twice.I propose a simple code modification (without breaking the
calculateAllPluginslogic):let { corePlugins, userPlugins } = this.calculateAllPlugins(runnerOptions, plugins); // filter prisma plugin const prismaPlugin = corePlugins.find((p) => p.provider === CorePlugins.Prisma); // filter core zod plugin const coreZodPlugin = corePlugins.find((p) => p.provider === CorePlugins.Zod); // filter enhancer plugin const enhancerPlugin = corePlugins.find((p) => p.provider === CorePlugins.Enhancer); // filter zod plugin in userPlugins const userZodPlugin = userPlugins.find((p) => p.provider === CorePlugins.Zod); const zodPlugin = userZodPlugin ?? coreZodPlugin; // filter not zod plugin in userPlugins const notZodPlugin = userPlugins.filter((p) => p.provider !== CorePlugins.Zod); corePlugins = [prismaPlugin, zodPlugin, enhancerPlugin] as PluginInfo[]; userPlugins = notZodPlugin;This execution order ensures that
zodPluginruns beforeenhancerPlugin. Alternatively, type checking could be removed fromenhancerPluginbefore generation.Note: When I linked ZenStack locally and ran
generate, it worked normally. I suspect this might be related totsconfig.json.
Hi @zysam , thanks for the information. Do you have a user-defined zod plugin in ZModel? Do you mind sharing the plugin sections of your model? Thanks!
@ymc9 Sure.
datasource db {
provider = 'sqlite'
url = 'file:../data/dev.sqlite'
}
generator client {
provider = "prisma-client-js"
output = "../generated/prisma/client"
}
plugin zod {
provider = '@core/zod'
output = './generated/zod'
compile = false
}
plugin enhancer {
provider = '@core/enhancer'
output = './generated/zenstack'
compile = false
preserveTsFiles = true
}
model User {
id Int @id @default(autoincrement())
name String @unique
email String? @email @unique
password String? @password @omit
access Access[]
ownedResources Resource[]
// can be created by anyone, even not logged in
@@allow('create', true)
// full access by oneself
@@allow('all', auth() == this)
}
model Access {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
resource Resource @relation(fields: [resourceId], references: [id], onDelete: Cascade)
resourceId Int
// view permission
view Boolean?
// manage permission
manage Boolean?
// resource owner has full control over its access list
@@allow('all', resource.owner == auth())
}
model Resource {
id Int @id @default(autoincrement())
name String
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
ownerId Int @default(auth().id)
access Access[]
// owner has full control
@@allow('all', owner == auth())
// readable if there exists a "read" permission for the current user
@@allow('read', access?[user == auth() && view])
// writeable if there exists a "manage" permission for the current user
@@allow('update,delete', access?[user == auth() && manage])
}
Note: compile = false doesn't work. in PluginRunner, I try to add some code like that.
for (const { name, description, run, options: pluginOptions } of corePlugins) {
const options = { ...pluginOptions, prismaClientPath };
// debug compile
console.log('running core plugin', name, options, 'runnerOptions', runnerOptions.compile);
// @ts-ignore
runnerOptions.compile = !!options.compile;
P.S.: suggest using custom output dir in the workspace which use pnpm.
I think my issue is related to this. If I use delegate, zod typechecking fails during generation, but only when I use the --output flag. It works properly when I don't specify an output.
Here is the delegated model in the simplest form:
// Base model for ListItem
model ListItem {
id String @id @db.Uuid()
value String
contentType String
@@delegate(contentType)
}
// AppItem inherits from Item
model ListItemApp extends ListItem {
name String
}
Deleting ListItemApp fixes the problem so that I can use the output flag.
@ymc9 Sure.
datasource db { provider = 'sqlite' url = 'file:../data/dev.sqlite' } generator client { provider = "prisma-client-js" output = "../generated/prisma/client" } plugin zod { provider = '@core/zod' output = './generated/zod' compile = false } plugin enhancer { provider = '@core/enhancer' output = './generated/zenstack' compile = false preserveTsFiles = true } model User { id Int @id @default(autoincrement()) name String @unique email String? @email @unique password String? @password @omit access Access[] ownedResources Resource[] // can be created by anyone, even not logged in @@allow('create', true) // full access by oneself @@allow('all', auth() == this) } model Access { id Int @id @default(autoincrement()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int resource Resource @relation(fields: [resourceId], references: [id], onDelete: Cascade) resourceId Int // view permission view Boolean? // manage permission manage Boolean? // resource owner has full control over its access list @@allow('all', resource.owner == auth()) } model Resource { id Int @id @default(autoincrement()) name String owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) ownerId Int @default(auth().id) access Access[] // owner has full control @@allow('all', owner == auth()) // readable if there exists a "read" permission for the current user @@allow('read', access?[user == auth() && view]) // writeable if there exists a "manage" permission for the current user @@allow('update,delete', access?[user == auth() && manage]) }Note:
compile = falsedoesn't work. in PluginRunner, I try to add some code like that.for (const { name, description, run, options: pluginOptions } of corePlugins) { const options = { ...pluginOptions, prismaClientPath }; // debug compile console.log('running core plugin', name, options, 'runnerOptions', runnerOptions.compile); // @ts-ignore runnerOptions.compile = !!options.compile;P.S.: suggest using custom output dir in the workspace which use pnpm.
Hi @zysam , thanks for the detailed repro. It's a problematic design that each core plugin (enhancer, zod) allows to specify its own output dir, since they are not really independent - this can result in combinations that are confusing and not working.
What you need can be achieved by using CLI options instead:
npx zenstack --no-compile --output ./generated
The --output switch sets the output dir for all core plugins, and the "--no-compile" option turns of compilation for all of them.
I think in V3 we'll probably deprecate the "output" and "compile" options from the core plugin level and only allow to control them from the CLI.
// Base model for ListItem model ListItem { id String @id @db.Uuid() value String contentType String
@@delegate(contentType) }
// AppItem inherits from Item model ListItemApp extends ListItem { name String }
Hi @bbozzay , do you have settings for core plugins in your zmodel? I tried to reproduce the issue with your models and `npx zenstack generate --output generated" but couldn't see the error.
i don't have this specific error anymore in the newer versions of zenstack, (about --output flag: i didn't tried), from my perspective this issue can be closed
i don't have this specific error anymore in the newer versions of zenstack, (about --output flag: i didn't tried), from my perspective this issue can be closed
Thanks for confirming it! I'll wait a bit for comments from @zysam and @bbozzay.
I should add to my previous response: If I set an output path, I get a type checking error and generate stops early. If I don't specify output, the generate command completes without error. However, my sveltekit dev server is unable to find "enhance." Both issues were resolved when I dropped the delegated tables. Another important note is that this is within a mono-repo (turbo repo with pnpm).
Here are the current plugin configurations:
generator client {
provider = "prisma-client-js"
}
plugin enhancer {
provider = '@core/enhancer'
generatePermissionChecker = true
}
plugin prisma {
provider = '@core/prisma'
output = './prisma/schema.prisma'
format = true
generateClient = true
}
plugin hooks {
provider = '@zenstackhq/tanstack-query'
output = './src/lib/hooks/zenstack'
target = 'svelte'
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
I should add to my previous response: If I set an output path, I get a type checking error and generate stops early. If I don't specify output, the generate command completes without error. However, my sveltekit dev server is unable to find "enhance." Both issues were resolved when I dropped the delegated tables. Another important note is that this is within a mono-repo (turbo repo with pnpm).
Here are the current plugin configurations:
generator client { provider = "prisma-client-js" } plugin enhancer { provider = '@core/enhancer' generatePermissionChecker = true } plugin prisma { provider = '@core/prisma' output = './prisma/schema.prisma' format = true generateClient = true } plugin hooks { provider = '@zenstackhq/tanstack-query' output = './src/lib/hooks/zenstack' target = 'svelte' } datasource db { provider = "postgresql" url = env("DATABASE_URL") directUrl = env("DIRECT_URL") }
Thanks for giving more context @bbozzay . Is it possible to share a minimum repro that resembles your setup?
i dont have a repro anymore unfortunately, but I will attempt this upgrade again in the coming weeks and can share if I encounter the issue again.
Closing for now. Please reactive if you run into it again.