diff --git a/package.json b/package.json index 8dd17b6e7..ab3b806f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.24", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index a16d6b364..a4ac83e33 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.24", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index ec1d9cb47..e1c6fc51d 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/openapi", "displayName": "ZenStack Plugin and Runtime for OpenAPI", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.24", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 1b99f4e1c..8ab4b6769 100644 --- a/packages/plugins/swr/package.json +++ b/packages/plugins/swr/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/swr", "displayName": "ZenStack plugin for generating SWR hooks", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.24", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 28d0f1fbb..ca6ff0eaf 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/tanstack-query", "displayName": "ZenStack plugin for generating tanstack-query hooks", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.24", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index a5c3220f3..d67e72f6f 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/trpc", "displayName": "ZenStack plugin for tRPC", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.24", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index d16660ec2..f1e389549 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.24", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 3b752d362..733ebf159 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -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-beta.23", + "version": "1.0.0-beta.24", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 281e3ad3b..655b54b77 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.24", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 627e9760a..914ddfaaa 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.24", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index e0b27f666..de26086e9 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.24", "description": "ZenStack Test Tools", "main": "index.js", "publishConfig": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5379c7236..83b27a82e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,7 +124,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) + version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -596,7 +596,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.3 - version: 29.0.3(@babel/core@7.22.5)(esbuild@0.15.12)(jest@29.5.0)(typescript@4.8.4) + version: 29.0.3(@babel/core@7.22.9)(esbuild@0.15.12)(jest@29.5.0)(typescript@4.8.4) ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@18.0.0)(typescript@4.8.4) @@ -10564,7 +10564,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.3(@babel/core@7.22.5)(esbuild@0.15.12)(jest@29.5.0)(typescript@4.8.4): + /ts-jest@29.0.3(@babel/core@7.22.9)(esbuild@0.15.12)(jest@29.5.0)(typescript@4.8.4): resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10585,7 +10585,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.22.5 + '@babel/core': 7.22.9 bs-logger: 0.2.6 esbuild: 0.15.12 fast-json-stable-stringify: 2.1.0 @@ -10599,7 +10599,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): + /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10620,7 +10620,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.22.9 + '@babel/core': 7.22.5 bs-logger: 0.2.6 esbuild: 0.18.13 fast-json-stable-stringify: 2.1.0 @@ -10630,11 +10630,11 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.4 + typescript: 4.9.5 yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10665,7 +10665,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.5 + typescript: 4.9.4 yargs-parser: 21.1.1 dev: true diff --git a/tests/integration/tests/regression/issue-704.test.ts b/tests/integration/tests/regression/issue-704.test.ts new file mode 100644 index 000000000..30a83d498 --- /dev/null +++ b/tests/integration/tests/regression/issue-704.test.ts @@ -0,0 +1,350 @@ +import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; +import { randomUUID } from 'crypto'; + +const model = ` +generator client { + provider = "prisma-client-js" + previewFeatures = ["multiSchema"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + schemas = ["userauth", "orgs", "courses", "logging"] +} + +abstract model Basic { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy User @relation(fields: [createdBy_id], references: [id], onDelete: Cascade) + createdBy_id String + updatedBy User? @relation(fields: [updatedBy_id], references: [id], onDelete: Cascade) + updatedBy_id String? + institute Institute @relation(fields: [institute_id], references: [id], onDelete: Cascade) + institute_id String + + @@allow('read,create,update', createdBy == auth() || institute.userRoles?[user == auth()] ) + @@allow('all', (createdBy == auth() && institute.userRoles?[user == auth()]) || institute.userRoles?[user == auth() && 'Admin' in role]) +} + +model User { + id String @id @unique + firstname String + lastname String? + username String @unique + photo String @default("media/default/avatar.jpg") + dob DateTime? + gender String? + bio String? + tagline String? + address String? + city String? + country String? + email String? + phone String? + accountRegType String @default("General") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @default(now()) + createdBy_id String @default("self") + updatedBy_id String @default("self") + auth_session AuthSession[] + auth_key AuthKey[] + userRole userRole[] + batchUser BatchUser[] + enrollment Enrollment[] + userRoleCreatedBy userRole[] @relation("userRolesCreatedBy") + instituteCreatedBy Institute[] @relation("InstituteCreatedBy") + instituteUpdatedBy Institute[] @relation("InstituteUpdatedBy") + courseCreatedBy Course[] @relation("CourseCreatedBy") + courseUpdatedBy Course[] @relation("CourseUpdatedBy") + courseAuthors Course[] @relation("CourseAuthors") + courseInstructor Course[] @relation("CourseInstructors") + enrollmentCreatedBy Enrollment[] @relation("EnrollmentCreatedBy") + enrollmentUpdatedBy Enrollment[] @relation("EnrollmentUpdatedBy") + certificateIssued CertificateIssued[] + admissions Admission[] @relation(name: "AdmissionStudent") + + deleted Boolean @default(false) @omit // soft delete + @@deny('read', deleted) + + // everybody can signup + @@allow('create,read', true) + + // full access by self + @@allow('all', auth() == this) + + @@allow('all', userRole?[user == auth() && 'Admin' in role]) + @@allow('read', userRole?[user == auth()]) + + @@map("auth_user") + @@schema("userauth") +} + +model AuthSession { + id String @id @unique + user_id String + active_expires BigInt + idle_expires BigInt + auth_user User @relation(references: [id], fields: [user_id], onDelete: Cascade) + + @@index([user_id]) + @@map("auth_session") + @@schema("userauth") +} + +model AuthKey { + id String @id @unique + hashed_password String? + user_id String + primary_key Boolean? + expires BigInt? + auth_user User @relation(references: [id], fields: [user_id], onDelete: Cascade) + + @@index([user_id]) + @@map("auth_key") + @@schema("userauth") +} + +model Institute { + id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + mode String + schoolname String @unique + name String + address String? + city String + country String + type String? + website String? + phone BigInt? + authenticationMethod String[] + periodsInADay Int? + periodDuration Int? + periods Int[] + workingDays String[] + officeHoursFrom Int? + officeHoursTo Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy User? @relation(name: "InstituteCreatedBy", fields: [createdBy_id], references: [id]) + createdBy_id String? + updatedBy User? @relation(name: "InstituteUpdatedBy", fields: [updatedBy_id], references: [id]) + updatedBy_id String? + userRoles userRole[] + enrollments Enrollment[] + certificatesIssued CertificateIssued[] + Batch Batch[] + courses Course[] + + admissions Admission[] + + @@allow('read', userRoles?[user == auth()]) + + @@map("institute") + @@schema("orgs") +} + +model userRole { + id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + user User @relation(references: [id], onDelete: Cascade, fields: [user_id]) + user_id String + institute Institute @relation(references: [id], onDelete: Cascade, fields: [institute_id]) + institute_id String @db.Uuid + role String[] + department String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy User @relation(name: "userRolesCreatedBy", fields: [createdBy_id], references: [id], onDelete: Cascade) + createdBy_id String + + @@unique([user_id, institute_id]) + + @@allow('all', institute.userRoles?[user == auth() && 'Admin' in role]) + @@allow('read', institute.userRoles?[user == auth()]) + + @@map("user_role") + @@schema("userauth") +} + +model Batch { + id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + name String + users BatchUser[] + institute Institute @relation(references: [id], fields: [institute_id]) + institute_id String @db.Uuid + capacity Int @default(30) + intake String + year Int @default(2023) + status String @default("Active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy String + updatedBy String + + enrollments Enrollment[] + + @@map("batch") + @@schema("courses") +} + +model BatchUser { + id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + user_id String + user User @relation(fields: [user_id], references: [id]) + role String + Batch Batch? @relation(fields: [batch_id], references: [id]) + batch_id String? @db.Uuid + + @@map("batch_user") + @@schema("courses") +} + +model Admission { + id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + student User @relation(name: "AdmissionStudent", fields: [student_id], references: [id]) + student_id String + admissionOfficer String? + admissionNumber BigInt + admissionDate DateTime @default(now()) + enrollments Enrollment[] + status String @default("Applied") + institute Institute @relation(references: [id], fields: [institute_id]) + institute_id String @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy_id String @default("self") + updatedBy_id String @default("self") + + @@unique([student_id, institute_id]) + @@unique([admissionNumber, institute_id]) + + @@map("admission") + @@schema("orgs") +} + +model Enrollment { + id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + student User @relation(fields: [student_id], references: [id]) + student_id String + admission Admission @relation(fields: [admission_id], references: [id]) + admission_id String @db.Uuid + course Course @relation(fields: [course_id], references: [id]) + course_id String @db.Uuid + batch Batch @relation(fields: [batch_id], references: [id]) + batch_id String @db.Uuid + enrollmentDate DateTime @default(now()) + status String @default("Active") + institute Institute @relation(references: [id], fields: [institute_id]) + institute_id String @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy User @relation(name: "EnrollmentCreatedBy", fields: [createdBy_id], references: [id]) + createdBy_id String + updatedBy User @relation(name: "EnrollmentUpdatedBy", fields: [updatedBy_id], references: [id]) + updatedBy_id String + certificatesIssued CertificateIssued[] + + @@allow('all', (createdBy == auth() && institute.userRoles?[user == auth()]) || institute.userRoles?[user == auth() && 'Admin' in role]) + @@allow('read', (student == auth() || course.instructors == auth() || course.authors == auth()) && institute.userRoles?[user == auth()]) + + @@map("enrollment") + @@schema("courses") +} + +model CertificateIssued { + id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + enrollment Enrollment? @relation(references: [id], fields: [enrollment_id]) + enrollment_id String? @db.Uuid + student User @relation(fields: [student_id], references: [id]) + student_id String + customContent String + issueDate DateTime + certificateUrl String + certificateType String + certificateSharing String? + institute Institute @relation(references: [id], fields: [institute_id]) + institute_id String @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy String + updatedBy String + + @@map("certificate_issued") + @@schema("courses") +} + +model Course { + id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + authors User[] @relation("CourseAuthors") + instructors User[] @relation("CourseInstructors") + authorityCode String + title String + description String? + image String? + basePrice Float @default(0) + baseCurrency String @default("QAR") + order Int @default(autoincrement()) + status String + sharedLevel String[] @default(["Course Instructors"]) //['Public', 'Institute Everyone', 'Course Everyone', 'Course Instructors', 'Batch', 'Grade'] + institute Institute @relation(references: [id], fields: [institute_id]) + institute_id String @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy User @relation(name: "CourseCreatedBy", fields: [createdBy_id], references: [id]) + createdBy_id String + updatedBy User @relation(name: "CourseUpdatedBy", fields: [updatedBy_id], references: [id]) + updatedBy_id String + enrollments Enrollment[] + + + deleted Boolean @default(false) @omit // soft delete + @@deny('read', deleted) + + @@allow('all', (createdBy == auth() && institute.userRoles?[user == auth()]) || institute.userRoles?[user == auth() && 'Admin' in role]) + @@allow('read', true) //public can read + @@allow('read, update', (authors == auth() || instructors == auth()) && institute.userRoles?[user == auth()]) + @@allow('read, update, create', institute.userRoles?[user == auth() && ('Instructor' in role || 'Author' in role)]) + + @@map("course") + @@schema("courses") +}`; + +describe('Regression: issue 704', () => { + const DB_NAME = 'refactor'; + let dbUrl: string; + let prisma: any; + + beforeEach(async () => { + dbUrl = await createPostgresDb(DB_NAME); + }); + + afterEach(async () => { + if (prisma) { + await prisma.$disconnect(); + } + await dropPostgresDb(DB_NAME); + }); + + it('regression', async () => { + const dbUrl = await createPostgresDb(DB_NAME); + const myModel = model.replace('env("DATABASE_URL")', `"${dbUrl}"`); + + const r = await loadSchema(myModel, { + provider: 'postgresql', + dbUrl, + addPrelude: false, + logPrismaQuery: true, + }); + + prisma = r.prisma; + const db = r.enhance({ id: '1' }); + const myCorseIds = [randomUUID()]; + await db.enrollment.count({ + where: { + course_id: { + in: myCorseIds, + }, + }, + }); + }); +});