Closed tmax22 closed 2 weeks ago
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();
});
});
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 missingcreatedAt
andupdatedAt
. 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?
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.
Hey, could you guys help check if the new 2.6.1 release resolves the issue for you? @svetch @tmax22
@ymc9 The new release resolves the issue on my end. Thanks for the quick fix!
@ymc9 The new release resolves the issue on my end. Thanks for the quick fix!
Awesome. Thanks for the confirmation!
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:
how should i make ClientRequirementsStage inherent both from ProductStage and Base in this case?
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 fromProductStage
andBase
) 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: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.
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!
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
.
for example, for the given input zmodel schema:
you would get:
note that
ClientRequirementsStage
is missingcreatedAt
andupdatedAt
. the expected output would be: