zenstackhq / zenstack

Fullstack TypeScript toolkit enhances Prisma ORM with flexible Authorization layer for RBAC/ABAC/PBAC/ReBAC, offering auto-generated type-safe APIs and frontend hooks.
https://zenstack.dev
MIT License
1.83k stars 78 forks source link

[BUG] extended tables do not preserve `@id`'s `@default(autoincrement())` #1520

Closed piscopancer closed 1 week ago

piscopancer commented 2 weeks ago

Bug

This operation fails (prisma throws an error)...

const res = await db.course.create({
  data: {
    title: 'English classes',
    tutorId: 2,
    students: {
      connect: [
        {
          id: 1,
        },
      ],
    },
    addedToNotifications: {
      createMany: {
        data: [
          {
            // fails here because due to a mismatch `id` is expected by prisma but typescript marks it as optional
            receiverId: 1,
            senderId: 2,
          },
        ],
      },
    },
  },
})
PrismaClientValidationError: 
Invalid `prisma.course.create()` invocation:

{
  data: {
    title: "English classes",
    tutorId: 2,
    students: {
      connect: [
        {
          id: 1
        }
      ]
    },
    addedToNotifications: {
      createMany: {
        data: [
          {
            delegate_aux_notification: {
              create: {
                type: "AddedToCourseNotification",
                receiverId: 1,
                senderId: 2
              }
            }
          }
        ]
      }
    }
  }
}

Argument `id` is missing.

... because generated prisma schema wrongly replicates id from the table it extends from. See the model for my MTI relations

schema.zmodel

model Notification {
  id         Int              @id @default(autoincrement())
  createdAt  DateTime         @default(now())
  type       NotificationType

  senderId   Int
  sender     User             @relation("user-notification-sent", fields: [senderId], references: [id], onDelete: Cascade)
  receiverId Int
  receiver   User             @relation("user-notification-received", fields: [receiverId], references: [id], onDelete: Cascade)

  @@delegate (type)
}

model AddedToGroupNotification extends Notification {
  groupId Int   
  group   Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
}

model AddedToCourseNotification  extends Notification {
  courseId Int    
  course   Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
}

now take a look at the generated prisma.schema

prisma.schema

/// @@delegate(type)
model Notification {
  id                                     Int                        @id() @default(autoincrement())
  createdAt                              DateTime                   @default(now())
  type                                   NotificationType
  senderId                               Int
  sender                                 User                       @relation("user-notification-sent", fields: [senderId], references: [id], onDelete: Cascade)
  receiverId                             Int
  receiver                               User                       @relation("user-notification-received", fields: [receiverId], references: [id], onDelete: Cascade)
  delegate_aux_addedToGroupNotification  AddedToGroupNotification?
  delegate_aux_addedToCourseNotification AddedToCourseNotification?
}

model AddedToGroupNotification {
  id                        Int          @id()
  groupId                   Int          
  group                     Group        @relation(fields: [groupId], references: [id], onDelete: Cascade)
  delegate_aux_notification Notification @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade)
}

model AddedToCourseNotification {
  id                        Int          @id()
  courseId                  Int          
  course                    Course       @relation(fields: [courseId], references: [id], onDelete: Cascade)
  delegate_aux_notification Notification @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade)
}

As you can clearly see, id columns of AddedToGroupNotification and AddedToCourseNotification do not have parent's properties/functions, namely @default(autoincrement()). Because of this, the operation from the first code block fails even though typescript, generated by zenstack allows omitting id which is obviously an unexpected behavior and mismatch:

image

Here is a small video to give you a view from my perspective

https://github.com/zenstackhq/zenstack/assets/109352196/702b5b36-9552-452b-a30c-576e4b01a18f

An attempt to fix it

Trying to explicitly provide the create operation with an id results in broken code

const res = await db.course.create({
  data: {
    title: 'English classes',
    tutorId: 2,
    students: {
      connect: [
        {
          id: 1,
        },
      ],
    },
    addedToNotifications: {
      createMany: {
        data: [
          {
            id: 1, // adding an explicit id, no typescript error though
            receiverId: 1,
            senderId: 2,
          },
        ],
      },
    },
  },
})

throws the following error

PrismaClientValidationError:
Invalid `prisma.course.create()` invocation:

{
  data: {
    title: "English classes",
    tutorId: 2,
    students: {
      connect: [
        {
          id: 1
        }
      ]
    },
    addedToNotifications: {
      createMany: {
        data: [
          {
            id: 1, 
            delegate_aux_notification: {
              create: {
                type: "AddedToCourseNotification",
                receiverId: 1,
                senderId: 2
              }
            }
          }
        ]
      }
    }
  }
}

Unknown argument `delegate_aux_notification`. Available options are marked with ?.

As you can see, id was not inserted where it was supposed to go, inside create object to join other fields like type, receiverId and senderId. I have no idea how to fix it atm

Setup

piscopancer commented 2 weeks ago

Must be the duplicate of/similar to #1518.

ymc9 commented 2 weeks ago

Hi @piscopancer , do you mind sharing the definition of the Course model in ZModel? I'd like to understand how it uses the polymorphic models.

piscopancer commented 2 weeks ago

@ymc9 sure

model Course {
  id                   Int                         @id @default(autoincrement())
  createdAt            DateTime                    @default(now())
  title                String
  description          String?

  tutorId              Int
  tutor                User                        @relation("created-courses", fields: [tutorId], references: [id], onDelete: Cascade)
  groups               Group[]
  works                Work[]                      @relation("courses-works")
  students             User[]                      @relation("participated-courses")
  addedToNotifications AddedToCourseNotification[]
  worksStates          WorkState[]
  attempts             Attempt[]
}
ymc9 commented 1 week ago

Hi @piscopancer , it should have been fixed in the v2.2.4 release. Could you upgrade and give it a try?

piscopancer commented 1 week ago

@ymc9 incredible! the command from above works now, thank you for maintaining this project winks