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
1 change: 0 additions & 1 deletion .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,3 @@ jobs:

${{ steps.changelog.outputs.changelog }}
draft: true
prerelease: true
6 changes: 4 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
"pack": "pnpm pack"
},
"dependencies": {
"@zenstackhq/common-helpers": "workspace:*",
"@zenstackhq/language": "workspace:*",
"@zenstackhq/sdk": "workspace:*",
"@zenstackhq/common-helpers": "workspace:*",
"colors": "1.4.0",
"commander": "^8.3.0",
"langium": "catalog:",
Expand All @@ -43,10 +43,12 @@
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/tmp": "^0.2.6",
"@zenstackhq/eslint-config": "workspace:*",
"@zenstackhq/runtime": "workspace:*",
"@zenstackhq/testtools": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*",
"better-sqlite3": "^11.8.1"
"better-sqlite3": "^11.8.1",
"tmp": "^0.2.3"
}
}
14 changes: 12 additions & 2 deletions packages/cli/src/actions/action-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import fs from 'node:fs';
import { CliError } from '../cli-error';
import { loadDocument } from '@zenstackhq/language';
import { PrismaSchemaGenerator } from '@zenstackhq/sdk';
import colors from 'colors';
import fs from 'node:fs';
import path from 'node:path';
import { CliError } from '../cli-error';

