Skip to content

Commit 93cb6bf

Browse files
authored
feat: support self relations (#244)
1 parent 7e55e65 commit 93cb6bf

File tree

14 files changed

+349
-23
lines changed

14 files changed

+349
-23
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-monorepo",
3-
"version": "1.0.0-alpha.57",
3+
"version": "1.0.0-alpha.58",
44
"description": "",
55
"scripts": {
66
"build": "pnpm -r build",

packages/language/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/language",
3-
"version": "1.0.0-alpha.57",
3+
"version": "1.0.0-alpha.58",
44
"displayName": "ZenStack modeling language compiler",
55
"description": "ZenStack modeling language compiler",
66
"homepage": "https://zenstack.dev",

packages/next/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/next",
3-
"version": "1.0.0-alpha.57",
3+
"version": "1.0.0-alpha.58",
44
"displayName": "ZenStack Next.js integration",
55
"description": "ZenStack Next.js integration",
66
"homepage": "https://zenstack.dev",

packages/plugins/react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/react",
33
"displayName": "ZenStack plugin and runtime for ReactJS",
4-
"version": "1.0.0-alpha.57",
4+
"version": "1.0.0-alpha.58",
55
"description": "ZenStack plugin and runtime for ReactJS",
66
"main": "index.js",
77
"repository": {

packages/plugins/trpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/trpc",
33
"displayName": "ZenStack plugin for tRPC",
4-
"version": "1.0.0-alpha.57",
4+
"version": "1.0.0-alpha.58",
55
"description": "ZenStack plugin for tRPC",
66
"main": "index.js",
77
"repository": {

packages/runtime/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/runtime",
33
"displayName": "ZenStack Runtime Library",
4-
"version": "1.0.0-alpha.57",
4+
"version": "1.0.0-alpha.58",
55
"description": "Runtime of ZenStack for both client-side and server-side environments.",
66
"repository": {
77
"type": "git",

packages/schema/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publisher": "zenstack",
44
"displayName": "ZenStack Language Tools",
55
"description": "A toolkit for building secure CRUD apps with Next.js + Typescript",
6-
"version": "1.0.0-alpha.57",
6+
"version": "1.0.0-alpha.58",
77
"author": {
88
"name": "ZenStack Team"
99
},

packages/schema/src/language-server/validator/datamodel-validator.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,13 @@ export default class DataModelValidator implements AstValidator<DataModel> {
118118
}
119119

120120
if (!fields || !references) {
121-
if (accept) {
122-
accept('error', `Both "fields" and "references" must be provided`, { node: relAttr });
121+
if (this.isSelfRelation(field, name)) {
122+
// self relations are partial
123+
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations
124+
} else {
125+
if (accept) {
126+
accept('error', `Both "fields" and "references" must be provided`, { node: relAttr });
127+
}
123128
}
124129
} else {
125130
// validate "fields" and "references" typing consistency
@@ -157,6 +162,33 @@ export default class DataModelValidator implements AstValidator<DataModel> {
157162
return { attr: relAttr, name, fields, references, valid };
158163
}
159164

165+
private isSelfRelation(field: DataModelField, relationName?: string) {
166+
if (field.type.reference?.ref === field.$container) {
167+
// field directly references back to its type
168+
return true;
169+
}
170+
171+
if (relationName) {
172+
// field's relation points to another type, and that type's opposite relation field
173+
// points back
174+
const oppositeModelFields = field.type.reference?.ref?.fields as DataModelField[];
175+
if (oppositeModelFields) {
176+
for (const oppositeField of oppositeModelFields) {
177+
const { name: oppositeRelationName } = this.parseRelation(oppositeField);
178+
if (
179+
oppositeRelationName === relationName &&
180+
oppositeField.type.reference?.ref === field.$container
181+
) {
182+
// found an opposite relation field that points back to this field's type
183+
return true;
184+
}
185+
}
186+
}
187+
}
188+
189+
return false;
190+
}
191+
160192
private validateRelationField(field: DataModelField, accept: ValidationAcceptor) {
161193
const thisRelation = this.parseRelation(field, accept);
162194
if (!thisRelation.valid) {
@@ -180,15 +212,20 @@ export default class DataModelValidator implements AstValidator<DataModel> {
180212
);
181213
return;
182214
} else if (oppositeFields.length > 1) {
183-
oppositeFields.forEach((f) =>
184-
accept(
185-
'error',
186-
`Fields ${oppositeFields.map((f) => '"' + f.name + '"').join(', ')} on model "${
187-
oppositeModel.name
188-
}" refer to the same relation to model "${field.$container.name}"`,
189-
{ node: f }
190-
)
191-
);
215+
oppositeFields.forEach((f) => {
216+
if (this.isSelfRelation(f)) {
217+
// self relations are partial
218+
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations
219+
} else {
220+
accept(
221+
'error',
222+
`Fields ${oppositeFields.map((f) => '"' + f.name + '"').join(', ')} on model "${
223+
oppositeModel.name
224+
}" refer to the same relation to model "${field.$container.name}"`,
225+
{ node: f }
226+
);
227+
}
228+
});
192229
return;
193230
}
194231

packages/schema/tests/schema/validation/datamodel-validation.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,4 +449,80 @@ describe('Data Model Validation Tests', () => {
449449
}
450450
`);
451451
});
452+
453+
it('self relation', async () => {
454+
// one-to-one
455+
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#one-to-one-self-relations
456+
await loadModel(`
457+
${prelude}
458+
model User {
459+
id Int @id @default(autoincrement())
460+
name String?
461+
successorId Int? @unique
462+
successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id])
463+
predecessor User? @relation("BlogOwnerHistory")
464+
}
465+
`);
466+
467+
// one-to-many
468+
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#one-to-many-self-relations
469+
await loadModel(`
470+
${prelude}
471+
model User {
472+
id Int @id @default(autoincrement())
473+
name String?
474+
teacherId Int?
475+
teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])
476+
students User[] @relation("TeacherStudents")
477+
}
478+
`);
479+
480+
// many-to-many
481+
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#many-to-many-self-relations
482+
await loadModel(`
483+
${prelude}
484+
model User {
485+
id Int @id @default(autoincrement())
486+
name String?
487+
followedBy User[] @relation("UserFollows")
488+
following User[] @relation("UserFollows")
489+
}
490+
`);
491+
492+
// many-to-many explicit
493+
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#many-to-many-self-relations
494+
await loadModel(`
495+
${prelude}
496+
model User {
497+
id Int @id @default(autoincrement())
498+
name String?
499+
followedBy Follows[] @relation("following")
500+
following Follows[] @relation("follower")
501+
}
502+
503+
model Follows {
504+
follower User @relation("follower", fields: [followerId], references: [id])
505+
followerId Int
506+
following User @relation("following", fields: [followingId], references: [id])
507+
followingId Int
508+
509+
@@id([followerId, followingId])
510+
}
511+
`);
512+
513+
// multiple self relations
514+
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#defining-multiple-self-relations-on-the-same-model
515+
await loadModel(`
516+
${prelude}
517+
model User {
518+
id Int @id @default(autoincrement())
519+
name String?
520+
teacherId Int?
521+
teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])
522+
students User[] @relation("TeacherStudents")
523+
followedBy User[] @relation("UserFollows")
524+
following User[] @relation("UserFollows")
525+
}
526+
`);
527+
});
452528
});

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/sdk",
3-
"version": "1.0.0-alpha.57",
3+
"version": "1.0.0-alpha.58",
44
"description": "ZenStack plugin development SDK",
55
"main": "index.js",
66
"scripts": {

packages/testtools/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/testtools",
3-
"version": "1.0.0-alpha.57",
3+
"version": "1.0.0-alpha.58",
44
"description": "ZenStack Test Tools",
55
"main": "index.js",
66
"publishConfig": {

tests/integration/test-run/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/tests/with-policy/relation-to-one-filter.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import path from 'path';
33

44
describe('With Policy: relation to-one filter', () => {
55
let origDir: string;
6-
const suite = 'relation-to-one-filter';
76

87
beforeAll(async () => {
98
origDir = path.resolve('.');

0 commit comments

Comments
 (0)