Skip to content

on v2.6.0 some fields on concrete model inherited from polymorphic base model disappear #1734

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

Closed
tmax22 opened this issue Sep 23, 2024 · 10 comments

Comments

@tmax22
Copy link

tmax22 commented Sep 23, 2024

for example, for the given input zmodel schema:

generator client {
    provider = "prisma-client-js"
    binaryTargets = ["native", "rhel-openssl-3.0.x"]
}

datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
}

abstract model Base {
    id        String   @id @default(uuid())
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt()
}

model ProductStage extends Base {
    stage      String

    stageTable String @default("")
    @@delegate(stageTable)
}


model ClientRequirementsStage extends ProductStage {
    someField String
}

you would get:

//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE                                                                  //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
//////////////////////////////////////////////////////////////////////////////////////////////

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "rhel-openssl-3.0.x"]
}

/// @@delegate(stageTable)
model ProductStage {
  id                                   String                   @id() @default(uuid())
  createdAt                            DateTime                 @default(now())
  updatedAt                            DateTime                 @updatedAt()
  stage                                String
  stageTable                           String                   @default("")
  delegate_aux_clientRequirementsStage ClientRequirementsStage?
}

model ClientRequirementsStage {
  id                        String       @id() @default(uuid())
  someField                 String
  delegate_aux_productStage ProductStage @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade)
}

note that ClientRequirementsStage is missing createdAt and updatedAt. the expected output would be:

//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE                                                                  //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
//////////////////////////////////////////////////////////////////////////////////////////////

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "rhel-openssl-3.0.x"]
}

/// @@delegate(stageTable)
model ProductStage {
  id                                   String                   @id() @default(uuid())
  createdAt                            DateTime                 @default(now())
  updatedAt                            DateTime                 @updatedAt()
  stage                                String
  stageTable                           String                   @default("")
  delegate_aux_clientRequirementsStage ClientRequirementsStage?
}

model ClientRequirementsStage {
  id                        String       @id() @default(uuid())
  createdAt                            DateTime                 @default(now())
  updatedAt                            DateTime                 @updatedAt()
  someField                 String
  delegate_aux_productStage ProductStage @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade)
}
@svetch
Copy link
Contributor

svetch commented Sep 23, 2024

Hi!

A similar issue happend with me.
I'm not sure that these fields should be included in a concrete model at database level, but this causing an error.

If a concrete model field access policy has a relation condition, Prisma throws an error:

  console.log
    prisma:error
    Invalid `prisma.profile.findFirst()` invocation:

    {
      where: {
        id: "cm1f40s6t0000dofw8o2j6ozw"
      },
      include: {
        delegate_aux_organization: {
          where: {
            AND: []
          },
          select: {
            id: true,
            createdAt: true,
            ~~~~~~~~~
            updatedAt: true,
            displayName: true,
            type: true,
            ownerId: true,
            published: true,
            access: {
              select: {
                user: {
                  select: {
                    id: true
                  }
                }
              }
            },
    ?       owner?: true,
    ?       delegate_aux_profile?: true,
    ?       _count?: true
          }
        },
        delegate_aux_user: {
          where: {
            AND: []
          }
        }
      }
    }

    Unknown field `createdAt` for select statement on model `Organization`. Available options are marked with ?.

I created a regression test that reproduces the issue:

import { loadSchema } from '@zenstackhq/testtools';
describe('issue new', () => {
    it('regression', async () => {
        const { enhance, enhanceRaw, prisma } = await loadSchema(
            `
                abstract model Base {
                    id        String   @id @default(cuid())
                    createdAt DateTime @default(now())
                    updatedAt DateTime @updatedAt
                }

                model Profile extends Base {
                    displayName String
                    type        String

                    @@allow('read', true)
                    @@delegate(type)
                }

                model User extends Profile {
                    username     String         @unique
                    access       Access[]
                    organization Organization[]
                }

                model Access extends Base {
                    user           User         @relation(fields: [userId], references: [id])
                    userId         String

                    organization   Organization @relation(fields: [organizationId], references: [id])
                    organizationId String

                    manage         Boolean      @default(false)

                    superadmin     Boolean      @default(false)

                    @@unique([userId,organizationId])
                }

                model Organization extends Profile {
                    owner     User     @relation(fields: [ownerId], references: [id])
                    ownerId   String   @default(auth().id)
                    published Boolean  @default(false) @allow('read', access?[user == auth()]) // <-- this policy is causing the issue
                    access    Access[]
                }

            `,
            {
                logPrismaQuery: true,
            }
        );
        const db = enhance();
        const rootDb = enhanceRaw(prisma, undefined, {
            kinds: ['delegate'],
        });

        const user = await rootDb.user.create({
            data: {
                username: 'test',
                displayName: 'test',
            },
        });

        const organization = await rootDb.organization.create({
            data: {
                displayName: 'test',
                owner: {
                    connect: {
                        id: user.id,
                    },
                },
                access: {
                    create: {
                        user: {
                            connect: {
                                id: user.id,
                            },
                        },
                        manage: true,
                        superadmin: true,
                    },
                },
            },
        });

        const foundUser = await db.profile.findFirst({
            where: {
                id: user.id,
            },
        });

        expect(foundUser).toBeTruthy();
    });
});

