Skip to content

Commit 774f3c4

Browse files
authored
fix: stricter type def validation and compound unique field fix (#223)
1 parent 1d1f2c9 commit 774f3c4

File tree

7 files changed

+309
-9
lines changed

7 files changed

+309
-9
lines changed

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [x] init
88
- [x] validate
99
- [ ] format
10+
- [ ] repl
1011
- [x] plugin mechanism
1112
- [x] built-in plugins
1213
- [x] typescript
@@ -82,7 +83,6 @@
8283
- [x] Error system
8384
- [x] Custom table name
8485
- [x] Custom field name
85-
- [ ] Strict undefined checks
8686
- [ ] DbNull vs JsonNull
8787
- [ ] Migrate to tsdown
8888
- [ ] Benchmark

packages/language/src/validators/typedef-validator.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ValidationAcceptor } from 'langium';
2-
import type { DataField, TypeDef } from '../generated/ast';
2+
import { isDataModel, type DataField, type TypeDef } from '../generated/ast';
33
import { validateAttributeApplication } from './attribute-application-validator';
44
import { validateDuplicatedDeclarations, type AstValidator } from './common';
55

@@ -22,6 +22,11 @@ export default class TypeDefValidator implements AstValidator<TypeDef> {
2222
}
2323

2424
private validateField(field: DataField, accept: ValidationAcceptor): void {
25+
if (isDataModel(field.type.reference?.ref)) {
26+
accept('error', 'Type field cannot be a relation', {
27+
node: field.type,
28+
});
29+
}
2530
field.attributes.forEach((attr) => validateAttributeApplication(attr, accept));
2631
}
2732
}

packages/language/test/mixin.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,19 @@ describe('Mixin Tests', () => {
106106
'can only be applied once',
107107
);
108108
});
109+
110+
it('does not allow relation fields in type', async () => {
111+
await loadSchemaWithError(
112+
`
113+
model User {
114+
id Int @id @default(autoincrement())
115+
}
116+
117+
type T {
118+
u User
119+
}
120+
`,
121+
'Type field cannot be a relation',
122+
);
123+
});
109124
});

