Skip to content

Commit 00c53d7

Browse files
authored
fix(sdk): correctly handle mixin fields for delegate model inheritance (#2456)
2 parents bd2b111 + 956a64d commit 00c53d7

File tree

5 files changed

+124
-17
lines changed

5 files changed

+124
-17
lines changed

packages/sdk/src/model-utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,36 @@ export function isDelegateModel(node: AstNode) {
7171
return isDataModel(node) && hasAttribute(node, '@@delegate');
7272
}
7373

74+
/**
75+
* Returns all fields that physically belong to a model's table: its directly declared
76+
* fields plus fields from its mixins (recursively).
77+
*/
78+
export function getOwnedFields(model: DataModel | TypeDef): DataField[] {
79+
const fields: DataField[] = [...model.fields];
80+
for (const mixin of model.mixins) {
81+
if (mixin.ref) {
82+
fields.push(...getOwnedFields(mixin.ref));
83+
}
84+
}
85+
return fields;
86+
}
87+
88+
/**
89+
* Returns the name of the delegate base model that "owns" the given field in the context of
90+
* `contextModel`. This handles both direct fields of delegate models and mixin fields that
91+
* belong to a mixin used by a delegate base model.
92+
*/
93+
export function getDelegateOriginModel(field: DataField, contextModel: DataModel): string | undefined {
94+
let base = contextModel.baseModel?.ref;
95+
while (base) {
96+
if (isDelegateModel(base) && getOwnedFields(base).includes(field)) {
97+
return base.name;
98+
}
99+
base = base.baseModel?.ref;
100+
}
101+
return undefined;
102+
}
103+
74104
export function isUniqueField(field: DataField) {
75105
if (hasAttribute(field, '@unique')) {
76106
return true;

packages/sdk/src/prisma/prisma-schema-generator.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {
4242
import { AstUtils } from 'langium';
4343
import { match } from 'ts-pattern';
4444
import { ModelUtils } from '..';
45-
import { DELEGATE_AUX_RELATION_PREFIX, getIdFields } from '../model-utils';
45+
import { DELEGATE_AUX_RELATION_PREFIX, getDelegateOriginModel, getIdFields } from '../model-utils';
4646
import {
4747
AttributeArgValue,
4848
ModelFieldType,
@@ -204,7 +204,7 @@ export class PrismaSchemaGenerator {
204204
continue; // skip computed fields
205205
}
206206
// exclude non-id fields inherited from delegate
207-
if (ModelUtils.isIdField(field, decl) || !this.isInheritedFromDelegate(field, decl)) {
207+
if (ModelUtils.isIdField(field, decl) || !getDelegateOriginModel(field, decl)) {
208208
this.generateModelField(model, field, decl);
209209
}
210210
}
@@ -311,7 +311,7 @@ export class PrismaSchemaGenerator {
311311
// when building physical schema, exclude `@default` for id fields inherited from delegate base
312312
!(
313313
ModelUtils.isIdField(field, contextModel) &&
314-
this.isInheritedFromDelegate(field, contextModel) &&
314+
getDelegateOriginModel(field, contextModel) &&
315315
attr.decl.$refText === '@default'
316316
),
317317
)
@@ -335,10 +335,6 @@ export class PrismaSchemaGenerator {
335335
return AstUtils.streamAst(expr).some(isAuthInvocation);
336336
}
337337

338-
private isInheritedFromDelegate(field: DataField, contextModel: DataModel) {
339-
return field.$container !== contextModel && ModelUtils.isDelegateModel(field.$container);
340-
}
341-
342338
private makeFieldAttribute(attr: DataFieldAttribute) {
343339
const attrName = attr.decl.ref!.name;
344340
return new PrismaFieldAttribute(

packages/sdk/src/ts-schema-generator.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { ModelUtils } from '.';
4545
import {
4646
getAttribute,
4747
getAuthDecl,
48+
getDelegateOriginModel,
4849
getIdFields,
4950
hasAttribute,
5051
isDelegateModel,
@@ -587,17 +588,14 @@ export class TsSchemaGenerator {
587588
if (
588589
contextModel &&
589590
// id fields are duplicated in inherited models
590-
!isIdField(field, contextModel) &&
591-
field.$container !== contextModel &&
592-
isDelegateModel(field.$container)
591+
!isIdField(field, contextModel)
593592
) {
594-
// field is inherited from delegate
595-
objectFields.push(
596-
ts.factory.createPropertyAssignment(
597-
'originModel',
598-
ts.factory.createStringLiteral(field.$container.name),
599-
),
600-
);
593+
const delegateOrigin = getDelegateOriginModel(field, contextModel);
594+
if (delegateOrigin) {
595+
objectFields.push(
596+
ts.factory.createPropertyAssignment('originModel', ts.factory.createStringLiteral(delegateOrigin)),
597+
);
598+
}
601599
}
602600

603601
// discriminator

packages/testtools/src/client.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@ export async function createTestClient(
238238
execSync('npx prisma db push --schema ./schema.prisma --skip-generate --force-reset', {
239239
cwd: workDir,
240240
stdio: options.debug ? 'inherit' : 'ignore',
241+
env: {
242+
...process.env,
243+
PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: 'true',
244+
},
241245
});
242246
} else {
243247
await prepareDatabase(provider, dbName);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { createPolicyTestClient } from '@zenstackhq/testtools';
2+
import { describe, expect, it } from 'vitest';
3+
4+
// https://github.com/zenstackhq/zenstack/issues/2351
5+
describe('Regression for issue 2351', () => {
6+
it('should correctly query delegate model that inherits from a model using a mixin abstract type', async () => {
7+
const db = await createPolicyTestClient(
8+
`
9+
type BaseEntity {
10+
id String @id @default(cuid())
11+
createdOn DateTime @default(now())
12+
updatedOn DateTime @updatedAt
13+
isDeleted Boolean @default(false)
14+
isArchived Boolean @default(false)
15+
}
16+
17+
enum DataType {
18+
DataText
19+
DataNumber
20+
}
21+
22+
model RoutineData with BaseEntity {
23+
dataType DataType
24+
routineId String
25+
Routine Routine @relation(fields: [routineId], references: [id])
26+
@@delegate(dataType)
27+
@@allow('all', auth().id == Routine.userId)
28+
}
29+
30+
model Routine {
31+
id String @id @default(cuid())
32+
userId String
33+
User User @relation(fields: [userId], references: [id])
34+
data RoutineData[]
35+
@@allow('all', true)
36+
}
37+
38+
model User {
39+
id String @id @default(cuid())
40+
name String
41+
routines Routine[]
42+
@@allow('all', true)
43+
}
44+
45+
model DataText extends RoutineData {
46+
textValue String
47+
}
48+
`,
49+
{ usePrismaPush: true },
50+
);
51+
52+
const user = await db.user.create({
53+
data: {
54+
name: 'Test User',
55+
},
56+
});
57+
58+
const routine = await db.routine.create({
59+
data: {
60+
userId: user.id,
61+
},
62+
});
63+
64+
const authDb = db.$setAuth({ id: user.id });
65+
const created = await authDb.dataText.create({
66+
data: { textValue: 'hello', routineId: routine.id },
67+
});
68+
expect(created.textValue).toBe('hello');
69+
expect(created.isDeleted).toBe(false);
70+
expect(created.isArchived).toBe(false);
71+
72+
const found = await authDb.dataText.findUnique({
73+
where: { id: created.id },
74+
});
75+
expect(found).not.toBeNull();
76+
expect(found!.textValue).toBe('hello');
77+
expect(found!.createdOn).toBeDefined();
78+
});
79+
});

0 commit comments

Comments
 (0)