Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
tmax22 opened this issue Sep 9, 2024 · 13 comments

Comments

@tmax22
Copy link

tmax22 commented Sep 9, 2024

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
@ymc9
Copy link
Member

ymc9 commented Sep 12, 2024

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

@ymc9 ymc9 modified the milestones: v2.x, v2.5.x Sep 12, 2024
@zysam
Copy link

zysam commented Sep 12, 2024

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.

@ymc9
Copy link
Member

ymc9 commented Sep 12, 2024

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!

@zysam
Copy link

zysam commented Sep 12, 2024

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

@bbozzay
Copy link
Contributor

bbozzay commented Oct 4, 2024

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.

@ymc9 ymc9 modified the milestones: v2.6.x, v2.7.0 Oct 6, 2024
@ymc9
Copy link
Member

ymc9 commented Oct 9, 2024

@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
Copy link
Member

ymc9 commented Oct 9, 2024

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

@tmax22
Copy link
Author

tmax22 commented Oct 9, 2024

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

@ymc9
Copy link
Member

ymc9 commented Oct 9, 2024

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 ymc9 added feedback and removed in-progress labels Oct 9, 2024
@bbozzay
Copy link
Contributor

bbozzay commented Oct 9, 2024

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

@ymc9
Copy link
Member

ymc9 commented Oct 9, 2024

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?

@bbozzay
Copy link
Contributor

bbozzay commented Oct 9, 2024

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.

@ymc9 ymc9 modified the milestones: v2.7.0, v2.8.0 Oct 17, 2024
@ymc9
Copy link
Member

ymc9 commented Nov 8, 2024

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

@ymc9 ymc9 closed this as completed Nov 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants