Skip to content

Commit 8df3f88

Browse files
authored
feat: cli validate command (#103)
1 parent 6ad973d commit 8df3f88

File tree

4 files changed

+132
-2
lines changed

4 files changed

+132
-2
lines changed

packages/cli/src/actions/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ import { run as generate } from './generate';
33
import { run as info } from './info';
44
import { run as init } from './init';
55
import { run as migrate } from './migrate';
6+
import { run as validate } from './validate';
67

7-
export { db, generate, info, init, migrate };
8+
export { db, generate, info, init, migrate, validate };
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import colors from 'colors';
2+
import { getSchemaFile, loadSchemaDocument } from './action-utils';
3+
4+
type Options = {
5+
schema?: string;
6+
};
7+
8+
/**
9+
* CLI action for validating schema without generation
10+
*/
11+
export async function run(options: Options) {
12+
const schemaFile = getSchemaFile(options.schema);
13+
14+
try {
15+
await loadSchemaDocument(schemaFile);
16+
console.log(colors.green('✓ Schema validation completed successfully.'));
17+
} catch (error) {
18+
console.error(colors.red('✗ Schema validation failed.'));
19+
// Re-throw to maintain CLI exit code behavior
20+
throw error;
21+
}
22+
}

packages/cli/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const initAction = async (projectPath: string): Promise<void> => {
2424
await actions.init(projectPath);
2525
};
2626

27+
const validateAction = async (options: Parameters<typeof actions.validate>[0]): Promise<void> => {
28+
await actions.validate(options);
29+
};
30+
2731
export function createProgram() {
2832
const program = new Command('zenstack');
2933

@@ -35,7 +39,7 @@ export function createProgram() {
3539
.description(
3640
`${colors.bold.blue(
3741
'ζ',
38-
)} ZenStack is a Prisma power pack for building full-stack apps.\n\nDocumentation: https://zenstack.dev.`,
42+
)} ZenStack is a database access toolkit for TypeScript apps.\n\nDocumentation: https://zenstack.dev.`,
3943
)
4044
.showHelpAfterError()
4145
.showSuggestionAfterError();
@@ -115,6 +119,8 @@ export function createProgram() {
115119
.argument('[path]', 'project path', '.')
116120
.action(initAction);
117121

122+
program.command('validate').description('Validate a ZModel schema.').addOption(schemaOption).action(validateAction);
123+
118124
return program;
119125
}
120126

packages/cli/test/validate.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { describe, expect, it } from 'vitest';
4+
import { createProject, runCli } from './utils';
5+
6+
const validModel = `
7+
model User {
8+
id String @id @default(cuid())
9+
email String @unique
10+
name String?
11+
posts Post[]
12+
}
13+
14+
model Post {
15+
id String @id @default(cuid())
16+
title String
17+
content String?
18+
author User @relation(fields: [authorId], references: [id])
19+
authorId String
20+
}
21+
`;
22+
23+
const invalidModel = `
24+
model User {
25+
id String @id @default(cuid())
26+
email String @unique
27+
posts Post[]
28+
}
29+
30+
model Post {
31+
id String @id @default(cuid())
32+
title String
33+
author User @relation(fields: [authorId], references: [id])
34+
// Missing authorId field - should cause validation error
35+
}
36+
`;
37+
38+
describe('CLI validate command test', () => {
39+
it('should validate a valid schema successfully', () => {
40+
const workDir = createProject(validModel);
41+
42+
// Should not throw an error
43+
expect(() => runCli('validate', workDir)).not.toThrow();
44+
});
45+
46+
it('should fail validation for invalid schema', () => {
47+
const workDir = createProject(invalidModel);
48+
49+
// Should throw an error due to validation failure
50+
expect(() => runCli('validate', workDir)).toThrow();
51+
});
52+
53+
it('should respect custom schema location', () => {
54+
const workDir = createProject(validModel);
55+
fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'zenstack/custom.zmodel'));
56+
57+
// Should not throw an error when using custom schema path
58+
expect(() => runCli('validate --schema ./zenstack/custom.zmodel', workDir)).not.toThrow();
59+
});
60+
61+
it('should fail when schema file does not exist', () => {
62+
const workDir = createProject(validModel);
63+
64+
// Should throw an error when schema file doesn't exist
65+
expect(() => runCli('validate --schema ./nonexistent.zmodel', workDir)).toThrow();
66+
});
67+
68+
it('should respect package.json config', () => {
69+
const workDir = createProject(validModel);
70+
fs.mkdirSync(path.join(workDir, 'foo'));
71+
fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'foo/schema.zmodel'));
72+
fs.rmdirSync(path.join(workDir, 'zenstack'));
73+
74+
const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8'));
75+
pkgJson.zenstack = {
76+
schema: './foo/schema.zmodel',
77+
};
78+
fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
79+
80+
// Should not throw an error when using package.json config
81+
expect(() => runCli('validate', workDir)).not.toThrow();
82+
});
83+
84+
it('should validate schema with syntax errors', () => {
85+
const modelWithSyntaxError = `
86+
datasource db {
87+
provider = "sqlite"
88+
url = "file:./dev.db"
89+
}
90+
91+
model User {
92+
id String @id @default(cuid())
93+
email String @unique
94+
// Missing closing brace - syntax error
95+
`;
96+
const workDir = createProject(modelWithSyntaxError, false);
97+
98+
// Should throw an error due to syntax error
99+
expect(() => runCli('validate', workDir)).toThrow();
100+
});
101+
});

0 commit comments

Comments
 (0)