packages/runtime/test/policy/client-extensions.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe('client extensions tests for policies', () => {
2222
await rawDb.model.create({ data: { x: 2, y: 300 } });
2323

2424
const ext = definePlugin({
25-
id: 'prisma-extension-queryOverride',
25+
id: 'queryOverride',
2626
onQuery: async ({ args, proceed }: any) => {
2727
args = args ?? {};
2828
args.where = { ...args.where, y: { lt: 300 } };
@@ -53,7 +53,7 @@ describe('client extensions tests for policies', () => {
5353
await rawDb.model.create({ data: { x: 2, y: 300 } });
5454

5555
const ext = definePlugin({
56-
id: 'prisma-extension-queryOverride',
56+
id: 'queryOverride',
5757
onQuery: async ({ args, proceed }: any) => {
5858
args = args ?? {};
5959
args.where = { ...args.where, y: { lt: 300 } };
@@ -84,7 +84,7 @@ describe('client extensions tests for policies', () => {
8484
await rawDb.model.create({ data: { x: 2, y: 300 } });
8585

8686
const ext = definePlugin({
87-
id: 'prisma-extension-queryOverride',
87+
id: 'queryOverride',
8888
onQuery: async ({ args, proceed }: any) => {
8989
args = args ?? {};
9090
args.where = { ...args.where, y: { lt: 300 } };
@@ -115,7 +115,7 @@ describe('client extensions tests for policies', () => {
115115
await rawDb.model.create({ data: { x: 2, y: 300 } });
116116

117117
const ext = definePlugin({
118-
id: 'prisma-extension-queryOverride',
118+
id: 'queryOverride',
119119
onQuery: async ({ args, proceed }: any) => {
120120
args = args ?? {};
121121
args.where = { ...args.where, y: { lt: 300 } };
@@ -144,7 +144,7 @@ describe('client extensions tests for policies', () => {
144144
await rawDb.model.create({ data: { value: 1 } });
145145

146146
const ext = definePlugin({
147-
id: 'prisma-extension-resultMutation',
147+
id: 'resultMutation',
148148
onQuery: async ({ args, proceed }: any) => {
149149
const r: any = await proceed(args);
150150
for (let i = 0; i < r.length; i++) {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { createPolicyTestClient } from './utils';
3+
4+
describe('Abstract models', () => {
5+
it('connect test1', async () => {
6+
const db = await createPolicyTestClient(
7+
`
8+
model User {
9+
id Int @id @default(autoincrement())
10+
profile Profile? @relation(fields: [profileId], references: [id])
11+
profileId Int? @unique
12+
13+
@@allow('create,read', true)
14+
@@allow('update', auth().id == 1)
15+
}
16+
17+
type BaseProfile {
18+
id Int @id @default(autoincrement())
19+
20+
@@allow('all', true)
21+
}
22+
23+
model Profile with BaseProfile {
24+
name String
25+
user User?
26+
}
27+
`,
28+
);
29+
30+
const dbUser2 = db.$setAuth({ id: 2 });
31+
const user = await dbUser2.user.create({ data: { id: 1 } });
32+
const profile = await dbUser2.profile.create({ data: { id: 1, name: 'John' } });
33+
await expect(
34+
dbUser2.profile.update({ where: { id: 1 }, data: { user: { connect: { id: user.id } } } }),
35+
).toBeRejectedNotFound();
36+
await expect(
37+
dbUser2.user.update({ where: { id: 1 }, data: { profile: { connect: { id: profile.id } } } }),
38+
).toBeRejectedNotFound();
39+
40+
const dbUser1 = db.$setAuth({ id: 1 });
41+
await expect(
42+
dbUser1.profile.update({ where: { id: 1 }, data: { user: { connect: { id: user.id } } } }),
43+
).toResolveTruthy();
44+
await expect(
45+
dbUser1.user.update({ where: { id: 1 }, data: { profile: { connect: { id: profile.id } } } }),
46+
).toResolveTruthy();
47+
});
48+
49+
it('connect test2', async () => {
50+
const db = await createPolicyTestClient(
51+
`
52+
model User {
53+
id Int @id @default(autoincrement())
54+
profile Profile?
55+
56+
@@allow('all', true)
57+
}
58+
59+
type BaseProfile {
60+
id Int @id @default(autoincrement())
61+
62+
@@allow('create,read', true)
63+
@@allow('update', auth().id == 1)
64+
}
65+
66+
model Profile with BaseProfile {
67+
name String
68+
user User? @relation(fields: [userId], references: [id])
69+
userId Int? @unique
70+
}
71+
`,
72+
);
73+
74+
const dbUser2 = db.$setAuth({ id: 2 });
75+
const user = await dbUser2.user.create({ data: { id: 1 } });
76+
const profile = await dbUser2.profile.create({ data: { id: 1, name: 'John' } });
77+
await expect(
78+
dbUser2.profile.update({ where: { id: 1 }, data: { user: { connect: { id: user.id } } } }),
79+
).toBeRejectedNotFound();
80+
await expect(
81+
dbUser2.user.update({ where: { id: 1 }, data: { profile: { connect: { id: profile.id } } } }),
82+
).toBeRejectedNotFound();
83+
84+
const dbUser1 = db.$setAuth({ id: 1 });
85+
await expect(
86+
dbUser1.profile.update({ where: { id: 1 }, data: { user: { connect: { id: user.id } } } }),
87+
).toResolveTruthy();
88+
await expect(
89+
dbUser1.user.update({ where: { id: 1 }, data: { profile: { connect: { id: profile.id } } } }),
90+
).toResolveTruthy();
91+
});
92+
});
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import path from 'path';
2+
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
3+
import { createPolicyTestClient } from './utils';
4+
import { QueryError } from '../../src';
5+
6+
describe('With Policy: multi-field unique', () => {
7+
let origDir: string;
8+
9+
beforeAll(async () => {
10+
origDir = path.resolve('.');
11+
});
12+
13+
afterEach(() => {
14+
process.chdir(origDir);
15+
});
16+
17+
it('toplevel crud test unnamed constraint', async () => {
18+
const db = await createPolicyTestClient(
19+
`
20+
model Model {
21+
id String @id @default(uuid())
22+
a String
23+
b String
24+
x Int
25+
@@unique([a, b])
26+
27+
@@allow('all', x > 0)
28+
@@deny('update', x > 1)
29+
}
30+
`,
31+
);
32+
33+
await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 1 } })).toResolveTruthy();
34+
await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 2 } })).rejects.toThrow(QueryError);
35+
await expect(db.model.create({ data: { a: 'a2', b: 'b2', x: 0 } })).toBeRejectedByPolicy();
36+
37+
await expect(db.model.findUnique({ where: { a_b: { a: 'a1', b: 'b1' } } })).toResolveTruthy();
38+
await expect(db.model.findUnique({ where: { a_b: { a: 'a1', b: 'b2' } } })).toResolveFalsy();
39+
await expect(db.model.update({ where: { a_b: { a: 'a1', b: 'b1' } }, data: { x: 2 } })).toResolveTruthy();
40+
await expect(db.model.update({ where: { a_b: { a: 'a1', b: 'b1' } }, data: { x: 0 } })).toBeRejectedNotFound();
41+
42+
await expect(db.model.delete({ where: { a_b: { a: 'a1', b: 'b1' } } })).toResolveTruthy();
43+
});
44+
45+
it('toplevel crud test named constraint', async () => {
46+
const db = await createPolicyTestClient(
47+
`
48+
model Model {
49+
id String @id @default(uuid())
50+
a String
51+
b String
52+
x Int
53+
@@unique([a, b], name: 'myconstraint')
54+
55+
@@allow('all', x > 0)
56+
@@deny('update', x > 1)
57+
}
58+
`,
59+
);
60+
61+
await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 1 } })).toResolveTruthy();
62+
await expect(db.model.findUnique({ where: { myconstraint: { a: 'a1', b: 'b1' } } })).toResolveTruthy();
63+
await expect(db.model.findUnique({ where: { myconstraint: { a: 'a1', b: 'b2' } } })).toResolveFalsy();
64+
await expect(
65+
db.model.update({ where: { myconstraint: { a: 'a1', b: 'b1' } }, data: { x: 2 } }),
66+
).toResolveTruthy();
67+
await expect(
68+
db.model.update({ where: { myconstraint: { a: 'a1', b: 'b1' } }, data: { x: 0 } }),
69+
).toBeRejectedNotFound();
70+
await expect(db.model.delete({ where: { myconstraint: { a: 'a1', b: 'b1' } } })).toResolveTruthy();
71+
});
72+
73+
it('nested crud test', async () => {
74+
const db = await createPolicyTestClient(
75+
`
76+
model M1 {
77+
id String @id @default(uuid())
78+
m2 M2[]
79+
@@allow('all', true)
80+
}
81+
82+
model M2 {
83+
id String @id @default(uuid())
84+
a String
85+
b String
86+
x Int
87+
m1 M1 @relation(fields: [m1Id], references: [id])
88+
m1Id String
89+
90+
@@unique([a, b])
91+
@@allow('all', x > 0)
92+
}
93+
`,
94+
);
95+
96+
await expect(db.m1.create({ data: { id: '1', m2: { create: { a: 'a1', b: 'b1', x: 1 } } } })).toResolveTruthy();
97+
await expect(db.m1.create({ data: { id: '2', m2: { create: { a: 'a1', b: 'b1', x: 2 } } } })).rejects.toThrow(
98+
QueryError,
99+
);
100+
await expect(
101+
db.m1.create({ data: { id: '3', m2: { create: { a: 'a1', b: 'b2', x: 0 } } } }),
102+
).toBeRejectedByPolicy();
103+
104+
await expect(
105+
db.m1.update({
106+
where: { id: '1' },
107+
data: {
108+
m2: {
109+
connectOrCreate: {
110+
where: { a_b: { a: 'a1', b: 'b1' } },
111+
create: { a: 'a1', b: 'b1', x: 2 },
112+
},
113+
},
114+
},
115+
}),
116+
).toResolveTruthy();
117+
await expect(db.m2.count()).resolves.toBe(1);
118+
119+
await expect(
120+
db.m1.update({
121+
where: { id: '1' },
122+
data: {
123+
m2: {
124+
connectOrCreate: {
125+
where: { a_b: { a: 'a1', b: 'b2' } },
126+
create: { a: 'a1', b: 'b2', x: 2 },
127+
},
128+
},
129+
},
130+
}),
131+
).toResolveTruthy();
132+
await expect(db.m2.count()).resolves.toBe(2);
133+
134+
await expect(
135+
db.m1.update({
136+
where: { id: '1' },
137+
data: {
138+
m2: {
139+
connectOrCreate: {
140+
where: { a_b: { a: 'a2', b: 'b2' } },
141+
create: { a: 'a2', b: 'b2', x: 0 },
142+
},
143+
},
144+
},
145+
}),
146+
).toBeRejectedByPolicy();
147+
148+
await expect(
149+
db.m1.update({
150+
where: { id: '1' },
151+
data: {
152+
m2: {
153+
update: {
154+
where: { a_b: { a: 'a1', b: 'b2' } },
155+
data: { x: 3 },
156+
},
157+
},
158+
},
159+
}),
160+
).toResolveTruthy();
161+
await expect(db.m2.findUnique({ where: { a_b: { a: 'a1', b: 'b2' } } })).resolves.toEqual(
162+
expect.objectContaining({ x: 3 }),
163+
);
164+
165+
await expect(
166+
db.m1.update({
167+
where: { id: '1' },
168+
data: {
169+
m2: {
170+
delete: {
171+
a_b: { a: 'a1', b: 'b1' },
172+
},
173+
},
174+
},
175+
}),
176+
).toResolveTruthy();
177+
await expect(db.m2.count()).resolves.toBe(1);
178+
});
179+
});

0 commit comments

Comments
 (0)