From 4555a590475f73e9ccb59ba153f5a98ca0ef2395 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:22:57 +0800 Subject: [PATCH 1/6] chore: misc changes --- .../cli/src/actions/{validate.ts => check.ts} | 2 +- packages/cli/src/actions/generate.ts | 7 +- packages/cli/src/actions/index.ts | 4 +- packages/cli/src/index.ts | 29 ++++--- packages/language/res/stdlib.zmodel | 81 ++----------------- packages/runtime/src/client/client-impl.ts | 2 +- .../src/client/crud/operations/base.ts | 4 - .../executor/zenstack-query-executor.ts | 4 - packages/runtime/src/client/options.ts | 5 -- 9 files changed, 28 insertions(+), 110 deletions(-) rename packages/cli/src/actions/{validate.ts => check.ts} (91%) diff --git a/packages/cli/src/actions/validate.ts b/packages/cli/src/actions/check.ts similarity index 91% rename from packages/cli/src/actions/validate.ts rename to packages/cli/src/actions/check.ts index 8ae9932c..a4582aac 100644 --- a/packages/cli/src/actions/validate.ts +++ b/packages/cli/src/actions/check.ts @@ -6,7 +6,7 @@ type Options = { }; /** - * CLI action for validating schema without generation + * CLI action for checks a schema's validity. */ export async function run(options: Options) { const schemaFile = getSchemaFile(options.schema); diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index b70c0cda..f0041c3d 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -12,7 +12,6 @@ import { getPkgJsonConfig, getSchemaFile, loadSchemaDocument } from './action-ut type Options = { schema?: string; output?: string; - silent?: boolean; }; /** @@ -28,9 +27,8 @@ export async function run(options: Options) { await runPlugins(schemaFile, model, outputPath); - if (!options.silent) { - console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.\n`)); - console.log(`You can now create a ZenStack client with it. + console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.\n`)); + console.log(`You can now create a ZenStack client with it. \`\`\`ts import { ZenStackClient } from '@zenstackhq/runtime'; @@ -40,7 +38,6 @@ const client = new ZenStackClient(schema, { dialect: { ... } }); \`\`\``); - } } function getOutputPath(options: Options, schemaFile: string) { diff --git a/packages/cli/src/actions/index.ts b/packages/cli/src/actions/index.ts index c8ce5ed9..f2238829 100644 --- a/packages/cli/src/actions/index.ts +++ b/packages/cli/src/actions/index.ts @@ -3,6 +3,6 @@ import { run as generate } from './generate'; import { run as info } from './info'; import { run as init } from './init'; import { run as migrate } from './migrate'; -import { run as validate } from './validate'; +import { run as check } from './check'; -export { db, generate, info, init, migrate, validate }; +export { db, generate, info, init, migrate, check }; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a275800d..7b17a37e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -25,8 +25,8 @@ const initAction = async (projectPath: string): Promise => { await actions.init(projectPath); }; -const validateAction = async (options: Parameters[0]): Promise => { - await actions.validate(options); +const checkAction = async (options: Parameters[0]): Promise => { + await actions.check(options); }; export function createProgram() { @@ -40,26 +40,25 @@ export function createProgram() { .description( `${colors.bold.blue( 'ΞΆ', - )} ZenStack is a database access toolkit for TypeScript apps.\n\nDocumentation: https://zenstack.dev.`, + )} ZenStack is the data layer for modern TypeScript apps.\n\nDocumentation: https://zenstack.dev.`, ) .showHelpAfterError() .showSuggestionAfterError(); const schemaOption = new Option( '--schema ', - `schema file (with extension ${schemaExtensions}). Defaults to "schema.zmodel" unless specified in package.json.`, + `schema file (with extension ${schemaExtensions}). Defaults to "zenstack/schema.zmodel" unless specified in package.json.`, ); program .command('generate') - .description('Run code generation.') + .description('Run code generation plugins.') .addOption(schemaOption) - .addOption(new Option('--silent', 'do not print any output')) - .addOption(new Option('-o, --output ', 'default output directory for core plugins')) + .addOption(new Option('-o, --output ', 'default output directory for code generation')) .action(generateAction); - const migrateCommand = program.command('migrate').description('Update the database schema with migrations.'); - const migrationsOption = new Option('--migrations ', 'path for migrations'); + const migrateCommand = program.command('migrate').description('Run database schema migration related tasks.'); + const migrationsOption = new Option('--migrations ', 'path that contains the "migrations" directory'); migrateCommand .command('dev') @@ -98,14 +97,14 @@ export function createProgram() { .addOption(migrationsOption) .addOption(new Option('--applied ', 'record a specific migration as applied')) .addOption(new Option('--rolled-back ', 'record a specific migration as rolled back')) - .description('Resolve issues with database migrations in deployment databases') + .description('Resolve issues with database migrations in deployment databases.') .action((options) => migrateAction('resolve', options)); const dbCommand = program.command('db').description('Manage your database schema during development.'); dbCommand .command('push') - .description('Push the state from your schema to your database') + .description('Push the state from your schema to your database.') .addOption(schemaOption) .addOption(new Option('--accept-data-loss', 'ignore data loss warnings')) .addOption(new Option('--force-reset', 'force a reset of the database before push')) @@ -113,7 +112,7 @@ export function createProgram() { program .command('info') - .description('Get information of installed ZenStack and related packages.') + .description('Get information of installed ZenStack packages.') .argument('[path]', 'project path', '.') .action(infoAction); @@ -123,7 +122,11 @@ export function createProgram() { .argument('[path]', 'project path', '.') .action(initAction); - program.command('validate').description('Validate a ZModel schema.').addOption(schemaOption).action(validateAction); + program + .command('check') + .description('Check a ZModel schema for syntax or semantic errors.') + .addOption(schemaOption) + .action(checkAction); return program; } diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index 8f91957f..0d8c4264 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -424,25 +424,12 @@ attribute @@fulltext(_ fields: FieldReference[], map: String?) @@@prisma // String type modifiers -enum MSSQLServerTypes { - Max -} - -attribute @db.String(_ x: Int?) @@@targetField([StringField]) @@@prisma attribute @db.Text() @@@targetField([StringField]) @@@prisma -attribute @db.NText() @@@targetField([StringField]) @@@prisma attribute @db.Char(_ x: Int?) @@@targetField([StringField]) @@@prisma -attribute @db.NChar(_ x: Int?) @@@targetField([StringField]) @@@prisma attribute @db.VarChar(_ x: Any?) @@@targetField([StringField]) @@@prisma -attribute @db.NVarChar(_ x: Any?) @@@targetField([StringField]) @@@prisma -attribute @db.CatalogSingleChar() @@@targetField([StringField]) @@@prisma -attribute @db.TinyText() @@@targetField([StringField]) @@@prisma -attribute @db.MediumText() @@@targetField([StringField]) @@@prisma -attribute @db.LongText() @@@targetField([StringField]) @@@prisma attribute @db.Bit(_ x: Int?) @@@targetField([StringField, BooleanField, BytesField]) @@@prisma attribute @db.VarBit(_ x: Int?) @@@targetField([StringField]) @@@prisma attribute @db.Uuid() @@@targetField([StringField]) @@@prisma -attribute @db.UniqueIdentifier() @@@targetField([StringField]) @@@prisma attribute @db.Xml() @@@targetField([StringField]) @@@prisma attribute @db.Inet() @@@targetField([StringField]) @@@prisma attribute @db.Citext() @@@targetField([StringField]) @@@prisma @@ -450,8 +437,6 @@ attribute @db.Citext() @@@targetField([StringField]) @@@prisma // Boolean type modifiers attribute @db.Boolean() @@@targetField([BooleanField]) @@@prisma -attribute @db.TinyInt(_ x: Int?) @@@targetField([BooleanField, IntField]) @@@prisma -attribute @db.Bool() @@@targetField([BooleanField]) @@@prisma // Int type modifiers @@ -459,38 +444,19 @@ attribute @db.Int() @@@targetField([IntField]) @@@prisma attribute @db.Integer() @@@targetField([IntField]) @@@prisma attribute @db.SmallInt() @@@targetField([IntField]) @@@prisma attribute @db.Oid() @@@targetField([IntField]) @@@prisma -attribute @db.UnsignedInt() @@@targetField([IntField]) @@@prisma -attribute @db.UnsignedSmallInt() @@@targetField([IntField]) @@@prisma -attribute @db.MediumInt() @@@targetField([IntField]) @@@prisma -attribute @db.UnsignedMediumInt() @@@targetField([IntField]) @@@prisma -attribute @db.UnsignedTinyInt() @@@targetField([IntField]) @@@prisma -attribute @db.Year() @@@targetField([IntField]) @@@prisma -attribute @db.Int4() @@@targetField([IntField]) @@@prisma -attribute @db.Int2() @@@targetField([IntField]) @@@prisma // BigInt type modifiers attribute @db.BigInt() @@@targetField([BigIntField]) @@@prisma -attribute @db.UnsignedBigInt() @@@targetField([BigIntField]) @@@prisma -attribute @db.Int8() @@@targetField([BigIntField]) @@@prisma // Float/Decimal type modifiers attribute @db.DoublePrecision() @@@targetField([FloatField, DecimalField]) @@@prisma attribute @db.Real() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Float() @@@targetField([FloatField, DecimalField]) @@@prisma attribute @db.Decimal(_ p: Int?, _ s: Int?) @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Double() @@@targetField([FloatField, DecimalField]) @@@prisma attribute @db.Money() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.SmallMoney() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Float8() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Float4() @@@targetField([FloatField, DecimalField]) @@@prisma // DateTime type modifiers -attribute @db.DateTime(_ x: Int?) @@@targetField([DateTimeField]) @@@prisma -attribute @db.DateTime2() @@@targetField([DateTimeField]) @@@prisma -attribute @db.SmallDateTime() @@@targetField([DateTimeField]) @@@prisma -attribute @db.DateTimeOffset() @@@targetField([DateTimeField]) @@@prisma attribute @db.Timestamp(_ x: Int?) @@@targetField([DateTimeField]) @@@prisma attribute @db.Timestamptz(_ x: Int?) @@@targetField([DateTimeField]) @@@prisma attribute @db.Date() @@@targetField([DateTimeField]) @@@prisma @@ -504,49 +470,14 @@ attribute @db.JsonB() @@@targetField([JsonField]) @@@prisma // Bytes type modifiers -attribute @db.Bytes() @@@targetField([BytesField]) @@@prisma attribute @db.ByteA() @@@targetField([BytesField]) @@@prisma -attribute @db.LongBlob() @@@targetField([BytesField]) @@@prisma -attribute @db.Binary() @@@targetField([BytesField]) @@@prisma -attribute @db.VarBinary(_ x: Int?) @@@targetField([BytesField]) @@@prisma -attribute @db.TinyBlob() @@@targetField([BytesField]) @@@prisma -attribute @db.Blob() @@@targetField([BytesField]) @@@prisma -attribute @db.MediumBlob() @@@targetField([BytesField]) @@@prisma -attribute @db.Image() @@@targetField([BytesField]) @@@prisma - -/** - * Specifies the schema to use in a multi-schema database. https://www.prisma.io/docs/guides/database/multi-schema. - * - * @param: The name of the database schema. - */ -attribute @@schema(_ name: String) @@@prisma -/** - * Indicates that the field is a password field and needs to be hashed before persistence. - * - * ZenStack uses `bcryptjs` library to hash password. You can use the `saltLength` parameter - * to configure the cost of hashing, or use `salt` parameter to provide an explicit salt. - * By default, salt length of 12 is used. - * - * @see https://www.npmjs.com/package/bcryptjs for details - * - * @param saltLength: length of salt to use (cost factor for the hash function) - * @param salt: salt to use (a pregenerated valid salt) - */ -attribute @password(saltLength: Int?, salt: String?) @@@targetField([StringField]) - - -/** - * Indicates that the field is encrypted when storing in the DB and should be decrypted when read - * - * ZenStack uses the Web Crypto API to encrypt and decrypt the field. - */ -attribute @encrypted() @@@targetField([StringField]) - -/** - * Indicates that the field should be omitted when read from the generated services. - */ -attribute @omit() +// /** +// * Specifies the schema to use in a multi-schema database. https://www.prisma.io/docs/guides/database/multi-schema. +// * +// * @param: The name of the database schema. +// */ +// attribute @@schema(_ name: String) @@@prisma ////////////////////////////////////////////// // Begin validation attributes and functions diff --git a/packages/runtime/src/client/client-impl.ts b/packages/runtime/src/client/client-impl.ts index be1f2dff..e86e91c8 100644 --- a/packages/runtime/src/client/client-impl.ts +++ b/packages/runtime/src/client/client-impl.ts @@ -35,7 +35,7 @@ import type { ToKysely } from './query-builder'; import { ResultProcessor } from './result-processor'; /** - * ZenStack client. + * ZenStack ORM client. */ export const ZenStackClient = function ( this: any, diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index 19fca142..e6f5a034 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -11,7 +11,6 @@ import { type SelectQueryBuilder, } from 'kysely'; import { nanoid } from 'nanoid'; -import { inspect } from 'node:util'; import { match } from 'ts-pattern'; import { ulid } from 'ulid'; import * as uuid from 'uuid'; @@ -176,9 +175,6 @@ export abstract class BaseOperationHandler { result = r.rows; } catch (err) { let message = `Failed to execute query: ${err}, sql: ${compiled.sql}`; - if (this.options.debug) { - message += `, parameters: \n${compiled.parameters.map((p) => inspect(p)).join('\n')}`; - } throw new QueryError(message, err); } diff --git a/packages/runtime/src/client/executor/zenstack-query-executor.ts b/packages/runtime/src/client/executor/zenstack-query-executor.ts index 8a8496f8..d578063b 100644 --- a/packages/runtime/src/client/executor/zenstack-query-executor.ts +++ b/packages/runtime/src/client/executor/zenstack-query-executor.ts @@ -20,7 +20,6 @@ import { type TableNode, } from 'kysely'; import { nanoid } from 'nanoid'; -import { inspect } from 'node:util'; import { match } from 'ts-pattern'; import type { GetModels, SchemaDef } from '../../schema'; import { type ClientImpl } from '../client-impl'; @@ -161,9 +160,6 @@ export class ZenStackQueryExecutor extends DefaultQuer }); } catch (err) { let message = `Failed to execute query: ${err}, sql: ${compiled.sql}`; - if (this.options.debug) { - message += `, parameters: \n${compiled.parameters.map((p) => inspect(p)).join('\n')}`; - } throw new QueryError(message, err); } } diff --git a/packages/runtime/src/client/options.ts b/packages/runtime/src/client/options.ts index 5e412e5b..3146a402 100644 --- a/packages/runtime/src/client/options.ts +++ b/packages/runtime/src/client/options.ts @@ -41,11 +41,6 @@ export type ClientOptions = { * Logging configuration. */ log?: KyselyConfig['log']; - - /** - * Debug mode. - */ - debug?: boolean; } & (HasComputedFields extends true ? { /** From 57348470997ba5882a74fb6928d61e7ee8151578 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:34:46 +0800 Subject: [PATCH 2/6] update --- packages/cli/src/actions/check.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/actions/check.ts b/packages/cli/src/actions/check.ts index a4582aac..10f06314 100644 --- a/packages/cli/src/actions/check.ts +++ b/packages/cli/src/actions/check.ts @@ -6,7 +6,7 @@ type Options = { }; /** - * CLI action for checks a schema's validity. + * CLI action for checking a schema's validity. */ export async function run(options: Options) { const schemaFile = getSchemaFile(options.schema); From 560c1b9c9d64c0ac75dd987798a9efff17c494e7 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:39:42 +0800 Subject: [PATCH 3/6] fix tests --- packages/runtime/test/schemas/todo/schema.ts | 3 +-- packages/runtime/test/schemas/todo/todo.zmodel | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/runtime/test/schemas/todo/schema.ts b/packages/runtime/test/schemas/todo/schema.ts index 25a8290c..14ef60d1 100644 --- a/packages/runtime/test/schemas/todo/schema.ts +++ b/packages/runtime/test/schemas/todo/schema.ts @@ -180,8 +180,7 @@ export const schema = { password: { name: "password", type: "String", - optional: true, - attributes: [{ name: "@password" }, { name: "@omit" }] + optional: true }, emailVerified: { name: "emailVerified", diff --git a/packages/runtime/test/schemas/todo/todo.zmodel b/packages/runtime/test/schemas/todo/todo.zmodel index 0adb21f6..d91ed34a 100644 --- a/packages/runtime/test/schemas/todo/todo.zmodel +++ b/packages/runtime/test/schemas/todo/todo.zmodel @@ -70,7 +70,7 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt email String @unique @email - password String? @password @omit + password String? emailVerified DateTime? name String? bio String? @ignore From 2c2b60fa8573a144688c48db1d26b6c498690840 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:45:46 +0800 Subject: [PATCH 4/6] fix lint --- packages/runtime/src/client/crud/operations/base.ts | 2 +- packages/runtime/src/client/executor/zenstack-query-executor.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index e6f5a034..80954f67 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -174,7 +174,7 @@ export abstract class BaseOperationHandler { const r = await kysely.getExecutor().executeQuery(compiled, queryId); result = r.rows; } catch (err) { - let message = `Failed to execute query: ${err}, sql: ${compiled.sql}`; + const message = `Failed to execute query: ${err}, sql: ${compiled.sql}`; throw new QueryError(message, err); } diff --git a/packages/runtime/src/client/executor/zenstack-query-executor.ts b/packages/runtime/src/client/executor/zenstack-query-executor.ts index d578063b..a7b55977 100644 --- a/packages/runtime/src/client/executor/zenstack-query-executor.ts +++ b/packages/runtime/src/client/executor/zenstack-query-executor.ts @@ -159,7 +159,7 @@ export class ZenStackQueryExecutor extends DefaultQuer return { result, connection }; }); } catch (err) { - let message = `Failed to execute query: ${err}, sql: ${compiled.sql}`; + const message = `Failed to execute query: ${err}, sql: ${compiled.sql}`; throw new QueryError(message, err); } } From fe6c99977a0ed9335413a08545977905baf2fea7 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:56:36 +0800 Subject: [PATCH 5/6] update test --- packages/cli/test/check.test.ts | 101 ++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 packages/cli/test/check.test.ts diff --git a/packages/cli/test/check.test.ts b/packages/cli/test/check.test.ts new file mode 100644 index 00000000..287bb6b8 --- /dev/null +++ b/packages/cli/test/check.test.ts @@ -0,0 +1,101 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { createProject, runCli } from './utils'; + +const validModel = ` +model User { + id String @id @default(cuid()) + email String @unique + name String? + posts Post[] +} + +model Post { + id String @id @default(cuid()) + title String + content String? + author User @relation(fields: [authorId], references: [id]) + authorId String +} +`; + +const invalidModel = ` +model User { + id String @id @default(cuid()) + email String @unique + posts Post[] +} + +model Post { + id String @id @default(cuid()) + title String + author User @relation(fields: [authorId], references: [id]) + // Missing authorId field - should cause validation error +} +`; + +describe('CLI validate command test', () => { + it('should validate a valid schema successfully', () => { + const workDir = createProject(validModel); + + // Should not throw an error + expect(() => runCli('check', workDir)).not.toThrow(); + }); + + it('should fail validation for invalid schema', () => { + const workDir = createProject(invalidModel); + + // Should throw an error due to validation failure + expect(() => runCli('check', workDir)).toThrow(); + }); + + it('should respect custom schema location', () => { + const workDir = createProject(validModel); + fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'zenstack/custom.zmodel')); + + // Should not throw an error when using custom schema path + expect(() => runCli('check --schema ./zenstack/custom.zmodel', workDir)).not.toThrow(); + }); + + it('should fail when schema file does not exist', () => { + const workDir = createProject(validModel); + + // Should throw an error when schema file doesn't exist + expect(() => runCli('check --schema ./nonexistent.zmodel', workDir)).toThrow(); + }); + + it('should respect package.json config', () => { + const workDir = createProject(validModel); + fs.mkdirSync(path.join(workDir, 'foo')); + fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'foo/schema.zmodel')); + fs.rmdirSync(path.join(workDir, 'zenstack')); + + const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8')); + pkgJson.zenstack = { + schema: './foo/schema.zmodel', + }; + fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify(pkgJson, null, 2)); + + // Should not throw an error when using package.json config + expect(() => runCli('check', workDir)).not.toThrow(); + }); + + it('should validate schema with syntax errors', () => { + const modelWithSyntaxError = ` +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +model User { + id String @id @default(cuid()) + email String @unique + // Missing closing brace - syntax error + `; + const workDir = createProject(modelWithSyntaxError, false); + + // Should throw an error due to syntax error + expect(() => runCli('check', workDir)).toThrow(); + }); +}); From c479520cb479b559c6f2617c4c71b42043e8a753 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:03:35 +0800 Subject: [PATCH 6/6] update --- packages/cli/test/validate.test.ts | 101 ----------------------------- 1 file changed, 101 deletions(-) delete mode 100644 packages/cli/test/validate.test.ts diff --git a/packages/cli/test/validate.test.ts b/packages/cli/test/validate.test.ts deleted file mode 100644 index 5c7ec61e..00000000 --- a/packages/cli/test/validate.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { createProject, runCli } from './utils'; - -const validModel = ` -model User { - id String @id @default(cuid()) - email String @unique - name String? - posts Post[] -} - -model Post { - id String @id @default(cuid()) - title String - content String? - author User @relation(fields: [authorId], references: [id]) - authorId String -} -`; - -const invalidModel = ` -model User { - id String @id @default(cuid()) - email String @unique - posts Post[] -} - -model Post { - id String @id @default(cuid()) - title String - author User @relation(fields: [authorId], references: [id]) - // Missing authorId field - should cause validation error -} -`; - -describe('CLI validate command test', () => { - it('should validate a valid schema successfully', () => { - const workDir = createProject(validModel); - - // Should not throw an error - expect(() => runCli('validate', workDir)).not.toThrow(); - }); - - it('should fail validation for invalid schema', () => { - const workDir = createProject(invalidModel); - - // Should throw an error due to validation failure - expect(() => runCli('validate', workDir)).toThrow(); - }); - - it('should respect custom schema location', () => { - const workDir = createProject(validModel); - fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'zenstack/custom.zmodel')); - - // Should not throw an error when using custom schema path - expect(() => runCli('validate --schema ./zenstack/custom.zmodel', workDir)).not.toThrow(); - }); - - it('should fail when schema file does not exist', () => { - const workDir = createProject(validModel); - - // Should throw an error when schema file doesn't exist - expect(() => runCli('validate --schema ./nonexistent.zmodel', workDir)).toThrow(); - }); - - it('should respect package.json config', () => { - const workDir = createProject(validModel); - fs.mkdirSync(path.join(workDir, 'foo')); - fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'foo/schema.zmodel')); - fs.rmdirSync(path.join(workDir, 'zenstack')); - - const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8')); - pkgJson.zenstack = { - schema: './foo/schema.zmodel', - }; - fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify(pkgJson, null, 2)); - - // Should not throw an error when using package.json config - expect(() => runCli('validate', workDir)).not.toThrow(); - }); - - it('should validate schema with syntax errors', () => { - const modelWithSyntaxError = ` -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -model User { - id String @id @default(cuid()) - email String @unique - // Missing closing brace - syntax error - `; - const workDir = createProject(modelWithSyntaxError, false); - - // Should throw an error due to syntax error - expect(() => runCli('validate', workDir)).toThrow(); - }); -});