diff --git a/packages/runtime/package.json b/packages/runtime/package.json index b46b800f..2ef29296 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -8,7 +8,7 @@ "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "test": "vitest run && pnpm test:generate && pnpm test:typecheck", - "test:generate": "tsx test/typing/generate.ts", + "test:generate": "tsx test/scripts/generate.ts", "test:typecheck": "tsc --project tsconfig.test.json", "pack": "pnpm pack" }, diff --git a/packages/runtime/test/scripts/generate.ts b/packages/runtime/test/scripts/generate.ts new file mode 100644 index 00000000..da402546 --- /dev/null +++ b/packages/runtime/test/scripts/generate.ts @@ -0,0 +1,25 @@ +import { glob } from 'glob'; +import { TsSchemaGenerator } from '@zenstackhq/sdk'; +import path from 'node:path'; +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const dir = path.dirname(fileURLToPath(import.meta.url)); + +async function main() { + await generate(path.resolve(dir, '../typing/typing-test.zmodel')); + await generate(path.resolve(dir, '../test-schema/schema.zmodel')); +} + +async function generate(schemaPath: string) { + const generator = new TsSchemaGenerator(); + const outputDir = path.dirname(schemaPath); + const tsPath = path.join(outputDir, 'schema.ts'); + const pluginModelFiles = glob.sync(path.resolve(dir, '../../dist/**/plugin.zmodel')); + await generator.generate(schemaPath, pluginModelFiles, outputDir); + const content = fs.readFileSync(tsPath, 'utf-8'); + fs.writeFileSync(tsPath, content.replace(/@zenstackhq\/runtime/g, '../../dist')); + console.log('TS schema generated at:', outputDir); +} + +main(); diff --git a/packages/runtime/test/test-schema.ts b/packages/runtime/test/test-schema.ts deleted file mode 100644 index 8102b20b..00000000 --- a/packages/runtime/test/test-schema.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { ExpressionUtils, type DataSourceProviderType, type SchemaDef } from '../src/schema'; - -export const schema = { - provider: { - type: 'sqlite', - }, - models: { - User: { - fields: { - id: { - type: 'String', - id: true, - default: ExpressionUtils.call('cuid'), - attributes: [ - { name: '@id' }, - { - name: '@default', - args: [ - { - value: { - kind: 'call', - function: 'cuid', - }, - }, - ], - }, - ], - }, - email: { - type: 'String', - unique: true, - attributes: [ - { - name: '@unique', - }, - ], - }, - name: { - type: 'String', - optional: true, - }, - createdAt: { - type: 'DateTime', - default: ExpressionUtils.call('now'), - attributes: [ - { - name: '@default', - args: [ - { - value: { - kind: 'call', - function: 'now', - }, - }, - ], - }, - ], - }, - updatedAt: { - type: 'DateTime', - updatedAt: true, - attributes: [ - { - name: '@updatedAt', - }, - ], - }, - role: { - type: 'Role', - default: 'USER', - }, - posts: { - type: 'Post', - array: true, - relation: { - opposite: 'author', - }, - }, - profile: { - type: 'Profile', - relation: { - opposite: 'user', - }, - optional: true, - }, - }, - idFields: ['id'], - uniqueFields: { - id: { type: 'String' }, - email: { type: 'String' }, - }, - attributes: [ - // @@allow('all', auth() == this) - { - name: '@@allow', - args: [ - { - name: 'operation', - value: ExpressionUtils.literal('all'), - }, - { - name: 'condition', - value: ExpressionUtils.binary( - ExpressionUtils.member(ExpressionUtils.call('auth'), ['id']), - '==', - ExpressionUtils.field('id'), - ), - }, - ], - }, - // @@allow('read', auth() != null) - { - name: '@@allow', - args: [ - { - name: 'operation', - value: ExpressionUtils.literal('read'), - }, - { - name: 'condition', - value: ExpressionUtils.binary(ExpressionUtils.call('auth'), '!=', ExpressionUtils._null()), - }, - ], - }, - ], - }, - Post: { - fields: { - id: { - type: 'String', - id: true, - default: ExpressionUtils.call('cuid'), - }, - createdAt: { - type: 'DateTime', - default: ExpressionUtils.call('now'), - }, - updatedAt: { - type: 'DateTime', - updatedAt: true, - }, - title: { - type: 'String', - }, - content: { - type: 'String', - optional: true, - }, - published: { - type: 'Boolean', - default: false, - }, - author: { - type: 'User', - relation: { - fields: ['authorId'], - references: ['id'], - opposite: 'posts', - onUpdate: 'Cascade', - onDelete: 'Cascade', - }, - }, - authorId: { - type: 'String', - foreignKeyFor: ['author'], - }, - comments: { - type: 'Comment', - array: true, - relation: { - opposite: 'post', - }, - }, - }, - idFields: ['id'], - uniqueFields: { - id: { type: 'String' }, - }, - attributes: [ - // @@deny('all', auth() == null) - { - name: '@@deny', - args: [ - { - name: 'operation', - value: ExpressionUtils.literal('all'), - }, - { - name: 'condition', - value: ExpressionUtils.binary(ExpressionUtils.call('auth'), '==', ExpressionUtils._null()), - }, - ], - }, - // @@allow('all', auth() == author) - { - name: '@@allow', - args: [ - { - name: 'operation', - value: ExpressionUtils.literal('all'), - }, - { - name: 'condition', - value: ExpressionUtils.binary( - ExpressionUtils.member(ExpressionUtils.call('auth'), ['id']), - '==', - ExpressionUtils.field('authorId'), - ), - }, - ], - }, - // @@allow('read', published) - { - name: '@@allow', - args: [ - { - name: 'operation', - value: ExpressionUtils.literal('read'), - }, - { - name: 'condition', - value: ExpressionUtils.field('published'), - }, - ], - }, - ], - }, - Comment: { - fields: { - id: { - type: 'String', - id: true, - default: ExpressionUtils.call('cuid'), - }, - createdAt: { - type: 'DateTime', - default: ExpressionUtils.call('now'), - }, - updatedAt: { - type: 'DateTime', - updatedAt: true, - }, - content: { - type: 'String', - }, - post: { - type: 'Post', - optional: true, - relation: { - fields: ['postId'], - references: ['id'], - opposite: 'comments', - onUpdate: 'Cascade', - onDelete: 'Cascade', - }, - }, - postId: { - type: 'String', - foreignKeyFor: ['post'], - optional: true, - }, - }, - idFields: ['id'], - uniqueFields: { - id: { type: 'String' }, - }, - }, - Profile: { - fields: { - id: { - type: 'String', - id: true, - default: ExpressionUtils.call('cuid'), - }, - bio: { type: 'String' }, - age: { type: 'Int', optional: true }, - user: { - type: 'User', - optional: true, - relation: { - fields: ['userId'], - references: ['id'], - opposite: 'profile', - onUpdate: 'Cascade', - onDelete: 'Cascade', - }, - }, - userId: { - type: 'String', - optional: true, - unique: true, - foreignKeyFor: ['user'], - }, - }, - idFields: ['id'], - uniqueFields: { - id: { type: 'String' }, - userId: { type: 'String' }, - }, - }, - }, - authType: 'User', - enums: { - Role: { - ADMIN: 'ADMIN', - USER: 'USER', - }, - }, - plugins: {}, -} as const satisfies SchemaDef; - -export function getSchema(type: ProviderType) { - return { - ...schema, - provider: { - type, - }, - }; -} diff --git a/packages/runtime/test/test-schema/helper.ts b/packages/runtime/test/test-schema/helper.ts new file mode 100644 index 00000000..6eac1589 --- /dev/null +++ b/packages/runtime/test/test-schema/helper.ts @@ -0,0 +1,11 @@ +import type { DataSourceProviderType } from '@zenstackhq/sdk/schema'; +import { schema } from './schema'; + +export function getSchema(type: ProviderType) { + return { + ...schema, + provider: { + type, + }, + }; +} diff --git a/packages/runtime/test/test-schema/index.ts b/packages/runtime/test/test-schema/index.ts new file mode 100644 index 00000000..895d9ca2 --- /dev/null +++ b/packages/runtime/test/test-schema/index.ts @@ -0,0 +1,2 @@ +export { getSchema } from './helper'; +export { schema } from './schema'; diff --git a/packages/runtime/test/test-schema/input.ts b/packages/runtime/test/test-schema/input.ts new file mode 100644 index 00000000..e283e0c9 --- /dev/null +++ b/packages/runtime/test/test-schema/input.ts @@ -0,0 +1,90 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema"; +import type { FindManyArgs as $FindManyArgs, FindUniqueArgs as $FindUniqueArgs, FindFirstArgs as $FindFirstArgs, CreateArgs as $CreateArgs, CreateManyArgs as $CreateManyArgs, CreateManyAndReturnArgs as $CreateManyAndReturnArgs, UpdateArgs as $UpdateArgs, UpdateManyArgs as $UpdateManyArgs, UpdateManyAndReturnArgs as $UpdateManyAndReturnArgs, UpsertArgs as $UpsertArgs, DeleteArgs as $DeleteArgs, DeleteManyArgs as $DeleteManyArgs, CountArgs as $CountArgs, AggregateArgs as $AggregateArgs, GroupByArgs as $GroupByArgs, WhereInput as $WhereInput, SelectInput as $SelectInput, IncludeInput as $IncludeInput, OmitInput as $OmitInput } from "@zenstackhq/runtime"; +import type { SimplifiedModelResult as $SimplifiedModelResult, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/runtime"; +export type UserFindManyArgs = $FindManyArgs<$Schema, "User">; +export type UserFindUniqueArgs = $FindUniqueArgs<$Schema, "User">; +export type UserFindFirstArgs = $FindFirstArgs<$Schema, "User">; +export type UserCreateArgs = $CreateArgs<$Schema, "User">; +export type UserCreateManyArgs = $CreateManyArgs<$Schema, "User">; +export type UserCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "User">; +export type UserUpdateArgs = $UpdateArgs<$Schema, "User">; +export type UserUpdateManyArgs = $UpdateManyArgs<$Schema, "User">; +export type UserUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "User">; +export type UserUpsertArgs = $UpsertArgs<$Schema, "User">; +export type UserDeleteArgs = $DeleteArgs<$Schema, "User">; +export type UserDeleteManyArgs = $DeleteManyArgs<$Schema, "User">; +export type UserCountArgs = $CountArgs<$Schema, "User">; +export type UserAggregateArgs = $AggregateArgs<$Schema, "User">; +export type UserGroupByArgs = $GroupByArgs<$Schema, "User">; +export type UserWhereInput = $WhereInput<$Schema, "User">; +export type UserSelect = $SelectInput<$Schema, "User">; +export type UserInclude = $IncludeInput<$Schema, "User">; +export type UserOmit = $OmitInput<$Schema, "User">; +export type UserGetPayload> = $SimplifiedModelResult<$Schema, "User", Args>; +export type PostFindManyArgs = $FindManyArgs<$Schema, "Post">; +export type PostFindUniqueArgs = $FindUniqueArgs<$Schema, "Post">; +export type PostFindFirstArgs = $FindFirstArgs<$Schema, "Post">; +export type PostCreateArgs = $CreateArgs<$Schema, "Post">; +export type PostCreateManyArgs = $CreateManyArgs<$Schema, "Post">; +export type PostCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Post">; +export type PostUpdateArgs = $UpdateArgs<$Schema, "Post">; +export type PostUpdateManyArgs = $UpdateManyArgs<$Schema, "Post">; +export type PostUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Post">; +export type PostUpsertArgs = $UpsertArgs<$Schema, "Post">; +export type PostDeleteArgs = $DeleteArgs<$Schema, "Post">; +export type PostDeleteManyArgs = $DeleteManyArgs<$Schema, "Post">; +export type PostCountArgs = $CountArgs<$Schema, "Post">; +export type PostAggregateArgs = $AggregateArgs<$Schema, "Post">; +export type PostGroupByArgs = $GroupByArgs<$Schema, "Post">; +export type PostWhereInput = $WhereInput<$Schema, "Post">; +export type PostSelect = $SelectInput<$Schema, "Post">; +export type PostInclude = $IncludeInput<$Schema, "Post">; +export type PostOmit = $OmitInput<$Schema, "Post">; +export type PostGetPayload> = $SimplifiedModelResult<$Schema, "Post", Args>; +export type CommentFindManyArgs = $FindManyArgs<$Schema, "Comment">; +export type CommentFindUniqueArgs = $FindUniqueArgs<$Schema, "Comment">; +export type CommentFindFirstArgs = $FindFirstArgs<$Schema, "Comment">; +export type CommentCreateArgs = $CreateArgs<$Schema, "Comment">; +export type CommentCreateManyArgs = $CreateManyArgs<$Schema, "Comment">; +export type CommentCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Comment">; +export type CommentUpdateArgs = $UpdateArgs<$Schema, "Comment">; +export type CommentUpdateManyArgs = $UpdateManyArgs<$Schema, "Comment">; +export type CommentUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Comment">; +export type CommentUpsertArgs = $UpsertArgs<$Schema, "Comment">; +export type CommentDeleteArgs = $DeleteArgs<$Schema, "Comment">; +export type CommentDeleteManyArgs = $DeleteManyArgs<$Schema, "Comment">; +export type CommentCountArgs = $CountArgs<$Schema, "Comment">; +export type CommentAggregateArgs = $AggregateArgs<$Schema, "Comment">; +export type CommentGroupByArgs = $GroupByArgs<$Schema, "Comment">; +export type CommentWhereInput = $WhereInput<$Schema, "Comment">; +export type CommentSelect = $SelectInput<$Schema, "Comment">; +export type CommentInclude = $IncludeInput<$Schema, "Comment">; +export type CommentOmit = $OmitInput<$Schema, "Comment">; +export type CommentGetPayload> = $SimplifiedModelResult<$Schema, "Comment", Args>; +export type ProfileFindManyArgs = $FindManyArgs<$Schema, "Profile">; +export type ProfileFindUniqueArgs = $FindUniqueArgs<$Schema, "Profile">; +export type ProfileFindFirstArgs = $FindFirstArgs<$Schema, "Profile">; +export type ProfileCreateArgs = $CreateArgs<$Schema, "Profile">; +export type ProfileCreateManyArgs = $CreateManyArgs<$Schema, "Profile">; +export type ProfileCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Profile">; +export type ProfileUpdateArgs = $UpdateArgs<$Schema, "Profile">; +export type ProfileUpdateManyArgs = $UpdateManyArgs<$Schema, "Profile">; +export type ProfileUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Profile">; +export type ProfileUpsertArgs = $UpsertArgs<$Schema, "Profile">; +export type ProfileDeleteArgs = $DeleteArgs<$Schema, "Profile">; +export type ProfileDeleteManyArgs = $DeleteManyArgs<$Schema, "Profile">; +export type ProfileCountArgs = $CountArgs<$Schema, "Profile">; +export type ProfileAggregateArgs = $AggregateArgs<$Schema, "Profile">; +export type ProfileGroupByArgs = $GroupByArgs<$Schema, "Profile">; +export type ProfileWhereInput = $WhereInput<$Schema, "Profile">; +export type ProfileSelect = $SelectInput<$Schema, "Profile">; +export type ProfileInclude = $IncludeInput<$Schema, "Profile">; +export type ProfileOmit = $OmitInput<$Schema, "Profile">; +export type ProfileGetPayload> = $SimplifiedModelResult<$Schema, "Profile", Args>; diff --git a/packages/runtime/test/test-schema/models.ts b/packages/runtime/test/test-schema/models.ts new file mode 100644 index 00000000..f9dbfc98 --- /dev/null +++ b/packages/runtime/test/test-schema/models.ts @@ -0,0 +1,15 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { schema as $schema, type SchemaType as $Schema } from "./schema"; +import { type ModelResult as $ModelResult } from "@zenstackhq/runtime"; +export type User = $ModelResult<$Schema, "User">; +export type Post = $ModelResult<$Schema, "Post">; +export type Comment = $ModelResult<$Schema, "Comment">; +export type Profile = $ModelResult<$Schema, "Profile">; +export const Role = $schema.enums.Role; +export type Role = (typeof Role)[keyof typeof Role]; diff --git a/packages/runtime/test/test-schema/schema.ts b/packages/runtime/test/test-schema/schema.ts new file mode 100644 index 00000000..610cb4b0 --- /dev/null +++ b/packages/runtime/test/test-schema/schema.ts @@ -0,0 +1,211 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, ExpressionUtils } from "../../dist/schema"; +export const schema = { + provider: { + type: "sqlite" + }, + models: { + User: { + fields: { + id: { + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], + default: ExpressionUtils.call("cuid") + }, + email: { + type: "String", + unique: true, + attributes: [{ name: "@unique" }] + }, + name: { + type: "String", + optional: true + }, + createdAt: { + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] + }, + role: { + type: "Role", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("USER") }] }], + default: "USER" + }, + posts: { + type: "Post", + array: true, + relation: { opposite: "author" } + }, + profile: { + type: "Profile", + optional: true, + relation: { opposite: "user" } + } + }, + attributes: [ + { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("all") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"]), "==", ExpressionUtils.field("id")) }] }, + { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("read") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.call("auth"), "!=", ExpressionUtils._null()) }] } + ], + idFields: ["id"], + uniqueFields: { + id: { type: "String" }, + email: { type: "String" } + } + }, + Post: { + fields: { + id: { + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], + default: ExpressionUtils.call("cuid") + }, + createdAt: { + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] + }, + title: { + type: "String" + }, + content: { + type: "String", + optional: true + }, + published: { + type: "Boolean", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }], + default: false + }, + author: { + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } + }, + authorId: { + type: "String", + foreignKeyFor: [ + "author" + ] + }, + comments: { + type: "Comment", + array: true, + relation: { opposite: "post" } + } + }, + attributes: [ + { name: "@@deny", args: [{ name: "operation", value: ExpressionUtils.literal("all") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.call("auth"), "==", ExpressionUtils._null()) }] }, + { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("all") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"]), "==", ExpressionUtils.field("authorId")) }] }, + { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("read") }, { name: "condition", value: ExpressionUtils.field("published") }] } + ], + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + }, + Comment: { + fields: { + id: { + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], + default: ExpressionUtils.call("cuid") + }, + createdAt: { + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] + }, + content: { + type: "String" + }, + post: { + type: "Post", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("postId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "comments", fields: ["postId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } + }, + postId: { + type: "String", + optional: true, + foreignKeyFor: [ + "post" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + }, + Profile: { + fields: { + id: { + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], + default: ExpressionUtils.call("cuid") + }, + bio: { + type: "String" + }, + age: { + type: "Int", + optional: true + }, + user: { + type: "User", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "profile", fields: ["userId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } + }, + userId: { + type: "String", + unique: true, + optional: true, + attributes: [{ name: "@unique" }], + foreignKeyFor: [ + "user" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" }, + userId: { type: "String" } + } + } + }, + enums: { + Role: { + ADMIN: "ADMIN", + USER: "USER" + } + }, + authType: "User", + plugins: {} +} as const satisfies SchemaDef; +export type SchemaType = typeof schema; diff --git a/packages/runtime/test/test-schema/schema.zmodel b/packages/runtime/test/test-schema/schema.zmodel new file mode 100644 index 00000000..fb041b5a --- /dev/null +++ b/packages/runtime/test/test-schema/schema.zmodel @@ -0,0 +1,62 @@ +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +plugin policy { + provider = "../../dist/plugins/policy" +} + +enum Role { + ADMIN + USER +} + +model User { + id String @id @default(cuid()) + email String @unique + name String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + role Role @default(USER) + posts Post[] + profile Profile? + + // Access policies + @@allow('all', auth().id == id) + @@allow('read', auth() != null) +} + +model Post { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + content String? + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id], onUpdate: Cascade, onDelete: Cascade) + authorId String + comments Comment[] + + // Access policies + @@deny('all', auth() == null) + @@allow('all', auth().id == authorId) + @@allow('read', published) +} + +model Comment { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + content String + post Post? @relation(fields: [postId], references: [id], onUpdate: Cascade, onDelete: Cascade) + postId String? +} + +model Profile { + id String @id @default(cuid()) + bio String + age Int? + user User? @relation(fields: [userId], references: [id], onUpdate: Cascade, onDelete: Cascade) + userId String? @unique +} diff --git a/packages/runtime/test/typing/generate.ts b/packages/runtime/test/typing/generate.ts deleted file mode 100644 index 3ac77565..00000000 --- a/packages/runtime/test/typing/generate.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TsSchemaGenerator } from '@zenstackhq/sdk'; -import path from 'node:path'; -import fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; - -async function main() { - const generator = new TsSchemaGenerator(); - const dir = path.dirname(fileURLToPath(import.meta.url)); - const zmodelPath = path.join(dir, 'typing-test.zmodel'); - const tsPath = path.join(dir, 'schema.ts'); - await generator.generate(zmodelPath, [], dir); - - const content = fs.readFileSync(tsPath, 'utf-8'); - fs.writeFileSync(tsPath, content.replace(/@zenstackhq\/runtime/g, '../../dist')); - - console.log('TS schema generated at:', tsPath); -} - -main(); diff --git a/packages/runtime/test/typing/input.ts b/packages/runtime/test/typing/input.ts new file mode 100644 index 00000000..c7fd0f5f --- /dev/null +++ b/packages/runtime/test/typing/input.ts @@ -0,0 +1,130 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema"; +import type { FindManyArgs as $FindManyArgs, FindUniqueArgs as $FindUniqueArgs, FindFirstArgs as $FindFirstArgs, CreateArgs as $CreateArgs, CreateManyArgs as $CreateManyArgs, CreateManyAndReturnArgs as $CreateManyAndReturnArgs, UpdateArgs as $UpdateArgs, UpdateManyArgs as $UpdateManyArgs, UpdateManyAndReturnArgs as $UpdateManyAndReturnArgs, UpsertArgs as $UpsertArgs, DeleteArgs as $DeleteArgs, DeleteManyArgs as $DeleteManyArgs, CountArgs as $CountArgs, AggregateArgs as $AggregateArgs, GroupByArgs as $GroupByArgs, WhereInput as $WhereInput, SelectInput as $SelectInput, IncludeInput as $IncludeInput, OmitInput as $OmitInput } from "@zenstackhq/runtime"; +import type { SimplifiedModelResult as $SimplifiedModelResult, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/runtime"; +export type UserFindManyArgs = $FindManyArgs<$Schema, "User">; +export type UserFindUniqueArgs = $FindUniqueArgs<$Schema, "User">; +export type UserFindFirstArgs = $FindFirstArgs<$Schema, "User">; +export type UserCreateArgs = $CreateArgs<$Schema, "User">; +export type UserCreateManyArgs = $CreateManyArgs<$Schema, "User">; +export type UserCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "User">; +export type UserUpdateArgs = $UpdateArgs<$Schema, "User">; +export type UserUpdateManyArgs = $UpdateManyArgs<$Schema, "User">; +export type UserUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "User">; +export type UserUpsertArgs = $UpsertArgs<$Schema, "User">; +export type UserDeleteArgs = $DeleteArgs<$Schema, "User">; +export type UserDeleteManyArgs = $DeleteManyArgs<$Schema, "User">; +export type UserCountArgs = $CountArgs<$Schema, "User">; +export type UserAggregateArgs = $AggregateArgs<$Schema, "User">; +export type UserGroupByArgs = $GroupByArgs<$Schema, "User">; +export type UserWhereInput = $WhereInput<$Schema, "User">; +export type UserSelect = $SelectInput<$Schema, "User">; +export type UserInclude = $IncludeInput<$Schema, "User">; +export type UserOmit = $OmitInput<$Schema, "User">; +export type UserGetPayload> = $SimplifiedModelResult<$Schema, "User", Args>; +export type PostFindManyArgs = $FindManyArgs<$Schema, "Post">; +export type PostFindUniqueArgs = $FindUniqueArgs<$Schema, "Post">; +export type PostFindFirstArgs = $FindFirstArgs<$Schema, "Post">; +export type PostCreateArgs = $CreateArgs<$Schema, "Post">; +export type PostCreateManyArgs = $CreateManyArgs<$Schema, "Post">; +export type PostCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Post">; +export type PostUpdateArgs = $UpdateArgs<$Schema, "Post">; +export type PostUpdateManyArgs = $UpdateManyArgs<$Schema, "Post">; +export type PostUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Post">; +export type PostUpsertArgs = $UpsertArgs<$Schema, "Post">; +export type PostDeleteArgs = $DeleteArgs<$Schema, "Post">; +export type PostDeleteManyArgs = $DeleteManyArgs<$Schema, "Post">; +export type PostCountArgs = $CountArgs<$Schema, "Post">; +export type PostAggregateArgs = $AggregateArgs<$Schema, "Post">; +export type PostGroupByArgs = $GroupByArgs<$Schema, "Post">; +export type PostWhereInput = $WhereInput<$Schema, "Post">; +export type PostSelect = $SelectInput<$Schema, "Post">; +export type PostInclude = $IncludeInput<$Schema, "Post">; +export type PostOmit = $OmitInput<$Schema, "Post">; +export type PostGetPayload> = $SimplifiedModelResult<$Schema, "Post", Args>; +export type ProfileFindManyArgs = $FindManyArgs<$Schema, "Profile">; +export type ProfileFindUniqueArgs = $FindUniqueArgs<$Schema, "Profile">; +export type ProfileFindFirstArgs = $FindFirstArgs<$Schema, "Profile">; +export type ProfileCreateArgs = $CreateArgs<$Schema, "Profile">; +export type ProfileCreateManyArgs = $CreateManyArgs<$Schema, "Profile">; +export type ProfileCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Profile">; +export type ProfileUpdateArgs = $UpdateArgs<$Schema, "Profile">; +export type ProfileUpdateManyArgs = $UpdateManyArgs<$Schema, "Profile">; +export type ProfileUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Profile">; +export type ProfileUpsertArgs = $UpsertArgs<$Schema, "Profile">; +export type ProfileDeleteArgs = $DeleteArgs<$Schema, "Profile">; +export type ProfileDeleteManyArgs = $DeleteManyArgs<$Schema, "Profile">; +export type ProfileCountArgs = $CountArgs<$Schema, "Profile">; +export type ProfileAggregateArgs = $AggregateArgs<$Schema, "Profile">; +export type ProfileGroupByArgs = $GroupByArgs<$Schema, "Profile">; +export type ProfileWhereInput = $WhereInput<$Schema, "Profile">; +export type ProfileSelect = $SelectInput<$Schema, "Profile">; +export type ProfileInclude = $IncludeInput<$Schema, "Profile">; +export type ProfileOmit = $OmitInput<$Schema, "Profile">; +export type ProfileGetPayload> = $SimplifiedModelResult<$Schema, "Profile", Args>; +export type TagFindManyArgs = $FindManyArgs<$Schema, "Tag">; +export type TagFindUniqueArgs = $FindUniqueArgs<$Schema, "Tag">; +export type TagFindFirstArgs = $FindFirstArgs<$Schema, "Tag">; +export type TagCreateArgs = $CreateArgs<$Schema, "Tag">; +export type TagCreateManyArgs = $CreateManyArgs<$Schema, "Tag">; +export type TagCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Tag">; +export type TagUpdateArgs = $UpdateArgs<$Schema, "Tag">; +export type TagUpdateManyArgs = $UpdateManyArgs<$Schema, "Tag">; +export type TagUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Tag">; +export type TagUpsertArgs = $UpsertArgs<$Schema, "Tag">; +export type TagDeleteArgs = $DeleteArgs<$Schema, "Tag">; +export type TagDeleteManyArgs = $DeleteManyArgs<$Schema, "Tag">; +export type TagCountArgs = $CountArgs<$Schema, "Tag">; +export type TagAggregateArgs = $AggregateArgs<$Schema, "Tag">; +export type TagGroupByArgs = $GroupByArgs<$Schema, "Tag">; +export type TagWhereInput = $WhereInput<$Schema, "Tag">; +export type TagSelect = $SelectInput<$Schema, "Tag">; +export type TagInclude = $IncludeInput<$Schema, "Tag">; +export type TagOmit = $OmitInput<$Schema, "Tag">; +export type TagGetPayload> = $SimplifiedModelResult<$Schema, "Tag", Args>; +export type RegionFindManyArgs = $FindManyArgs<$Schema, "Region">; +export type RegionFindUniqueArgs = $FindUniqueArgs<$Schema, "Region">; +export type RegionFindFirstArgs = $FindFirstArgs<$Schema, "Region">; +export type RegionCreateArgs = $CreateArgs<$Schema, "Region">; +export type RegionCreateManyArgs = $CreateManyArgs<$Schema, "Region">; +export type RegionCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Region">; +export type RegionUpdateArgs = $UpdateArgs<$Schema, "Region">; +export type RegionUpdateManyArgs = $UpdateManyArgs<$Schema, "Region">; +export type RegionUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Region">; +export type RegionUpsertArgs = $UpsertArgs<$Schema, "Region">; +export type RegionDeleteArgs = $DeleteArgs<$Schema, "Region">; +export type RegionDeleteManyArgs = $DeleteManyArgs<$Schema, "Region">; +export type RegionCountArgs = $CountArgs<$Schema, "Region">; +export type RegionAggregateArgs = $AggregateArgs<$Schema, "Region">; +export type RegionGroupByArgs = $GroupByArgs<$Schema, "Region">; +export type RegionWhereInput = $WhereInput<$Schema, "Region">; +export type RegionSelect = $SelectInput<$Schema, "Region">; +export type RegionInclude = $IncludeInput<$Schema, "Region">; +export type RegionOmit = $OmitInput<$Schema, "Region">; +export type RegionGetPayload> = $SimplifiedModelResult<$Schema, "Region", Args>; +export type MetaFindManyArgs = $FindManyArgs<$Schema, "Meta">; +export type MetaFindUniqueArgs = $FindUniqueArgs<$Schema, "Meta">; +export type MetaFindFirstArgs = $FindFirstArgs<$Schema, "Meta">; +export type MetaCreateArgs = $CreateArgs<$Schema, "Meta">; +export type MetaCreateManyArgs = $CreateManyArgs<$Schema, "Meta">; +export type MetaCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Meta">; +export type MetaUpdateArgs = $UpdateArgs<$Schema, "Meta">; +export type MetaUpdateManyArgs = $UpdateManyArgs<$Schema, "Meta">; +export type MetaUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Meta">; +export type MetaUpsertArgs = $UpsertArgs<$Schema, "Meta">; +export type MetaDeleteArgs = $DeleteArgs<$Schema, "Meta">; +export type MetaDeleteManyArgs = $DeleteManyArgs<$Schema, "Meta">; +export type MetaCountArgs = $CountArgs<$Schema, "Meta">; +export type MetaAggregateArgs = $AggregateArgs<$Schema, "Meta">; +export type MetaGroupByArgs = $GroupByArgs<$Schema, "Meta">; +export type MetaWhereInput = $WhereInput<$Schema, "Meta">; +export type MetaSelect = $SelectInput<$Schema, "Meta">; +export type MetaInclude = $IncludeInput<$Schema, "Meta">; +export type MetaOmit = $OmitInput<$Schema, "Meta">; +export type MetaGetPayload> = $SimplifiedModelResult<$Schema, "Meta", Args>; diff --git a/packages/runtime/test/typing/models.ts b/packages/runtime/test/typing/models.ts index cb27df2a..e418ffad 100644 --- a/packages/runtime/test/typing/models.ts +++ b/packages/runtime/test/typing/models.ts @@ -5,14 +5,13 @@ /* eslint-disable */ -import { type ModelResult } from "@zenstackhq/runtime"; -import { schema } from "./schema"; -export type Schema = typeof schema; -export type User = ModelResult; -export type Post = ModelResult; -export type Profile = ModelResult; -export type Tag = ModelResult; -export type Region = ModelResult; -export type Meta = ModelResult; -export const Role = schema.enums.Role; +import { schema as $schema, type SchemaType as $Schema } from "./schema"; +import { type ModelResult as $ModelResult } from "@zenstackhq/runtime"; +export type User = $ModelResult<$Schema, "User">; +export type Post = $ModelResult<$Schema, "Post">; +export type Profile = $ModelResult<$Schema, "Profile">; +export type Tag = $ModelResult<$Schema, "Tag">; +export type Region = $ModelResult<$Schema, "Region">; +export type Meta = $ModelResult<$Schema, "Meta">; +export const Role = $schema.enums.Role; export type Role = (typeof Role)[keyof typeof Role]; diff --git a/packages/runtime/tsup.config.ts b/packages/runtime/tsup.config.ts index 9278c545..795f2b2b 100644 --- a/packages/runtime/tsup.config.ts +++ b/packages/runtime/tsup.config.ts @@ -1,11 +1,12 @@ import { defineConfig } from 'tsup'; +import fs from 'node:fs'; export default defineConfig({ entry: { index: 'src/index.ts', schema: 'src/schema/index.ts', helpers: 'src/helpers.ts', - 'plugins/policy': 'src/plugins/policy/index.ts', + 'plugins/policy/index': 'src/plugins/policy/index.ts', }, outDir: 'dist', splitting: false, @@ -13,4 +14,7 @@ export default defineConfig({ clean: true, dts: true, format: ['cjs', 'esm'], + async onSuccess() { + fs.cpSync('src/plugins/policy/plugin.zmodel', 'dist/plugins/policy/plugin.zmodel'); + }, }); diff --git a/tests/e2e/prisma-consistency/attributes.test.ts b/tests/e2e/prisma-consistency/attributes.test.ts new file mode 100644 index 00000000..be0eeb40 --- /dev/null +++ b/tests/e2e/prisma-consistency/attributes.test.ts @@ -0,0 +1,60 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema } from './test-utils'; + +describe('Attributes Validation', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should reject duplicate field attributes', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique @unique + name String +} + `); + + expectValidationFailure(result); + }); + + it('should reject invalid default value type', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @default(123) + name String +} + `); + + expectValidationFailure(result); + }); + + it('should accept valid @map attribute', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique @map("email_address") + name String + + @@map("users") +} + `); + + expectValidationSuccess(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/basic-models.test.ts b/tests/e2e/prisma-consistency/basic-models.test.ts new file mode 100644 index 00000000..067ab9c9 --- /dev/null +++ b/tests/e2e/prisma-consistency/basic-models.test.ts @@ -0,0 +1,99 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema } from './test-utils'; + +describe('Basic Models Validation', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should accept valid basic model with id field', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? +} + `); + + expectValidationSuccess(result); + }); + + it('should reject model without any unique criterion', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + email String + name String? +} + `); + + expectValidationFailure(result); + }); + + it('should reject model with multiple @id fields', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @id + name String? +} + `); + + expectValidationFailure(result); + }); + + it('should reject model with both @id field and @@id', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + firstName String + lastName String + + @@id([firstName, lastName]) +} + `); + + expectValidationFailure(result); + }); + + it('should reject optional ID field', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int? @id @default(autoincrement()) + email String @unique +} + `); + + expectValidationFailure(result); + }); + + it('should reject array ID field', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int[] @id + email String @unique +} + `); + + expectValidationFailure(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/compound-ids.test.ts b/tests/e2e/prisma-consistency/compound-ids.test.ts new file mode 100644 index 00000000..90b42f9b --- /dev/null +++ b/tests/e2e/prisma-consistency/compound-ids.test.ts @@ -0,0 +1,47 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema } from './test-utils'; + +describe('Compound IDs Validation', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should accept valid compound ID with @@id', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + firstName String + lastName String + age Int + + @@id([firstName, lastName]) +} + `); + + expectValidationSuccess(result); + }); + + it('should reject empty compound ID', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + firstName String + lastName String + + @@id([]) +} + `); + + expectValidationFailure(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/datasource.test.ts b/tests/e2e/prisma-consistency/datasource.test.ts new file mode 100644 index 00000000..7746dbf8 --- /dev/null +++ b/tests/e2e/prisma-consistency/datasource.test.ts @@ -0,0 +1,64 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema } from './test-utils'; + +describe('Datasource Validation', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should reject multiple datasources', () => { + const result = tester.runValidation(` +datasource db1 { + provider = "postgresql" + url = env("DATABASE_URL") +} + +datasource db2 { + provider = "sqlite" + url = "file:./dev.db" +} + +model User { + id Int @id @default(autoincrement()) + name String +} + `); + + expectValidationFailure(result); + }); + + it('should reject missing datasource', () => { + const result = tester.runValidation(` +model User { + id Int @id @default(autoincrement()) + name String +} + `); + + expectValidationFailure(result); + }); + + it('should reject invalid provider', () => { + const result = tester.runValidation(` +datasource db { + provider = "nosql" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + name String +} + `); + + expectValidationFailure(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/enums.test.ts b/tests/e2e/prisma-consistency/enums.test.ts new file mode 100644 index 00000000..9a0719c3 --- /dev/null +++ b/tests/e2e/prisma-consistency/enums.test.ts @@ -0,0 +1,53 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema } from './test-utils'; + +describe('Enums Validation', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should accept valid enum definition and usage', () => { + const result = tester.runValidation(` +${baseSchema} + +enum Role { + USER + ADMIN + MODERATOR +} + +model User { + id Int @id @default(autoincrement()) + role Role @default(USER) + name String +} + `); + + expectValidationSuccess(result); + }); + + it('should reject empty enum', () => { + const result = tester.runValidation(` +${baseSchema} + +enum Role { +} + +model User { + id Int @id @default(autoincrement()) + role Role @default(USER) + name String +} + `); + + expectValidationFailure(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/field-types.test.ts b/tests/e2e/prisma-consistency/field-types.test.ts new file mode 100644 index 00000000..ac9a0081 --- /dev/null +++ b/tests/e2e/prisma-consistency/field-types.test.ts @@ -0,0 +1,55 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema, sqliteSchema } from './test-utils'; + +describe('Field Types Validation', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should reject optional array field', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + tags String[]? +} + `); + + expectValidationFailure(result); + }); + + it('should reject array field with SQLite', () => { + const result = tester.runValidation(` +${sqliteSchema} + +model User { + id Int @id @default(autoincrement()) + tags String[] +} + `); + + expectValidationFailure(result); + }); + + it('should accept array field with PostgreSQL', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + tags String[] +} + `); + + expectValidationSuccess(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/relation-validation.test.ts b/tests/e2e/prisma-consistency/relation-validation.test.ts new file mode 100644 index 00000000..acf35ee3 --- /dev/null +++ b/tests/e2e/prisma-consistency/relation-validation.test.ts @@ -0,0 +1,163 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema } from './test-utils'; + +describe('Relation Validation Rules', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should reject mismatched length of fields and references arrays', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id, email]) + authorId Int +} + `); + + expectValidationFailure(result); + }); + + it('should reject empty fields array', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [], references: [id]) + authorId Int +} + `); + + expectValidationFailure(result); + }); + + it('should reject empty references array', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: []) + authorId Int +} + `); + + expectValidationFailure(result); + }); + + it('should reject partial relation specification with only fields', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId]) + authorId Int +} + `); + + expectValidationFailure(result); + }); + + it('should reject partial relation specification with only references', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(references: [id]) + authorId Int +} + `); + + expectValidationFailure(result); + }); + + it('should reject both sides of relation with fields/references', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] @relation(fields: [id], references: [authorId]) +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int +} + `); + + expectValidationFailure(result); + }); + + it('should reject type mismatch between fields and references', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id String @id @default(cuid()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int +} + `); + + expectValidationFailure(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/relations-many-to-many.test.ts b/tests/e2e/prisma-consistency/relations-many-to-many.test.ts new file mode 100644 index 00000000..7a89de96 --- /dev/null +++ b/tests/e2e/prisma-consistency/relations-many-to-many.test.ts @@ -0,0 +1,85 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema } from './test-utils'; + +describe('Many-to-Many Relations Validation', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should accept valid implicit many-to-many relation', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + authors User[] +} + `); + + expectValidationSuccess(result); + }); + + it('should accept valid explicit many-to-many relation', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts PostUser[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + authors PostUser[] +} + +model PostUser { + user User @relation(fields: [userId], references: [id]) + post Post @relation(fields: [postId], references: [id]) + userId Int + postId Int + + @@id([userId, postId]) +} + `); + + expectValidationSuccess(result); + }); + + it('should reject implicit many-to-many with explicit @relation', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] @relation(fields: [id], references: [id]) +} + +model Post { + id Int @id @default(autoincrement()) + title String + authors User[] +} + `); + + expectValidationFailure(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/relations-one-to-many.test.ts b/tests/e2e/prisma-consistency/relations-one-to-many.test.ts new file mode 100644 index 00000000..dc4048a8 --- /dev/null +++ b/tests/e2e/prisma-consistency/relations-one-to-many.test.ts @@ -0,0 +1,78 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema } from './test-utils'; + +describe('One-to-Many Relations Validation', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should accept valid one-to-many relation', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int +} + `); + + expectValidationSuccess(result); + }); + + it('should reject one-to-many without @relation annotation', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User + authorId Int +} + `); + + expectValidationFailure(result); + }); + + it('should reject one-to-many relation referencing non-existent FK field', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) +} + `); + + expectValidationFailure(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/relations-one-to-one.test.ts b/tests/e2e/prisma-consistency/relations-one-to-one.test.ts new file mode 100644 index 00000000..b73726dd --- /dev/null +++ b/tests/e2e/prisma-consistency/relations-one-to-one.test.ts @@ -0,0 +1,99 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema } from './test-utils'; + +describe('One-to-One Relations Validation', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should accept valid one-to-one relation', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + profile Profile? +} + +model Profile { + id Int @id @default(autoincrement()) + bio String + user User @relation(fields: [userId], references: [id]) + userId Int @unique +} + `); + + expectValidationSuccess(result); + }); + + it('should reject one-to-one relation without @unique on FK', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + profile Profile? +} + +model Profile { + id Int @id @default(autoincrement()) + bio String + user User @relation(fields: [userId], references: [id]) + userId Int +} + `); + + expectValidationFailure(result); + }); + + it('should reject one-to-one relation missing opposite field', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + profile Profile? +} + +model Profile { + id Int @id @default(autoincrement()) + bio String + userId Int @unique +} + `); + + expectValidationFailure(result); + }); + + it('should reject one-to-one with both sides required', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + profile Profile +} + +model Profile { + id Int @id @default(autoincrement()) + bio String + user User @relation(fields: [userId], references: [id]) + userId Int @unique +} + `); + + expectValidationFailure(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/relations-self.test.ts b/tests/e2e/prisma-consistency/relations-self.test.ts new file mode 100644 index 00000000..49077c6a --- /dev/null +++ b/tests/e2e/prisma-consistency/relations-self.test.ts @@ -0,0 +1,63 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema } from './test-utils'; + +describe('Self Relations Validation', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should accept valid self relation with proper naming', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + manager User? @relation("UserManager", fields: [managerId], references: [id]) + managerId Int? + employees User[] @relation("UserManager") +} + `); + + expectValidationSuccess(result); + }); + + it('should reject self relation without relation name', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + manager User? @relation(fields: [managerId], references: [id]) + managerId Int? + employees User[] +} + `); + + expectValidationFailure(result); + }); + + it('should accept self many-to-many relation', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String @unique + following User[] @relation("UserFollows") + followers User[] @relation("UserFollows") +} + `); + + expectValidationSuccess(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/test-utils.ts b/tests/e2e/prisma-consistency/test-utils.ts new file mode 100644 index 00000000..727426a5 --- /dev/null +++ b/tests/e2e/prisma-consistency/test-utils.ts @@ -0,0 +1,118 @@ +import { execSync } from 'child_process'; +import { randomUUID } from 'crypto'; +import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import { expect } from 'vitest'; + +export interface ValidationResult { + success: boolean; + errors: string[]; +} + +export class ZenStackValidationTester { + private testDir: string; + private schemaPath: string; + private cliPath: string; + + constructor(testDir: string) { + this.testDir = testDir; + this.schemaPath = join(testDir, 'zenstack', 'schema.zmodel'); + + // Get path relative to current test file + const currentDir = dirname(fileURLToPath(import.meta.url)); + this.cliPath = join(currentDir, '../node_modules/@zenstackhq/cli/bin/cli'); + } + + private setupTestDirectory() { + if (existsSync(this.testDir)) { + rmSync(this.testDir, { recursive: true, force: true }); + } + mkdirSync(this.testDir, { recursive: true }); + mkdirSync(join(this.testDir, 'zenstack'), { recursive: true }); + + // Create package.json + writeFileSync( + join(this.testDir, 'package.json'), + JSON.stringify( + { + name: 'zenstack-validation-test', + version: '1.0.0', + private: true, + }, + null, + 2, + ), + ); + } + + public runValidation(schema: string): ValidationResult { + this.setupTestDirectory(); + writeFileSync(this.schemaPath, schema); + + try { + execSync(`node ${this.cliPath} generate`, { + cwd: this.testDir, + stdio: 'pipe', + encoding: 'utf8', + }); + + return { + success: true, + errors: [], + }; + } catch (error: any) { + return { + success: false, + errors: this.extractErrors(error.stderr), + }; + } + } + + private extractErrors(output: string): string[] { + const lines = output.split('\n'); + const errors: string[] = []; + + for (const line of lines) { + if (line.includes('Error:') || line.includes('error:') || line.includes('✖')) { + errors.push(line.trim()); + } + } + + return errors; + } + + public cleanup() { + if (existsSync(this.testDir)) { + rmSync(this.testDir, { recursive: true, force: true }); + } + } +} + +export function createTestDir(): string { + return join(tmpdir(), 'zenstack-validation-test-' + randomUUID()); +} + +export function expectValidationSuccess(result: ValidationResult) { + expect(result.success).toBe(true); +} + +export function expectValidationFailure(result: ValidationResult) { + expect(result.success).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); +} + +export const baseSchema = ` +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} +`; + +export const sqliteSchema = ` +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} +`; \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/unique-constraints.test.ts b/tests/e2e/prisma-consistency/unique-constraints.test.ts new file mode 100644 index 00000000..c39d456e --- /dev/null +++ b/tests/e2e/prisma-consistency/unique-constraints.test.ts @@ -0,0 +1,63 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationSuccess, expectValidationFailure, baseSchema } from './test-utils'; + +describe('Unique Constraints Validation', () => { + let tester: ZenStackValidationTester; + let tempDir: string; + + beforeEach(() => { + tempDir = createTestDir(); + tester = new ZenStackValidationTester(tempDir); + }); + + afterEach(() => { + tester.cleanup(); + }); + + it('should accept valid compound unique constraint', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + firstName String + lastName String + email String @unique + + @@unique([firstName, lastName]) +} + `); + + expectValidationSuccess(result); + }); + + it('should reject empty unique constraint', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + firstName String + lastName String + + @@unique([]) +} + `); + + expectValidationFailure(result); + }); + + it('should accept unique constraint on optional field', () => { + const result = tester.runValidation(` +${baseSchema} + +model User { + id Int @id @default(autoincrement()) + email String? @unique + name String +} + `); + + expectValidationSuccess(result); + }); +}); \ No newline at end of file diff --git a/tests/e2e/prisma-consistency/zmodel-validation.test.ts b/tests/e2e/prisma-consistency/zmodel-validation.test.ts deleted file mode 100644 index 24730512..00000000 --- a/tests/e2e/prisma-consistency/zmodel-validation.test.ts +++ /dev/null @@ -1,1012 +0,0 @@ -import { execSync } from 'child_process'; -import { randomUUID } from 'crypto'; -import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'; -import { tmpdir } from 'os'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; - -interface ValidationResult { - success: boolean; - errors: string[]; -} - -class ZenStackValidationTester { - private testDir: string; - private schemaPath: string; - private cliPath: string; - - constructor(testDir: string) { - this.testDir = testDir; - this.schemaPath = join(testDir, 'zenstack', 'schema.zmodel'); - - // Get path relative to current test file - const currentDir = dirname(fileURLToPath(import.meta.url)); - this.cliPath = join(currentDir, '../node_modules/@zenstackhq/cli/bin/cli'); - } - - private setupTestDirectory() { - if (existsSync(this.testDir)) { - rmSync(this.testDir, { recursive: true, force: true }); - } - mkdirSync(this.testDir, { recursive: true }); - mkdirSync(join(this.testDir, 'zenstack'), { recursive: true }); - - // Create package.json - writeFileSync( - join(this.testDir, 'package.json'), - JSON.stringify( - { - name: 'zenstack-validation-test', - version: '1.0.0', - private: true, - }, - null, - 2, - ), - ); - } - - public runValidation(schema: string): ValidationResult { - this.setupTestDirectory(); - writeFileSync(this.schemaPath, schema); - - try { - execSync(`node ${this.cliPath} generate`, { - cwd: this.testDir, - stdio: 'pipe', - encoding: 'utf8', - }); - - return { - success: true, - errors: [], - }; - } catch (error: any) { - return { - success: false, - errors: this.extractErrors(error.stderr), - }; - } - } - - private extractErrors(output: string): string[] { - const lines = output.split('\n'); - const errors: string[] = []; - - for (const line of lines) { - if (line.includes('Error:') || line.includes('error:') || line.includes('✖')) { - errors.push(line.trim()); - } - } - - return errors; - } - - public cleanup() { - if (existsSync(this.testDir)) { - rmSync(this.testDir, { recursive: true, force: true }); - } - } -} - -describe('ZenStack validation consistency with Prisma', () => { - let tester: ZenStackValidationTester; - let tempDir: string; - - beforeEach(() => { - tempDir = join(tmpdir(), 'zenstack-validation-test-' + randomUUID()); - tester = new ZenStackValidationTester(tempDir); - }); - - afterEach(() => { - tester.cleanup(); - }); - - describe('basic_models', () => { - it('should accept valid basic model with id field', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - name String? -} - `); - - expect(result.success).toBe(true); - }); - - it('should reject model without any unique criterion', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - email String - name String? -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject model with multiple @id fields', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @id - name String? -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject model with both @id field and @@id', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - firstName String - lastName String - - @@id([firstName, lastName]) -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject optional ID field', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int? @id @default(autoincrement()) - email String @unique -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject array ID field', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int[] @id - email String @unique -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - }); - - describe('compound_ids', () => { - it('should accept valid compound ID with @@id', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - firstName String - lastName String - age Int - - @@id([firstName, lastName]) -} - `); - - expect(result.success).toBe(true); - }); - - it('should reject empty compound ID', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - firstName String - lastName String - - @@id([]) -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - }); - - describe('field_types', () => { - it('should reject optional array field', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - tags String[]? -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject array field with SQLite', () => { - const result = tester.runValidation(` -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -model User { - id Int @id @default(autoincrement()) - tags String[] -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should accept array field with PostgreSQL', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - tags String[] -} - `); - - expect(result.success).toBe(true); - }); - }); - - describe('relations_one_to_one', () => { - it('should accept valid one-to-one relation', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - profile Profile? -} - -model Profile { - id Int @id @default(autoincrement()) - bio String - user User @relation(fields: [userId], references: [id]) - userId Int @unique -} - `); - - expect(result.success).toBe(true); - }); - - it('should reject one-to-one relation without @unique on FK', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - profile Profile? -} - -model Profile { - id Int @id @default(autoincrement()) - bio String - user User @relation(fields: [userId], references: [id]) - userId Int -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject one-to-one relation missing opposite field', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - profile Profile? -} - -model Profile { - id Int @id @default(autoincrement()) - bio String - userId Int @unique -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject one-to-one with both sides required', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - profile Profile -} - -model Profile { - id Int @id @default(autoincrement()) - bio String - user User @relation(fields: [userId], references: [id]) - userId Int @unique -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - }); - - describe('relations_one_to_many', () => { - it('should accept valid one-to-many relation', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int -} - `); - - expect(result.success).toBe(true); - }); - - it('should reject one-to-many without @relation annotation', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User - authorId Int -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject one-to-many relation referencing non-existent FK field', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - }); - - describe('relations_many_to_many', () => { - it('should accept valid implicit many-to-many relation', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - authors User[] -} - `); - - expect(result.success).toBe(true); - }); - - it('should accept valid explicit many-to-many relation', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts PostUser[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - authors PostUser[] -} - -model PostUser { - user User @relation(fields: [userId], references: [id]) - post Post @relation(fields: [postId], references: [id]) - userId Int - postId Int - - @@id([userId, postId]) -} - `); - - expect(result.success).toBe(true); - }); - - it('should reject implicit many-to-many with explicit @relation', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] @relation(fields: [id], references: [id]) -} - -model Post { - id Int @id @default(autoincrement()) - title String - authors User[] -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - }); - - describe('relations_self', () => { - it('should accept valid self relation with proper naming', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - manager User? @relation("UserManager", fields: [managerId], references: [id]) - managerId Int? - employees User[] @relation("UserManager") -} - `); - - expect(result.success).toBe(true); - }); - - it('should reject self relation without relation name', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - manager User? @relation(fields: [managerId], references: [id]) - managerId Int? - employees User[] -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should accept self many-to-many relation', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - following User[] @relation("UserFollows") - followers User[] @relation("UserFollows") -} - `); - - expect(result.success).toBe(true); - }); - }); - - describe('relation_validation', () => { - it('should reject mismatched length of fields and references arrays', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id, email]) - authorId Int -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject empty fields array', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [], references: [id]) - authorId Int -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject empty references array', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: []) - authorId Int -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject partial relation specification with only fields', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId]) - authorId Int -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject partial relation specification with only references', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(references: [id]) - authorId Int -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject both sides of relation with fields/references', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] @relation(fields: [id], references: [authorId]) -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject type mismatch between fields and references', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id String @id @default(cuid()) - email String @unique - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - }); - - describe('unique_constraints', () => { - it('should accept valid compound unique constraint', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - firstName String - lastName String - email String @unique - - @@unique([firstName, lastName]) -} - `); - - expect(result.success).toBe(true); - }); - - it('should reject empty unique constraint', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - firstName String - lastName String - - @@unique([]) -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should accept unique constraint on optional field', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String? @unique - name String -} - `); - - expect(result.success).toBe(true); - }); - }); - - describe('enums', () => { - it('should accept valid enum definition and usage', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -enum Role { - USER - ADMIN - MODERATOR -} - -model User { - id Int @id @default(autoincrement()) - role Role @default(USER) - name String -} - `); - - expect(result.success).toBe(true); - }); - - it('should reject empty enum', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -enum Role { -} - -model User { - id Int @id @default(autoincrement()) - role Role @default(USER) - name String -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - }); - - describe('datasource', () => { - it('should reject multiple datasources', () => { - const result = tester.runValidation(` -datasource db1 { - provider = "postgresql" - url = env("DATABASE_URL") -} - -datasource db2 { - provider = "sqlite" - url = "file:./dev.db" -} - -model User { - id Int @id @default(autoincrement()) - name String -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject missing datasource', () => { - const result = tester.runValidation(` -model User { - id Int @id @default(autoincrement()) - name String -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject invalid provider', () => { - const result = tester.runValidation(` -datasource db { - provider = "nosql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - name String -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - }); - - describe('attributes', () => { - it('should reject duplicate field attributes', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique @unique - name String -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should reject invalid default value type', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @default(123) - name String -} - `); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should accept valid @map attribute', () => { - const result = tester.runValidation(` -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique @map("email_address") - name String - - @@map("users") -} - `); - - expect(result.success).toBe(true); - }); - }); -});