Skip to content

Commit

Permalink
feat: support self relations (zenstackhq#244)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 authored Mar 7, 2023
1 parent 7e55e65 commit 93cb6bf
Show file tree
Hide file tree
Showing 14 changed files with 349 additions and 23 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zenstack-monorepo",
"version": "1.0.0-alpha.57",
"version": "1.0.0-alpha.58",
"description": "",
"scripts": {
"build": "pnpm -r build",
Expand Down
2 changes: 1 addition & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/language",
"version": "1.0.0-alpha.57",
"version": "1.0.0-alpha.58",
"displayName": "ZenStack modeling language compiler",
"description": "ZenStack modeling language compiler",
"homepage": "https://zenstack.dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/next",
"version": "1.0.0-alpha.57",
"version": "1.0.0-alpha.58",
"displayName": "ZenStack Next.js integration",
"description": "ZenStack Next.js integration",
"homepage": "https://zenstack.dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/react/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/react",
"displayName": "ZenStack plugin and runtime for ReactJS",
"version": "1.0.0-alpha.57",
"version": "1.0.0-alpha.58",
"description": "ZenStack plugin and runtime for ReactJS",
"main": "index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/trpc/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/trpc",
"displayName": "ZenStack plugin for tRPC",
"version": "1.0.0-alpha.57",
"version": "1.0.0-alpha.58",
"description": "ZenStack plugin for tRPC",
"main": "index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/runtime",
"displayName": "ZenStack Runtime Library",
"version": "1.0.0-alpha.57",
"version": "1.0.0-alpha.58",
"description": "Runtime of ZenStack for both client-side and server-side environments.",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"publisher": "zenstack",
"displayName": "ZenStack Language Tools",
"description": "A toolkit for building secure CRUD apps with Next.js + Typescript",
"version": "1.0.0-alpha.57",
"version": "1.0.0-alpha.58",
"author": {
"name": "ZenStack Team"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,13 @@ export default class DataModelValidator implements AstValidator<DataModel> {
}

if (!fields || !references) {
if (accept) {
accept('error', `Both "fields" and "references" must be provided`, { node: relAttr });
if (this.isSelfRelation(field, name)) {
// self relations are partial
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations
} else {
if (accept) {
accept('error', `Both "fields" and "references" must be provided`, { node: relAttr });
}
}
} else {
// validate "fields" and "references" typing consistency
Expand Down Expand Up @@ -157,6 +162,33 @@ export default class DataModelValidator implements AstValidator<DataModel> {
return { attr: relAttr, name, fields, references, valid };
}

private isSelfRelation(field: DataModelField, relationName?: string) {
if (field.type.reference?.ref === field.$container) {
// field directly references back to its type
return true;
}

if (relationName) {
// field's relation points to another type, and that type's opposite relation field
// points back
const oppositeModelFields = field.type.reference?.ref?.fields as DataModelField[];
if (oppositeModelFields) {
for (const oppositeField of oppositeModelFields) {
const { name: oppositeRelationName } = this.parseRelation(oppositeField);
if (
oppositeRelationName === relationName &&
oppositeField.type.reference?.ref === field.$container
) {
// found an opposite relation field that points back to this field's type
return true;
}
}
}
}

return false;
}

private validateRelationField(field: DataModelField, accept: ValidationAcceptor) {
const thisRelation = this.parseRelation(field, accept);
if (!thisRelation.valid) {
Expand All @@ -180,15 +212,20 @@ export default class DataModelValidator implements AstValidator<DataModel> {
);
return;
} else if (oppositeFields.length > 1) {
oppositeFields.forEach((f) =>
accept(
'error',
`Fields ${oppositeFields.map((f) => '"' + f.name + '"').join(', ')} on model "${
oppositeModel.name
}" refer to the same relation to model "${field.$container.name}"`,
{ node: f }
)
);
oppositeFields.forEach((f) => {
if (this.isSelfRelation(f)) {
// self relations are partial
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations
} else {
accept(
'error',
`Fields ${oppositeFields.map((f) => '"' + f.name + '"').join(', ')} on model "${
oppositeModel.name
}" refer to the same relation to model "${field.$container.name}"`,
{ node: f }
);
}
});
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,4 +449,80 @@ describe('Data Model Validation Tests', () => {
}
`);
});

it('self relation', async () => {
// one-to-one
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#one-to-one-self-relations
await loadModel(`
${prelude}
model User {
id Int @id @default(autoincrement())
name String?
successorId Int? @unique
successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id])
predecessor User? @relation("BlogOwnerHistory")
}
`);

// one-to-many
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#one-to-many-self-relations
await loadModel(`
${prelude}
model User {
id Int @id @default(autoincrement())
name String?
teacherId Int?
teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])
students User[] @relation("TeacherStudents")
}
`);

// many-to-many
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#many-to-many-self-relations
await loadModel(`
${prelude}
model User {
id Int @id @default(autoincrement())
name String?
followedBy User[] @relation("UserFollows")
following User[] @relation("UserFollows")
}
`);

// many-to-many explicit
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#many-to-many-self-relations
await loadModel(`
${prelude}
model User {
id Int @id @default(autoincrement())
name String?
followedBy Follows[] @relation("following")
following Follows[] @relation("follower")
}
model Follows {
follower User @relation("follower", fields: [followerId], references: [id])
followerId Int
following User @relation("following", fields: [followingId], references: [id])
followingId Int
@@id([followerId, followingId])
}
`);

// multiple self relations
// https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#defining-multiple-self-relations-on-the-same-model
await loadModel(`
${prelude}
model User {
id Int @id @default(autoincrement())
name String?
teacherId Int?
teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])
students User[] @relation("TeacherStudents")
followedBy User[] @relation("UserFollows")
following User[] @relation("UserFollows")
}
`);
});
});
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/sdk",
"version": "1.0.0-alpha.57",
"version": "1.0.0-alpha.58",
"description": "ZenStack plugin development SDK",
"main": "index.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/testtools/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/testtools",
"version": "1.0.0-alpha.57",
"version": "1.0.0-alpha.58",
"description": "ZenStack Test Tools",
"main": "index.js",
"publishConfig": {
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/test-run/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import path from 'path';

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

beforeAll(async () => {
origDir = path.resolve('.');
Expand Down
Loading

0 comments on commit 93cb6bf

Please sign in to comment.