@ymc9
Copy link
Member

ymc9 commented Sep 23, 2024

for example, for the given input zmodel schema:

generator client {
    provider = "prisma-client-js"
    binaryTargets = ["native", "rhel-openssl-3.0.x"]
}

datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
}

abstract model Base {
    id        String   @id @default(uuid())
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt()
}

model ProductStage extends Base {
    stage      String

    stageTable String @default("")
    @@delegate(stageTable)
}


model ClientRequirementsStage extends ProductStage {
    someField String
}

you would get:

//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE                                                                  //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
//////////////////////////////////////////////////////////////////////////////////////////////

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "rhel-openssl-3.0.x"]
}

/// @@delegate(stageTable)
model ProductStage {
  id                                   String                   @id() @default(uuid())
  createdAt                            DateTime                 @default(now())
  updatedAt                            DateTime                 @updatedAt()
  stage                                String
  stageTable                           String                   @default("")
  delegate_aux_clientRequirementsStage ClientRequirementsStage?
}

model ClientRequirementsStage {
  id                        String       @id() @default(uuid())
  someField                 String
  delegate_aux_productStage ProductStage @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade)
}

note that ClientRequirementsStage is missing createdAt and updatedAt. the expected output would be:

//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE                                                                  //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
//////////////////////////////////////////////////////////////////////////////////////////////

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "rhel-openssl-3.0.x"]
}

/// @@delegate(stageTable)
model ProductStage {
  id                                   String                   @id() @default(uuid())
  createdAt                            DateTime                 @default(now())
  updatedAt                            DateTime                 @updatedAt()
  stage                                String
  stageTable                           String                   @default("")
  delegate_aux_clientRequirementsStage ClientRequirementsStage?
}

model ClientRequirementsStage {
  id                        String       @id() @default(uuid())
  createdAt                            DateTime                 @default(now())
  updatedAt                            DateTime                 @updatedAt()
  someField                 String
  delegate_aux_productStage ProductStage @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade)
}

Hi @tmax22 ,

The generated Prisma schema looks correct to me. ProductStage inherits the abstract Base so it gets the createdAt and updatedAt fields into its model. However, since ClientRequirementsStage is a polymorphic inheritance, it doesn't copy over the fields from its base, and instead at runtime will fetch them through reading the delegate_aux relation.

Have you run into any runtime issue?

@ymc9
Copy link
Member

ymc9 commented Sep 23, 2024

Hi!

A similar issue happend with me. I'm not sure that these fields should be included in a concrete model at database level, but this causing an error.

If a concrete model field access policy has a relation condition, Prisma throws an error:

  console.log
    prisma:error
    Invalid `prisma.profile.findFirst()` invocation:

    {
      where: {
        id: "cm1f40s6t0000dofw8o2j6ozw"
      },
      include: {
        delegate_aux_organization: {
          where: {
            AND: []
          },
          select: {
            id: true,
            createdAt: true,
            ~~~~~~~~~
            updatedAt: true,
            displayName: true,
            type: true,
            ownerId: true,
            published: true,
            access: {
              select: {
                user: {
                  select: {
                    id: true
                  }
                }
              }
            },
    ?       owner?: true,
    ?       delegate_aux_profile?: true,
    ?       _count?: true
          }
        },
        delegate_aux_user: {
          where: {
            AND: []
          }
        }
      }
    }

    Unknown field `createdAt` for select statement on model `Organization`. Available options are marked with ?.

