diff --git a/packages/cli/src/actions/index.ts b/packages/cli/src/actions/index.ts index a30763bb..c8ce5ed9 100644 --- a/packages/cli/src/actions/index.ts +++ b/packages/cli/src/actions/index.ts @@ -3,5 +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'; -export { db, generate, info, init, migrate }; +export { db, generate, info, init, migrate, validate }; diff --git a/packages/cli/src/actions/validate.ts b/packages/cli/src/actions/validate.ts new file mode 100644 index 00000000..c04f9b11 --- /dev/null +++ b/packages/cli/src/actions/validate.ts @@ -0,0 +1,22 @@ +import colors from 'colors'; +import { getSchemaFile, loadSchemaDocument } from './action-utils'; + +type Options = { + schema?: string; +}; + +/** + * CLI action for validating schema without generation + */ +export async function run(options: Options) { + const schemaFile = getSchemaFile(options.schema); + + try { + await loadSchemaDocument(schemaFile); + console.log(colors.green('✓ Schema validation completed successfully.')); + } catch (error) { + console.error(colors.red('✗ Schema validation failed.')); + // Re-throw to maintain CLI exit code behavior + throw error; + } +} \ No newline at end of file diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index d75cff32..5e16d0b2 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -24,6 +24,10 @@ const initAction = async (projectPath: string): Promise => { await actions.init(projectPath); }; +const validateAction = async (options: Parameters[0]): Promise => { + await actions.validate(options); +}; + export function createProgram() { const program = new Command('zenstack'); @@ -35,7 +39,7 @@ export function createProgram() { .description( `${colors.bold.blue( 'ζ', - )} ZenStack is a Prisma power pack for building full-stack apps.\n\nDocumentation: https://zenstack.dev.`, + )} ZenStack is a database access toolkit for TypeScript apps.\n\nDocumentation: https://zenstack.dev.`, ) .showHelpAfterError() .showSuggestionAfterError(); @@ -115,6 +119,8 @@ export function createProgram() { .argument('[path]', 'project path', '.') .action(initAction); + program.command('validate').description('Validate a ZModel schema.').addOption(schemaOption).action(validateAction); + return program; } diff --git a/packages/cli/test/validate.test.ts b/packages/cli/test/validate.test.ts new file mode 100644 index 00000000..5c7ec61e --- /dev/null +++ b/packages/cli/test/validate.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('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(); + }); +});