export function getSchemaFile(file?: string) {
if (file) {
Expand Down Expand Up @@ -41,3 +43,11 @@ export function handleSubProcessError(err: unknown) {
process.exit(1);
}
}

export async function generateTempPrismaSchema(zmodelPath: string) {
const model = await loadSchemaDocument(zmodelPath);
const prismaSchema = await new PrismaSchemaGenerator(model).generate();
const prismaSchemaFile = path.resolve(path.dirname(zmodelPath), '~schema.prisma');
fs.writeFileSync(prismaSchemaFile, prismaSchema);
return prismaSchemaFile;
}
53 changes: 27 additions & 26 deletions packages/cli/src/actions/db.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,44 @@
import path from 'node:path';
import fs from 'node:fs';
import { execPackage } from '../utils/exec-utils';
import { getSchemaFile, handleSubProcessError } from './action-utils';
import { run as runGenerate } from './generate';
import { generateTempPrismaSchema, getSchemaFile, handleSubProcessError } from './action-utils';

type CommonOptions = {
type Options = {
schema?: string;
name?: string;
acceptDataLoss?: boolean;
forceReset?: boolean;
};

/**
* CLI action for db related commands
*/
export async function run(command: string, options: CommonOptions) {
const schemaFile = getSchemaFile(options.schema);

// run generate first
await runGenerate({
schema: schemaFile,
silent: true,
});

const prismaSchemaFile = path.join(path.dirname(schemaFile), 'schema.prisma');

export async function run(command: string, options: Options) {
switch (command) {
case 'push':
await runPush(prismaSchemaFile, options);
await runPush(options);
break;
}
}

async function runPush(prismaSchemaFile: string, options: any) {
const cmd = `prisma db push --schema "${prismaSchemaFile}"${
options.acceptDataLoss ? ' --accept-data-loss' : ''
}${options.forceReset ? ' --force-reset' : ''} --skip-generate`;
async function runPush(options: Options) {
// generate a temp prisma schema file
const schemaFile = getSchemaFile(options.schema);
const prismaSchemaFile = await generateTempPrismaSchema(schemaFile);

try {
await execPackage(cmd, {
stdio: 'inherit',
});
} catch (err) {
handleSubProcessError(err);
// run prisma db push
const cmd = `prisma db push --schema "${prismaSchemaFile}"${
options.acceptDataLoss ? ' --accept-data-loss' : ''
}${options.forceReset ? ' --force-reset' : ''} --skip-generate`;
try {
await execPackage(cmd, {
stdio: 'inherit',
});
} catch (err) {
handleSubProcessError(err);
}
} finally {
if (fs.existsSync(prismaSchemaFile)) {
fs.unlinkSync(prismaSchemaFile);
}
}
}
12 changes: 10 additions & 2 deletions packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Options = {
schema?: string;
output?: string;
silent?: boolean;
savePrismaSchema?: string | boolean;
};

/**
Expand All @@ -28,8 +29,15 @@ export async function run(options: Options) {
await runPlugins(model, outputPath, tsSchemaFile);

// generate Prisma schema
const prismaSchema = await new PrismaSchemaGenerator(model).generate();
fs.writeFileSync(path.join(outputPath, 'schema.prisma'), prismaSchema);
if (options.savePrismaSchema) {
const prismaSchema = await new PrismaSchemaGenerator(model).generate();
let prismaSchemaFile = path.join(outputPath, 'schema.prisma');
if (typeof options.savePrismaSchema === 'string') {
prismaSchemaFile = path.resolve(outputPath, options.savePrismaSchema);
fs.mkdirSync(path.dirname(prismaSchemaFile), { recursive: true });
}
fs.writeFileSync(prismaSchemaFile, prismaSchema);
}

if (!options.silent) {
console.log(colors.green('Generation completed successfully.'));
Expand Down
75 changes: 44 additions & 31 deletions packages/cli/src/actions/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,70 @@
import path from 'node:path';
import fs from 'node:fs';
import { execPackage } from '../utils/exec-utils';
import { getSchemaFile } from './action-utils';
import { run as runGenerate } from './generate';
import { generateTempPrismaSchema, getSchemaFile } from './action-utils';

type CommonOptions = {
schema?: string;
};

type DevOptions = CommonOptions & {
name?: string;
createOnly?: boolean;
};

type ResetOptions = CommonOptions & {
force?: boolean;
};

type DeployOptions = CommonOptions;

type StatusOptions = CommonOptions;

/**
* CLI action for migration-related commands
*/
export async function run(command: string, options: CommonOptions) {
const schemaFile = getSchemaFile(options.schema);
const prismaSchemaFile = await generateTempPrismaSchema(schemaFile);

// run generate first
await runGenerate({
schema: schemaFile,
silent: true,
});

const prismaSchemaFile = path.join(path.dirname(schemaFile), 'schema.prisma');

switch (command) {
case 'dev':
await runDev(prismaSchemaFile, options);
break;
try {
switch (command) {
case 'dev':
await runDev(prismaSchemaFile, options as DevOptions);
break;

case 'reset':
await runReset(prismaSchemaFile, options as any);
break;
case 'reset':
await runReset(prismaSchemaFile, options as ResetOptions);
break;

case 'deploy':
await runDeploy(prismaSchemaFile, options);
break;
case 'deploy':
await runDeploy(prismaSchemaFile, options as DeployOptions);
break;

case 'status':
await runStatus(prismaSchemaFile, options);
break;
case 'status':
await runStatus(prismaSchemaFile, options as StatusOptions);
break;
}
} finally {
if (fs.existsSync(prismaSchemaFile)) {
fs.unlinkSync(prismaSchemaFile);
}
}
}

async function runDev(prismaSchemaFile: string, _options: unknown) {
async function runDev(prismaSchemaFile: string, options: DevOptions) {
try {
await execPackage(`prisma migrate dev --schema "${prismaSchemaFile}" --skip-generate`, {
stdio: 'inherit',
});
await execPackage(
`prisma migrate dev --schema "${prismaSchemaFile}" --skip-generate${options.name ? ` --name ${options.name}` : ''}${options.createOnly ? ' --create-only' : ''}`,
{
stdio: 'inherit',
},
);
} catch (err) {
handleSubProcessError(err);
}
}

async function runReset(prismaSchemaFile: string, options: { force: boolean }) {
async function runReset(prismaSchemaFile: string, options: ResetOptions) {
try {
await execPackage(`prisma migrate reset --schema "${prismaSchemaFile}"${options.force ? ' --force' : ''}`, {
stdio: 'inherit',
Expand All @@ -61,7 +74,7 @@ async function runReset(prismaSchemaFile: string, options: { force: boolean }) {
}
}

async function runDeploy(prismaSchemaFile: string, _options: unknown) {
async function runDeploy(prismaSchemaFile: string, _options: DeployOptions) {
try {
await execPackage(`prisma migrate deploy --schema "${prismaSchemaFile}"`, {
stdio: 'inherit',
Expand All @@ -71,7 +84,7 @@ async function runDeploy(prismaSchemaFile: string, _options: unknown) {
}
}

async function runStatus(prismaSchemaFile: string, _options: unknown) {
async function runStatus(prismaSchemaFile: string, _options: StatusOptions) {
try {
await execPackage(`prisma migrate status --schema "${prismaSchemaFile}"`, {
stdio: 'inherit',
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ export function createProgram() {
.command('generate')
.description('Run code generation.')
.addOption(schemaOption)
.addOption(new Option('--silent', 'do not print any output'))
.addOption(
new Option(
'--save-prisma-schema [path]',
'save a Prisma schema file, by default into the output directory',
),
)
.addOption(new Option('-o, --output <path>', 'default output directory for core plugins'))
.action(generateAction);

Expand Down
19 changes: 19 additions & 0 deletions packages/cli/test/db.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { createProject } from './utils';

const model = `
model User {
id String @id @default(cuid())
}
`;

describe('CLI db commands test', () => {
it('should generate a database with db push', () => {
const workDir = createProject(model);
execSync('node node_modules/@zenstackhq/cli/bin/cli db push');
expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
});
});
45 changes: 45 additions & 0 deletions packages/cli/test/generate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { createProject } from './utils';

const model = `
model User {
id String @id @default(cuid())
}
`;

describe('CLI generate command test', () => {
it('should generate a TypeScript schema', () => {
const workDir = createProject(model);
execSync('node node_modules/@zenstackhq/cli/bin/cli generate');
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(false);
});

it('should respect custom output directory', () => {
const workDir = createProject(model);
execSync('node node_modules/@zenstackhq/cli/bin/cli generate --output ./zen');
expect(fs.existsSync(path.join(workDir, 'zen/schema.ts'))).toBe(true);
});

it('should respect custom schema location', () => {
const workDir = createProject(model);
fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'zenstack/foo.zmodel'));
execSync('node node_modules/@zenstackhq/cli/bin/cli generate --schema ./zenstack/foo.zmodel');
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
});

it('should respect save prisma schema option', () => {
const workDir = createProject(model);
execSync('node node_modules/@zenstackhq/cli/bin/cli generate --save-prisma-schema');
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true);
});

it('should respect save prisma schema custom path option', () => {
const workDir = createProject(model);
execSync('node node_modules/@zenstackhq/cli/bin/cli generate --save-prisma-schema "../prisma/schema.prisma"');
expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true);
});
});
16 changes: 16 additions & 0 deletions packages/cli/test/init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import tmp from 'tmp';
import { describe, expect, it } from 'vitest';

describe('Cli init command tests', () => {
it('should create a new project', () => {
const { name: workDir } = tmp.dirSync({ unsafeCleanup: true });
process.chdir(workDir);
execSync('npm init -y');
const cli = path.join(__dirname, '../dist/index.js');
execSync(`node ${cli} init`);
expect(fs.existsSync('zenstack/schema.zmodel')).toBe(true);
});
});
Loading