I created a regression test that reproduces the issue:

import { loadSchema } from '@zenstackhq/testtools';
describe('issue new', () => {
    it('regression', async () => {
        const { enhance, enhanceRaw, prisma } = await loadSchema(
            `
                abstract model Base {
                    id        String   @id @default(cuid())
                    createdAt DateTime @default(now())
                    updatedAt DateTime @updatedAt
                }

                model Profile extends Base {
                    displayName String
                    type        String

                    @@allow('read', true)
                    @@delegate(type)
                }

                model User extends Profile {
                    username     String         @unique
                    access       Access[]
                    organization Organization[]
                }

                model Access extends Base {
                    user           User         @relation(fields: [userId], references: [id])
                    userId         String

                    organization   Organization @relation(fields: [organizationId], references: [id])
                    organizationId String

                    manage         Boolean      @default(false)

                    superadmin     Boolean      @default(false)

                    @@unique([userId,organizationId])
                }

                model Organization extends Profile {
                    owner     User     @relation(fields: [ownerId], references: [id])
                    ownerId   String   @default(auth().id)
                    published Boolean  @default(false) @allow('read', access?[user == auth()]) // <-- this policy is causing the issue
                    access    Access[]
                }

            `,
            {
                logPrismaQuery: true,
            }
        );
        const db = enhance();
        const rootDb = enhanceRaw(prisma, undefined, {
            kinds: ['delegate'],
        });

        const user = await rootDb.user.create({
            data: {
                username: 'test',
                displayName: 'test',
            },
        });

        const organization = await rootDb.organization.create({
            data: {
                displayName: 'test',
                owner: {
                    connect: {
                        id: user.id,
                    },
                },
                access: {
                    create: {
                        user: {
                            connect: {
                                id: user.id,
                            },
                        },
                        manage: true,
                        superadmin: true,
                    },
                },
            },
        });

        const foundUser = await db.profile.findFirst({
            where: {
                id: user.id,
            },
        });

        expect(foundUser).toBeTruthy();
    });
});

Hi @svetch , I appreciate the test case! Looking into it now.

@ymc9
Copy link
Member

ymc9 commented Sep 23, 2024

Hey, could you guys help check if the new 2.6.1 release resolves the issue for you? @svetch @tmax22

@svetch
Copy link
Contributor

svetch commented Sep 24, 2024

@ymc9 The new release resolves the issue on my end. Thanks for the quick fix!

@ymc9
Copy link
Member

ymc9 commented Sep 24, 2024

@ymc9 The new release resolves the issue on my end. Thanks for the quick fix!

Awesome. Thanks for the confirmation!

@tmax22
Copy link
Author

tmax22 commented Sep 24, 2024

ProductStage inherits the abstract Base so it gets the createdAt and updatedAt fields into its model. However, since ClientRequirementsStage is a polymorphic inheritance, it doesn't copy over the fields from its base, and instead at runtime will fetch them through reading the delegate_aux relation

actually, it make sense, but it does introduce a breaking change(since v2.2.4 introduces the schema with the polymorphic model fields). it means that i need to write my schema like this:

abstract model Base {
    id        String   @id @default(uuid())
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt()
}

model ProductStage extends Base {
    stage      String

    stageTable String @default("")
    @@delegate(stageTable)
}


model ClientRequirementsStage extends ProductStage,Base {
    someField String
}

(note: ClientRequirementsStage inherents both from ProductStage and Base)
is should be documented as well.

however, it does make some issues.
in this example, you get Model can include at most one field with @id attribute error:

image

how should i make ClientRequirementsStage inherent both from ProductStage and Base in this case?

@ymc9
Copy link
Member

ymc9 commented Sep 25, 2024

ProductStage inherits the abstract Base so it gets the createdAt and updatedAt fields into its model. However, since ClientRequirementsStage is a polymorphic inheritance, it doesn't copy over the fields from its base, and instead at runtime will fetch them through reading the delegate_aux relation

actually, it make sense, but it does introduce a breaking change(since v2.2.4 introduces the schema with the polymorphic model fields). it means that i need to write my schema like this:

abstract model Base {
    id        String   @id @default(uuid())
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt()
}

model ProductStage extends Base {
    stage      String

    stageTable String @default("")
    @@delegate(stageTable)
}


