Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/cli/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
22 changes: 22 additions & 0 deletions packages/cli/src/actions/validate.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
8 changes: 7 additions & 1 deletion packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const initAction = async (projectPath: string): Promise<void> => {
await actions.init(projectPath);
};

const validateAction = async (options: Parameters<typeof actions.validate>[0]): Promise<void> => {
await actions.validate(options);
};

export function createProgram() {
const program = new Command('zenstack');

Expand All @@ -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();
Expand Down Expand Up @@ -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;
}

Expand Down
101 changes: 101 additions & 0 deletions packages/cli/test/validate.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});