Skip to content

Commit 8d9f296

Browse files
authored
fix: relation include orderBy validation issue, more test migrations (#297)
1 parent 332b1db commit 8d9f296

21 files changed

+841
-5
lines changed

packages/language/src/utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,11 @@ export function getRecursiveBases(
166166
return result;
167167
}
168168
seen.add(decl);
169-
decl.mixins.forEach((mixin) => {
170-
// avoid using mixin.ref since this function can be called before linking
169+
const bases = [...decl.mixins, ...(isDataModel(decl) && decl.baseModel ? [decl.baseModel] : [])];
170+
bases.forEach((base) => {
171+
// avoid using .ref since this function can be called before linking
171172
const baseDecl = decl.$container.declarations.find(
172-
(d): d is TypeDef => isTypeDef(d) && d.name === mixin.$refText,
173+
(d): d is TypeDef | DataModel => isTypeDef(d) || (isDataModel(d) && d.name === base.$refText),
173174
);
174175
if (baseDecl) {
175176
if (!includeDelegate && isDelegateModel(baseDecl)) {

packages/runtime/src/client/crud/validator/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,9 @@ export class InputValidator<Schema extends SchemaDef> {
677677
...(fieldDef.array
678678
? {
679679
// to-many relations can be ordered, skipped, taken, and cursor-located
680-
orderBy: z.lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)).optional(),
680+
orderBy: z
681+
.lazy(() => this.orArray(this.makeOrderBySchema(fieldDef.type, true, false), true))
682+
.optional(),
681683
skip: this.makeSkipSchema().optional(),
682684
take: this.makeTakeSchema().optional(),
683685
cursor: this.makeCursorSchema(fieldDef.type).optional(),

packages/testtools/src/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export async function generateTsSchema(
5454

5555
if (extraSourceFiles) {
5656
for (const [fileName, content] of Object.entries(extraSourceFiles)) {
57-
const filePath = path.resolve(workDir, `${fileName}.ts`);
57+
const filePath = path.resolve(workDir, !fileName.endsWith('.ts') ? `${fileName}.ts` : fileName);
5858
fs.mkdirSync(path.dirname(filePath), { recursive: true });
5959
fs.writeFileSync(filePath, content);
6060
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
import { describe, it } from 'vitest';
3+
4+
// TODO: multi-schema support
5+
describe.skip('Regression for issue 1647', () => {
6+
it('inherits @@schema by default', async () => {
7+
await loadSchema(
8+
`
9+
model Asset {
10+
id Int @id
11+
type String
12+
@@delegate(type)
13+
@@schema('public')
14+
}
15+
16+
model Post extends Asset {
17+
title String
18+
}
19+
`,
20+
);
21+
});
22+
23+
it('respects sub model @@schema overrides', async () => {
24+
await loadSchema(
25+
`
26+
datasource db {
27+
provider = 'postgresql'
28+
url = env('DATABASE_URL')
29+
schemas = ['public', 'post']
30+
}
31+
32+
generator client {
33+
provider = 'prisma-client-js'
34+
previewFeatures = ['multiSchema']
35+
}
36+
37+
model Asset {
38+
id Int @id
39+
type String
40+
@@delegate(type)
41+
@@schema('public')
42+
}
43+
44+
model Post extends Asset {
45+
title String
46+
@@schema('post')
47+
}
48+
`,
49+
);
50+
});
51+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { createPolicyTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
it('verifies issue 1648', async () => {
5+
const db = await createPolicyTestClient(
6+
`
7+
model User {
8+
id Int @id @default(autoincrement())
9+
profile Profile?
10+
posts Post[]
11+
}
12+
13+
model Profile {
14+
id Int @id @default(autoincrement())
15+
someText String
16+
user User @relation(fields: [userId], references: [id])
17+
userId Int @unique
18+
}
19+
20+
model Post {
21+
id Int @id @default(autoincrement())
22+
title String
23+
24+
userId Int
25+
user User @relation(fields: [userId], references: [id])
26+
27+
// this will always be true, even if the someText field is "canUpdate"
28+
@@deny("post-update", user.profile.someText != "canUpdate")
29+
30+
@@allow("all", true)
31+
}
32+
`,
33+
);
34+
35+
await db.$unuseAll().user.create({ data: { id: 1, profile: { create: { someText: 'canUpdate' } } } });
36+
await db.$unuseAll().user.create({ data: { id: 2, profile: { create: { someText: 'nothing' } } } });
37+
await db.$unuseAll().post.create({ data: { id: 1, title: 'Post1', userId: 1 } });
38+
await db.$unuseAll().post.create({ data: { id: 2, title: 'Post2', userId: 2 } });
39+
40+
await expect(db.post.update({ where: { id: 1 }, data: { title: 'Post1-1' } })).toResolveTruthy();
41+
await expect(db.post.update({ where: { id: 2 }, data: { title: 'Post2-2' } })).toBeRejectedByPolicy();
42+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { createPolicyTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
it('verifies issue 1674', async () => {
5+
const db = await createPolicyTestClient(
6+
`
7+
model User {
8+
id String @id @default(cuid())
9+
email String @unique @email @length(6, 32)
10+
posts Post[]
11+
12+
// everybody can signup
13+
@@allow('create', true)
14+
15+
// full access by self
16+
@@allow('all', auth() == this)
17+
}
18+
19+
model Blog {
20+
id String @id @default(cuid())
21+
createdAt DateTime @default(now())
22+
updatedAt DateTime @updatedAt
23+
24+
post Post? @relation(fields: [postId], references: [id], onDelete: Cascade)
25+
postId String?
26+
}
27+
28+
model Post {
29+
id String @id @default(cuid())
30+
createdAt DateTime @default(now())
31+
updatedAt DateTime @updatedAt
32+
title String @length(1, 256)
33+
content String
34+
published Boolean @default(false)
35+
author User @relation(fields: [authorId], references: [id])
36+
authorId String
37+
38+
blogs Blog[]
39+
40+
type String
41+
42+
@@delegate(type)
43+
}
44+
45+
model PostA extends Post {
46+
}
47+
48+
model PostB extends Post {
49+
}
50+
`,
51+
);
52+
53+
const user = await db.$unuseAll().user.create({
54+
data: { email: '[email protected]' },
55+
});
56+
57+
const blog = await db.$unuseAll().blog.create({
58+
data: {},
59+
});
60+
61+
const authDb = db.$setAuth(user);
62+
await expect(
63+
authDb.postA.create({
64+
data: {
65+
content: 'content',
66+
title: 'title',
67+
blogs: {
68+
connect: {
69+
id: blog.id,
70+
},
71+
},
72+
author: {
73+
connect: {
74+
id: user.id,
75+
},
76+
},
77+
},
78+
}),
79+
).toBeRejectedByPolicy();
80+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
it('verifies issue 1681', async () => {
5+
const db = await createTestClient(
6+
`
7+
model User {
8+
id Int @id @default(autoincrement())
9+
posts Post[]
10+
@@allow('all', true)
11+
}
12+
13+
model Post {
14+
id Int @id @default(autoincrement())
15+
title String
16+
author User @relation(fields: [authorId], references: [id])
17+
authorId Int @default(auth().id)
18+
@@allow('all', true)
19+
}
20+
`,
21+
);
22+
23+
const authDb = db.$setAuth({ id: 1 });
24+
const user = await db.user.create({ data: {} });
25+
await expect(authDb.post.createMany({ data: [{ title: 'Post1' }] })).resolves.toMatchObject({ count: 1 });
26+
27+
const r = await authDb.post.createManyAndReturn({ data: [{ title: 'Post2' }] });
28+
expect(r[0].authorId).toBe(user.id);
29+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
import { it } from 'vitest';
3+
4+
it('verifies issue 1693', async () => {
5+
await loadSchema(
6+
`
7+
model Animal {
8+
id String @id @default(uuid())
9+
animalType String @default("")
10+
@@delegate(animalType)
11+
}
12+
13+
model Dog extends Animal {
14+
name String
15+
}
16+
`,
17+
);
18+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
import { it } from 'vitest';
3+
4+
it('verifies issue 1695', async () => {
5+
await loadSchema(
6+
`
7+
type SoftDelete {
8+
deleted Int @default(0)
9+
}
10+
11+
model MyModel with SoftDelete {
12+
id String @id @default(cuid())
13+
name String
14+
15+
@@deny('update', deleted != 0)
16+
@@deny('post-update', deleted != 0)
17+
@@deny('read', this.deleted != 0)
18+
}
19+
`,
20+
);
21+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
it('verifies issue 1698', async () => {
5+
const db = await createTestClient(
6+
`
7+
model House {
8+
id Int @id @default(autoincrement())
9+
doorTypeId Int
10+
door Door @relation(fields: [doorTypeId], references: [id])
11+
houseType String
12+
@@delegate(houseType)
13+
}
14+
15+
model PrivateHouse extends House {
16+
size Int
17+
}
18+
19+
model Skyscraper extends House {
20+
height Int
21+
}
22+
23+
model Door {
24+
id Int @id @default(autoincrement())
25+
color String
26+
doorType String
27+
houses House[]
28+
@@delegate(doorType)
29+
}
30+
31+
model IronDoor extends Door {
32+
strength Int
33+
}
34+
35+
model WoodenDoor extends Door {
36+
texture String
37+
}
38+
`,
39+
);
40+
41+
const door1 = await db.ironDoor.create({
42+
data: { strength: 100, color: 'blue' },
43+
});
44+
console.log(door1);
45+
46+
const door2 = await db.woodenDoor.create({
47+
data: { texture: 'pine', color: 'red' },
48+
});
49+
console.log(door2);
50+
51+
const house1 = await db.privateHouse.create({
52+
data: { size: 5000, door: { connect: { id: door1.id } } },
53+
});
54+
console.log(house1);
55+
56+
const house2 = await db.skyscraper.create({
57+
data: { height: 3000, door: { connect: { id: door2.id } } },
58+
});
59+
console.log(house2);
60+
61+
const r1 = await db.privateHouse.findFirst({ include: { door: true } });
62+
console.log(r1);
63+
expect(r1).toMatchObject({
64+
door: { color: 'blue', strength: 100 },
65+
});
66+
67+
const r2 = (await db.skyscraper.findMany({ include: { door: true } }))[0];
68+
console.log(r2);
69+
expect(r2).toMatchObject({
70+
door: { color: 'red', texture: 'pine' },
71+
});
72+
});

0 commit comments

Comments
 (0)