model ClientRequirementsStage extends ProductStage,Base {
    someField String
}

(note: ClientRequirementsStage inherents both from ProductStage and Base) is should be documented as well.

however, it does make some issues. in this example, you get Model can include at most one field with @id attribute error:

image

how should i make ClientRequirementsStage inherent both from ProductStage and Base in this case?

For such usage, the Base model is inherited twice, causing a conflict ... Right now it's an unsupported scenario. Please consider creating a separate issue for it. I guess you'll have to avoid inheriting Base from ClientRequirementStage for now.

@tmax22
Copy link
Author

tmax22 commented Sep 25, 2024

actually, after checking, it does not currently pose an issue as long we use zenstack to fetch the data because these 'hidden' fields are really fetched at runtime via the delegate_aux_... relation as you explained.
and again thanks for your awsome work!

@tmax22 tmax22 closed this as completed Sep 25, 2024
@j0rdanba1n
Copy link
Contributor

j0rdanba1n commented Oct 10, 2024

I'm encountering a similar issue. My zmodels are:

abstract model BaseAuth {
    id             String        @id @default(uuid())
    dateCreated    DateTime      @default(now())
    dateUpdated    DateTime      @updatedAt @default(now())

    organizationId String?
    organization   Organization? @relation(fields: [organizationId], references: [id], name: "organization")

    @@allow('all', organization.users?[user == auth()])
}

enum ResourceType {
    Personnel
}

model Resource extends BaseAuth {
    name     String?
    type     ResourceType?
    costRate Int?

    budgets  ResourceBudget[]

    @@delegate(type)
}

model Personnel extends Resource {
}

I get this error when trying to query a budget object, including resources:

  const {
    data: budget,
    isLoading,
    refetch,
  } = Api.budget.findUnique.useQuery({
    where: { id: budgetId as string },
    include: {
      periods: true,
      resourceBudgets: {
        include: { resource: true },
      },
    },
  })
Invalid `prisma.budget.findUnique()` invocation:

{
  include: {
    periods: {
      where: {
        OR: [
          {
            budget: {
              OR: [
                {
                  organization: {
                    users: {
                      some: {
                        user: {
                          is: {
                            id: "e2802b95-b51d-4474-a821-8a79172bbae5"
                          }
                        }
                      }
                    }
                  }
                }
              ]
            }
          }
        ]
      }
    },
    resourceBudgets: {
      include: {
        resource: {
          where: {
            OR: [
              {
                OR: [
                  {
                    organization: {
                      users: {
                        some: {
                          user: {
                            is: {
                              id: "e2802b95-b51d-4474-a821-8a79172bbae5"
                            }
                          }
                        }
                      }
                    }
                  }
                ]
              }
            ]
          },
          include: {
            delegate_aux_personnel: {
              where: {
                OR: [
                  {
                    OR: [
                      {
                        organization: {
                          users: {
                            some: {
                              user: {
                                is: {
                                  id: "e2802b95-b51d-4474-a821-8a79172bbae5"
                                }
                              }
                            }
                          }
                        }
                      }
                    ]
                  }
                ]
              },
              include: {
                delegate_aux_resource: {}
              }
            }
          }
        }
      },
      where: {
        OR: [
          {
            OR: [
              {
                budget: {
                  OR: [
                    {
                      organization: {
                        users: {
                          some: {
                            user: {
                              is: {
                                id: "e2802b95-b51d-4474-a821-8a79172bbae5"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              },
              {
                resource: {
                  OR: [
                    {
                      organization: {
                        users: {
                          some: {
                            user: {
                              is: {
                                id: "e2802b95-b51d-4474-a821-8a79172bbae5"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    }
  },
  where: {
    id: "6e278f65-0b19-43f6-9eab-1530795339d5",
    AND: [
      {
        OR: [
          {
            OR: [
              {
                organization: {
                  users: {
                    some: {
                      user: {
                        is: {
                          id: "e2802b95-b51d-4474-a821-8a79172bbae5"
                        }
                      }
                    }
                  }
                }
              }
            ]
          }
        ]
      }
    ],
    organizationId: "aab8670a-f927-494a-90c1-eac9258eae09"
  }
}

I believe the problem is that the policy @@allow('all', organization.users?[user == auth()]) is being applied to delegate_aux_personnel instead of delegate_aux_resource.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants