zenstack icon indicating copy to clipboard operation
zenstack copied to clipboard

typescript types broken on zod schema plugin on delegated tables in zenstack v2.5.1

Open tmax22 opened this issue 1 year ago • 4 comments

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

tmax22 avatar Sep 09 '24 16:09 tmax22

Thanks for reporting this @tmax22 . I'll look into it.

ymc9 avatar Sep 12 '24 01:09 ymc9

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:

  1. When enhancerPlugin executes, it depends on Zod types generated by zodPlugin, but zodPlugin hasn't run yet, causing type check failures.
  2. zodPlugin is 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.

zysam avatar Sep 12 '24 08:09 zysam

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:

  1. When enhancerPlugin executes, it depends on Zod types generated by zodPlugin, but zodPlugin hasn't run yet, causing type check failures.
  2. zodPlugin is 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.

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 avatar Sep 12 '24 14:09 ymc9

@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.

zysam avatar Sep 12 '24 16:09 zysam

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.

image

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.

bbozzay avatar Oct 04 '24 03:10 bbozzay

@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.

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.

ymc9 avatar Oct 09 '24 01:10 ymc9

// 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.

ymc9 avatar Oct 09 '24 01:10 ymc9

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

tmax22 avatar Oct 09 '24 07:10 tmax22

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.

ymc9 avatar Oct 09 '24 19:10 ymc9

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")
}

bbozzay avatar Oct 09 '24 21:10 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")

}

Thanks for giving more context @bbozzay . Is it possible to share a minimum repro that resembles your setup?

ymc9 avatar Oct 09 '24 21:10 ymc9

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.

bbozzay avatar Oct 09 '24 21:10 bbozzay

Closing for now. Please reactive if you run into it again.

ymc9 avatar Nov 08 '24 01:11 ymc9