Skip to content

Commit dfa6402

Browse files
authored
fix: type with @@auth cannot be properly compiled with new "prisma-client" generator (#2306)
1 parent c23ff10 commit dfa6402

File tree

4 files changed

+163
-22
lines changed

4 files changed

+163
-22
lines changed

packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { getIdFields, isAuthInvocation, isDataModelFieldReference } from '@zenstackhq/sdk';
1+
import { getIdFields, getPrismaClientGenerator, isAuthInvocation, isDataModelFieldReference } from '@zenstackhq/sdk';
22
import {
33
DataModel,
44
DataModelField,
55
Expression,
66
isDataModel,
77
isMemberAccessExpr,
8+
isTypeDef,
89
TypeDef,
910
type Model,
1011
} from '@zenstackhq/sdk/ast';
@@ -19,27 +20,39 @@ export function generateAuthType(model: Model, authDecl: DataModel | TypeDef) {
1920
const types = new Map<
2021
string,
2122
{
23+
isTypeDef: boolean;
2224
// relation fields to require
2325
requiredRelations: { name: string; type: string }[];
2426
}
2527
>();
2628

27-
types.set(authDecl.name, { requiredRelations: [] });
29+
types.set(authDecl.name, { isTypeDef: isTypeDef(authDecl), requiredRelations: [] });
2830

29-
const ensureType = (model: string) => {
30-
if (!types.has(model)) {
31-
types.set(model, { requiredRelations: [] });
31+
const findType = (name: string) =>
32+
model.declarations.find((d) => (isDataModel(d) || isTypeDef(d)) && d.name === name);
33+
34+
const ensureType = (name: string) => {
35+
if (!types.has(name)) {
36+
const decl = findType(name);
37+
if (!decl) {
38+
return;
39+
}
40+
types.set(name, { isTypeDef: isTypeDef(decl), requiredRelations: [] });
3241
}
3342
};
3443

35-
const addAddField = (model: string, name: string, type: string, array: boolean) => {
36-
let fields = types.get(model);
37-
if (!fields) {
38-
fields = { requiredRelations: [] };
39-
types.set(model, fields);
44+
const addTypeField = (typeName: string, fieldName: string, fieldType: string, array: boolean) => {
45+
let typeInfo = types.get(typeName);
46+
if (!typeInfo) {
47+
const decl = findType(typeName);
48+
if (!decl) {
49+
return;
50+
}
51+
typeInfo = { isTypeDef: isTypeDef(decl), requiredRelations: [] };
52+
types.set(typeName, typeInfo);
4053
}
41-
if (!fields.requiredRelations.find((f) => f.name === name)) {
42-
fields.requiredRelations.push({ name, type: array ? `${type}[]` : type });
54+
if (!typeInfo.requiredRelations.find((f) => f.name === fieldName)) {
55+
typeInfo.requiredRelations.push({ name: fieldName, type: array ? `${fieldType}[]` : fieldType });
4356
}
4457
};
4558

@@ -57,7 +70,7 @@ export function generateAuthType(model: Model, authDecl: DataModel | TypeDef) {
5770
// member is a relation
5871
const fieldType = memberDecl.type.reference.ref.name;
5972
ensureType(fieldType);
60-
addAddField(exprType.name, memberDecl.name, fieldType, memberDecl.type.array);
73+
addTypeField(exprType.name, memberDecl.name, fieldType, memberDecl.type.array);
6174
}
6275
}
6376
}
@@ -69,12 +82,15 @@ export function generateAuthType(model: Model, authDecl: DataModel | TypeDef) {
6982
if (isDataModel(fieldType)) {
7083
// field is a relation
7184
ensureType(fieldType.name);
72-
addAddField(fieldDecl.$container.name, node.target.$refText, fieldType.name, fieldDecl.type.array);
85+
addTypeField(fieldDecl.$container.name, node.target.$refText, fieldType.name, fieldDecl.type.array);
7386
}
7487
}
7588
});
7689
});
7790

91+
const prismaGenerator = getPrismaClientGenerator(model);
92+
const isNewGenerator = !!prismaGenerator?.isNewGenerator;
93+
7894
// generate:
7995
// `
8096
// namespace auth {
@@ -86,25 +102,27 @@ export function generateAuthType(model: Model, authDecl: DataModel | TypeDef) {
86102
return `export namespace auth {
87103
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
88104
${Array.from(types.entries())
89-
.map(([model, fields]) => {
90-
let result = `Partial<_P.${model}>`;
105+
.map(([type, typeInfo]) => {
106+
// TypeDef types are generated in "json-types.ts" for the new "prisma-client" generator
107+
const typeRef = isNewGenerator && typeInfo.isTypeDef ? `$TypeDefs.${type}` : `_P.${type}`;
108+
let result = `Partial<${typeRef}>`;
91109
92-
if (model === authDecl.name) {
110+
if (type === authDecl.name) {
93111
// auth model's id fields are always required
94112
const idFields = getIdFields(authDecl).map((f) => f.name);
95113
if (idFields.length > 0) {
96114
result = `WithRequired<${result}, ${idFields.map((f) => `'${f}'`).join('|')}>`;
97115
}
98116
}
99117
100-
if (fields.requiredRelations.length > 0) {
118+
if (typeInfo.requiredRelations.length > 0) {
101119
// merge required relation fields
102-
result = `${result} & { ${fields.requiredRelations.map((f) => `${f.name}: ${f.type}`).join('; ')} }`;
120+
result = `${result} & { ${typeInfo.requiredRelations.map((f) => `${f.name}: ${f.type}`).join('; ')} }`;
103121
}
104122
105123
result = `${result} & Record<string, unknown>`;
106124
107-
return ` export type ${model} = ${result};`;
125+
return ` export type ${type} = ${result};`;
108126
})
109127
.join('\n')}
110128
}`;

packages/schema/src/plugins/enhancer/enhance/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,13 @@ export function enhance<DbClient extends object>(prisma: DbClient, context?: Enh
322322
: // old generator has these types generated with the client
323323
`${prismaImport}/runtime/library`;
324324

325+
const hasTypeDef = this.model.declarations.some(isTypeDef);
326+
325327
return `import { Prisma as _Prisma, PrismaClient as _PrismaClient } from '${prismaTargetImport}';
326328
import type { InternalArgs, DynamicClientExtensionThis } from '${runtimeLibraryImport}';
327-
import type * as _P from '${prismaClientImport}';
329+
import type * as _P from '${prismaClientImport}';${
330+
hasTypeDef && this.isNewPrismaClientGenerator ? `\nimport type * as $TypeDefs from './json-types';` : ''
331+
}
328332
import type { Prisma, PrismaClient } from '${prismaClientImport}';
329333
export type { PrismaClient };
330334
`;

tests/integration/tests/enhancements/with-policy/auth.test.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,7 @@ describe('auth() compile-time test', () => {
865865
);
866866
});
867867

868-
it('"User" type as auth', async () => {
868+
it('"User" type as auth legacy generator', async () => {
869869
const { enhance } = await loadSchema(
870870
`
871871
type Profile {
@@ -920,4 +920,75 @@ describe('auth() compile-time test', () => {
920920
})
921921
).toResolveTruthy();
922922
});
923+
924+
it('"User" type as auth new generator', async () => {
925+
const { enhance } = await loadSchema(
926+
`
927+
datasource db {
928+
provider = "sqlite"
929+
url = "file:./dev.db"
930+
}
931+
932+
generator js {
933+
provider = "prisma-client"
934+
output = "./generated/client"
935+
moduleFormat = "cjs"
936+
}
937+
938+
type Profile {
939+
age Int
940+
}
941+
942+
type Role {
943+
name String
944+
permissions String[]
945+
}
946+
947+
type User {
948+
myId Int @id
949+
banned Boolean
950+
profile Profile
951+
roles Role[]
952+
}
953+
954+
model Foo {
955+
id Int @id @default(autoincrement())
956+
@@allow('read', true)
957+
@@allow('create', auth().myId == 1 && !auth().banned)
958+
@@allow('delete', auth().roles?['DELETE' in permissions])
959+
@@deny('all', auth().profile.age < 18)
960+
}
961+
`,
962+
{
963+
addPrelude: false,
964+
output: './zenstack',
965+
preserveTsFiles: true,
966+
prismaLoadPath: './prisma/generated/client/client',
967+
compile: true,
968+
extraSourceFiles: [
969+
{
970+
name: 'main.ts',
971+
content: `
972+
import { enhance } from "./zenstack/enhance";
973+
import { PrismaClient } from '@prisma/client';
974+
enhance(new PrismaClient(), { user: { myId: 1, profile: { age: 20 } } });
975+
`,
976+
},
977+
],
978+
}
979+
);
980+
981+
await expect(enhance().foo.create({ data: {} })).toBeRejectedByPolicy();
982+
await expect(enhance({ myId: 1, banned: true }).foo.create({ data: {} })).toBeRejectedByPolicy();
983+
await expect(enhance({ myId: 1, profile: { age: 16 } }).foo.create({ data: {} })).toBeRejectedByPolicy();
984+
const r = await enhance({ myId: 1, profile: { age: 20 } }).foo.create({ data: {} });
985+
await expect(
986+
enhance({ myId: 1, profile: { age: 20 } }).foo.delete({ where: { id: r.id } })
987+
).toBeRejectedByPolicy();
988+
await expect(
989+
enhance({ myId: 1, profile: { age: 20 }, roles: [{ name: 'ADMIN', permissions: ['DELETE'] }] }).foo.delete({
990+
where: { id: r.id },
991+
})
992+
).toResolveTruthy();
993+
});
923994
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
3+
describe('Issue 2294', () => {
4+
it('should work', async () => {
5+
await loadSchema(
6+
`
7+
datasource db {
8+
provider = "sqlite"
9+
url = "file:./dev.db"
10+
}
11+
12+
generator js {
13+
provider = "prisma-client"
14+
output = "./generated/client"
15+
moduleFormat = "cjs"
16+
}
17+
18+
type AuthUser {
19+
id Int @id
20+
name String?
21+
22+
@@auth
23+
}
24+
25+
model Foo {
26+
id Int @id
27+
@@allow('all', auth().name == 'admin')
28+
}
29+
`,
30+
{
31+
addPrelude: false,
32+
output: './zenstack',
33+
prismaLoadPath: './prisma/generated/client/client',
34+
compile: true,
35+
extraSourceFiles: [
36+
{
37+
name: 'main.ts',
38+
content: `
39+
import { enhance } from "./zenstack/enhance";
40+
import { PrismaClient } from './prisma/generated/client/client';
41+
enhance(new PrismaClient(), { user: { id: 1, name: 'admin' } });
42+
`,
43+
},
44+
],
45+
}
46+
);
47+
});
48+
});

0 commit comments

Comments
 (0)