@zenstackhq/cli 3.0.0-alpha.8 → 3.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +11 -11
- package/bin/cli +1 -1
- package/dist/index.cjs +649 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -4
- package/dist/index.d.ts +1 -4
- package/dist/index.js +651 -111
- package/dist/index.js.map +1 -1
- package/package.json +16 -11
- package/src/actions/action-utils.ts +69 -4
- package/src/actions/check.ts +22 -0
- package/src/actions/db.ts +9 -6
- package/src/actions/generate.ts +108 -36
- package/src/actions/index.ts +2 -1
- package/src/actions/info.ts +4 -1
- package/src/actions/migrate.ts +51 -16
- package/src/actions/templates.ts +4 -3
- package/src/constants.ts +2 -0
- package/src/index.ts +99 -28
- package/src/plugins/index.ts +2 -0
- package/src/plugins/prisma.ts +21 -0
- package/src/plugins/typescript.ts +21 -0
- package/src/telemetry.ts +139 -0
- package/src/utils/is-ci.ts +5 -0
- package/src/utils/is-container.ts +23 -0
- package/src/utils/is-docker.ts +31 -0
- package/src/utils/is-wsl.ts +18 -0
- package/src/utils/machine-id-utils.ts +76 -0
- package/src/utils/version-utils.ts +37 -0
- package/test/check.test.ts +101 -0
- package/test/generate.test.ts +12 -9
- package/test/migrate.test.ts +31 -0
- package/test/plugins/custom-plugin.test.ts +50 -0
- package/test/plugins/prisma-plugin.test.ts +60 -0
- package/test/ts-schema-gen.test.ts +180 -1
- package/tsconfig.json +0 -3
- package/tsup.config.ts +17 -0
- package/vitest.config.ts +1 -1
package/src/telemetry.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { init, type Mixpanel } from 'mixpanel';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { TELEMETRY_TRACKING_TOKEN } from './constants';
|
|
6
|
+
import { isInCi } from './utils/is-ci';
|
|
7
|
+
import { isInContainer } from './utils/is-container';
|
|
8
|
+
import isDocker from './utils/is-docker';
|
|
9
|
+
import { isWsl } from './utils/is-wsl';
|
|
10
|
+
import { getMachineId } from './utils/machine-id-utils';
|
|
11
|
+
import { getVersion } from './utils/version-utils';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Telemetry events
|
|
15
|
+
*/
|
|
16
|
+
export type TelemetryEvents =
|
|
17
|
+
| 'cli:start'
|
|
18
|
+
| 'cli:complete'
|
|
19
|
+
| 'cli:error'
|
|
20
|
+
| 'cli:command:start'
|
|
21
|
+
| 'cli:command:complete'
|
|
22
|
+
| 'cli:command:error'
|
|
23
|
+
| 'cli:plugin:start'
|
|
24
|
+
| 'cli:plugin:complete'
|
|
25
|
+
| 'cli:plugin:error';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Utility class for sending telemetry
|
|
29
|
+
*/
|
|
30
|
+
export class Telemetry {
|
|
31
|
+
private readonly mixpanel: Mixpanel | undefined;
|
|
32
|
+
private readonly hostId = getMachineId();
|
|
33
|
+
private readonly sessionid = randomUUID();
|
|
34
|
+
private readonly _os_type = os.type();
|
|
35
|
+
private readonly _os_release = os.release();
|
|
36
|
+
private readonly _os_arch = os.arch();
|
|
37
|
+
private readonly _os_version = os.version();
|
|
38
|
+
private readonly _os_platform = os.platform();
|
|
39
|
+
private readonly version = getVersion();
|
|
40
|
+
private readonly prismaVersion = this.getPrismaVersion();
|
|
41
|
+
private readonly isDocker = isDocker();
|
|
42
|
+
private readonly isWsl = isWsl();
|
|
43
|
+
private readonly isContainer = isInContainer();
|
|
44
|
+
private readonly isCi = isInCi;
|
|
45
|
+
|
|
46
|
+
constructor() {
|
|
47
|
+
if (process.env['DO_NOT_TRACK'] !== '1' && TELEMETRY_TRACKING_TOKEN) {
|
|
48
|
+
this.mixpanel = init(TELEMETRY_TRACKING_TOKEN, {
|
|
49
|
+
geolocate: true,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get isTracking() {
|
|
55
|
+
return !!this.mixpanel;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
track(event: TelemetryEvents, properties: Record<string, unknown> = {}) {
|
|
59
|
+
if (this.mixpanel) {
|
|
60
|
+
const payload = {
|
|
61
|
+
distinct_id: this.hostId,
|
|
62
|
+
session: this.sessionid,
|
|
63
|
+
time: new Date(),
|
|
64
|
+
$os: this._os_type,
|
|
65
|
+
osType: this._os_type,
|
|
66
|
+
osRelease: this._os_release,
|
|
67
|
+
osPlatform: this._os_platform,
|
|
68
|
+
osArch: this._os_arch,
|
|
69
|
+
osVersion: this._os_version,
|
|
70
|
+
nodeVersion: process.version,
|
|
71
|
+
version: this.version,
|
|
72
|
+
prismaVersion: this.prismaVersion,
|
|
73
|
+
isDocker: this.isDocker,
|
|
74
|
+
isWsl: this.isWsl,
|
|
75
|
+
isContainer: this.isContainer,
|
|
76
|
+
isCi: this.isCi,
|
|
77
|
+
...properties,
|
|
78
|
+
};
|
|
79
|
+
this.mixpanel.track(event, payload);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
trackError(err: Error) {
|
|
84
|
+
this.track('cli:error', {
|
|
85
|
+
message: err.message,
|
|
86
|
+
stack: err.stack,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async trackSpan<T>(
|
|
91
|
+
startEvent: TelemetryEvents,
|
|
92
|
+
completeEvent: TelemetryEvents,
|
|
93
|
+
errorEvent: TelemetryEvents,
|
|
94
|
+
properties: Record<string, unknown>,
|
|
95
|
+
action: () => Promise<T> | T,
|
|
96
|
+
) {
|
|
97
|
+
this.track(startEvent, properties);
|
|
98
|
+
const start = Date.now();
|
|
99
|
+
let success = true;
|
|
100
|
+
try {
|
|
101
|
+
return await action();
|
|
102
|
+
} catch (err: any) {
|
|
103
|
+
this.track(errorEvent, {
|
|
104
|
+
message: err.message,
|
|
105
|
+
stack: err.stack,
|
|
106
|
+
...properties,
|
|
107
|
+
});
|
|
108
|
+
success = false;
|
|
109
|
+
throw err;
|
|
110
|
+
} finally {
|
|
111
|
+
this.track(completeEvent, {
|
|
112
|
+
duration: Date.now() - start,
|
|
113
|
+
success,
|
|
114
|
+
...properties,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async trackCommand(command: string, action: () => Promise<void> | void) {
|
|
120
|
+
await this.trackSpan('cli:command:start', 'cli:command:complete', 'cli:command:error', { command }, action);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async trackCli(action: () => Promise<void> | void) {
|
|
124
|
+
await this.trackSpan('cli:start', 'cli:complete', 'cli:error', {}, action);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getPrismaVersion() {
|
|
128
|
+
try {
|
|
129
|
+
const packageJsonPath = import.meta.resolve('prisma/package.json');
|
|
130
|
+
const packageJsonUrl = new URL(packageJsonPath);
|
|
131
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonUrl, 'utf8'));
|
|
132
|
+
return packageJson.version;
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const telemetry = new Telemetry();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import isDocker from './is-docker';
|
|
3
|
+
|
|
4
|
+
let cachedResult: boolean | undefined;
|
|
5
|
+
|
|
6
|
+
// Podman detection
|
|
7
|
+
const hasContainerEnv = () => {
|
|
8
|
+
try {
|
|
9
|
+
fs.statSync('/run/.containerenv');
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function isInContainer() {
|
|
17
|
+
// TODO: Use `??=` when targeting Node.js 16.
|
|
18
|
+
if (cachedResult === undefined) {
|
|
19
|
+
cachedResult = hasContainerEnv() || isDocker();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return cachedResult;
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Copied over from https://github.com/sindresorhus/is-docker for CJS compatibility
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
let isDockerCached: boolean | undefined;
|
|
6
|
+
|
|
7
|
+
function hasDockerEnv() {
|
|
8
|
+
try {
|
|
9
|
+
fs.statSync('/.dockerenv');
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hasDockerCGroup() {
|
|
17
|
+
try {
|
|
18
|
+
return fs.readFileSync('/proc/self/cgroup', 'utf8').includes('docker');
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function isDocker() {
|
|
25
|
+
// TODO: Use `??=` when targeting Node.js 16.
|
|
26
|
+
if (isDockerCached === undefined) {
|
|
27
|
+
isDockerCached = hasDockerEnv() || hasDockerCGroup();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return isDockerCached;
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
export const isWsl = () => {
|
|
5
|
+
if (process.platform !== 'linux') {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (os.release().toLowerCase().includes('microsoft')) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft');
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// modified from https://github.com/automation-stack/node-machine-id
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const { platform } = process;
|
|
7
|
+
const win32RegBinPath = {
|
|
8
|
+
native: '%windir%\\System32',
|
|
9
|
+
mixed: '%windir%\\sysnative\\cmd.exe /c %windir%\\System32',
|
|
10
|
+
};
|
|
11
|
+
const guid = {
|
|
12
|
+
darwin: 'ioreg -rd1 -c IOPlatformExpertDevice',
|
|
13
|
+
win32:
|
|
14
|
+
`${win32RegBinPath[isWindowsProcessMixedOrNativeArchitecture()]}\\REG.exe ` +
|
|
15
|
+
'QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography ' +
|
|
16
|
+
'/v MachineGuid',
|
|
17
|
+
linux: '( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname 2> /dev/null) | head -n 1 || :',
|
|
18
|
+
freebsd: 'kenv -q smbios.system.uuid || sysctl -n kern.hostuuid',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function isWindowsProcessMixedOrNativeArchitecture() {
|
|
22
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
23
|
+
if (process.arch === 'ia32' && process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) {
|
|
24
|
+
return 'mixed';
|
|
25
|
+
}
|
|
26
|
+
return 'native';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hash(guid: string): string {
|
|
30
|
+
return createHash('sha256').update(guid).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function expose(result: string): string | undefined {
|
|
34
|
+
switch (platform) {
|
|
35
|
+
case 'darwin':
|
|
36
|
+
return result
|
|
37
|
+
.split('IOPlatformUUID')[1]
|
|
38
|
+
?.split('\n')[0]
|
|
39
|
+
?.replace(/=|\s+|"/gi, '')
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
case 'win32':
|
|
42
|
+
return result
|
|
43
|
+
.toString()
|
|
44
|
+
.split('REG_SZ')[1]
|
|
45
|
+
?.replace(/\r+|\n+|\s+/gi, '')
|
|
46
|
+
.toLowerCase();
|
|
47
|
+
case 'linux':
|
|
48
|
+
return result
|
|
49
|
+
.toString()
|
|
50
|
+
.replace(/\r+|\n+|\s+/gi, '')
|
|
51
|
+
.toLowerCase();
|
|
52
|
+
case 'freebsd':
|
|
53
|
+
return result
|
|
54
|
+
.toString()
|
|
55
|
+
.replace(/\r+|\n+|\s+/gi, '')
|
|
56
|
+
.toLowerCase();
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getMachineId() {
|
|
63
|
+
if (!(platform in guid)) {
|
|
64
|
+
return randomUUID();
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const value = execSync(guid[platform as keyof typeof guid]);
|
|
68
|
+
const id = expose(value.toString());
|
|
69
|
+
if (!id) {
|
|
70
|
+
return randomUUID();
|
|
71
|
+
}
|
|
72
|
+
return hash(id);
|
|
73
|
+
} catch {
|
|
74
|
+
return randomUUID();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import colors from 'colors';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import semver from 'semver';
|
|
6
|
+
|
|
7
|
+
const CHECK_VERSION_TIMEOUT = 2000;
|
|
8
|
+
const VERSION_CHECK_TAG = 'next';
|
|
4
9
|
|
|
5
10
|
export function getVersion() {
|
|
6
11
|
try {
|
|
@@ -11,3 +16,35 @@ export function getVersion() {
|
|
|
11
16
|
return undefined;
|
|
12
17
|
}
|
|
13
18
|
}
|
|
19
|
+
|
|
20
|
+
export async function checkNewVersion() {
|
|
21
|
+
const currVersion = getVersion();
|
|
22
|
+
let latestVersion: string;
|
|
23
|
+
try {
|
|
24
|
+
latestVersion = await getLatestVersion();
|
|
25
|
+
} catch {
|
|
26
|
+
// noop
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (latestVersion && currVersion && semver.gt(latestVersion, currVersion)) {
|
|
31
|
+
console.log(`A newer version ${colors.cyan(latestVersion)} is available.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getLatestVersion() {
|
|
36
|
+
const fetchResult = await fetch(`https://registry.npmjs.org/@zenstackhq/cli/${VERSION_CHECK_TAG}`, {
|
|
37
|
+
headers: { accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*' },
|
|
38
|
+
signal: AbortSignal.timeout(CHECK_VERSION_TIMEOUT),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (fetchResult.ok) {
|
|
42
|
+
const data: any = await fetchResult.json();
|
|
43
|
+
const latestVersion = data?.version;
|
|
44
|
+
if (typeof latestVersion === 'string' && semver.valid(latestVersion)) {
|
|
45
|
+
return latestVersion;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error('invalid npm registry response');
|
|
50
|
+
}
|
|
@@ -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('check', 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('check', 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('check --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('check --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('check', 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('check', workDir)).toThrow();
|
|
100
|
+
});
|
|
101
|
+
});
|
package/test/generate.test.ts
CHANGED
|
@@ -30,15 +30,18 @@ describe('CLI generate command test', () => {
|
|
|
30
30
|
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it('should respect
|
|
33
|
+
it('should respect package.json config', () => {
|
|
34
34
|
const workDir = createProject(model);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
fs.mkdirSync(path.join(workDir, 'foo'));
|
|
36
|
+
fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'foo/schema.zmodel'));
|
|
37
|
+
fs.rmdirSync(path.join(workDir, 'zenstack'));
|
|
38
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8'));
|
|
39
|
+
pkgJson.zenstack = {
|
|
40
|
+
schema: './foo/schema.zmodel',
|
|
41
|
+
output: './bar',
|
|
42
|
+
};
|
|
43
|
+
fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
|
|
44
|
+
runCli('generate', workDir);
|
|
45
|
+
expect(fs.existsSync(path.join(workDir, 'bar/schema.ts'))).toBe(true);
|
|
43
46
|
});
|
|
44
47
|
});
|
package/test/migrate.test.ts
CHANGED
|
@@ -38,4 +38,35 @@ describe('CLI migrate commands test', () => {
|
|
|
38
38
|
runCli('migrate dev --name init', workDir);
|
|
39
39
|
runCli('migrate status', workDir);
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
it('supports migrate resolve', () => {
|
|
43
|
+
const workDir = createProject(model);
|
|
44
|
+
runCli('migrate dev --name init', workDir);
|
|
45
|
+
|
|
46
|
+
// find the migration record "timestamp_init"
|
|
47
|
+
const migrationRecords = fs.readdirSync(path.join(workDir, 'zenstack/migrations'));
|
|
48
|
+
const migration = migrationRecords.find((f) => f.endsWith('_init'));
|
|
49
|
+
|
|
50
|
+
// force a migration failure
|
|
51
|
+
fs.writeFileSync(path.join(workDir, 'zenstack/migrations', migration!, 'migration.sql'), 'invalid content');
|
|
52
|
+
|
|
53
|
+
// redeploy the migration, which will fail
|
|
54
|
+
fs.rmSync(path.join(workDir, 'zenstack/dev.db'), { force: true });
|
|
55
|
+
try {
|
|
56
|
+
runCli('migrate deploy', workDir);
|
|
57
|
+
} catch {
|
|
58
|
+
// noop
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --rolled-back
|
|
62
|
+
runCli(`migrate resolve --rolled-back ${migration}`, workDir);
|
|
63
|
+
|
|
64
|
+
// --applied
|
|
65
|
+
runCli(`migrate resolve --applied ${migration}`, workDir);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should throw error when neither applied nor rolled-back is provided', () => {
|
|
69
|
+
const workDir = createProject(model);
|
|
70
|
+
expect(() => runCli('migrate resolve', workDir)).toThrow();
|
|
71
|
+
});
|
|
41
72
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
describe('Custom plugins tests', () => {
|
|
8
|
+
it('runs custom plugin generator', () => {
|
|
9
|
+
const workDir = createProject(`
|
|
10
|
+
plugin custom {
|
|
11
|
+
provider = '../my-plugin.js'
|
|
12
|
+
output = '../custom-output'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
model User {
|
|
16
|
+
id String @id @default(cuid())
|
|
17
|
+
}
|
|
18
|
+
`);
|
|
19
|
+
|
|
20
|
+
fs.writeFileSync(
|
|
21
|
+
path.join(workDir, 'my-plugin.ts'),
|
|
22
|
+
`
|
|
23
|
+
import type { CliPlugin } from '@zenstackhq/sdk';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
const plugin: CliPlugin = {
|
|
28
|
+
name: 'Custom Generator',
|
|
29
|
+
statusText: 'Generating foo.txt',
|
|
30
|
+
async generate({ model, defaultOutputPath, pluginOptions }) {
|
|
31
|
+
let outDir = defaultOutputPath;
|
|
32
|
+
if (typeof pluginOptions['output'] === 'string') {
|
|
33
|
+
outDir = path.resolve(defaultOutputPath, pluginOptions['output']);
|
|
34
|
+
if (!fs.existsSync(outDir)) {
|
|
35
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
fs.writeFileSync(path.join(outDir, 'foo.txt'), 'from my plugin');
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default plugin;
|
|
43
|
+
`,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
execSync('npx tsc', { cwd: workDir });
|
|
47
|
+
runCli('generate', workDir);
|
|
48
|
+
expect(fs.existsSync(path.join(workDir, 'custom-output/foo.txt'))).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
describe('Core plugins tests', () => {
|
|
7
|
+
it('can automatically generate a TypeScript schema with default output', () => {
|
|
8
|
+
const workDir = createProject(`
|
|
9
|
+
model User {
|
|
10
|
+
id String @id @default(cuid())
|
|
11
|
+
}
|
|
12
|
+
`);
|
|
13
|
+
runCli('generate', workDir);
|
|
14
|
+
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('can automatically generate a TypeScript schema with custom output', () => {
|
|
18
|
+
const workDir = createProject(`
|
|
19
|
+
plugin typescript {
|
|
20
|
+
provider = '@core/typescript'
|
|
21
|
+
output = '../generated-schema'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
model User {
|
|
25
|
+
id String @id @default(cuid())
|
|
26
|
+
}
|
|
27
|
+
`);
|
|
28
|
+
runCli('generate', workDir);
|
|
29
|
+
expect(fs.existsSync(path.join(workDir, 'generated-schema/schema.ts'))).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('can generate a Prisma schema with default output', () => {
|
|
33
|
+
const workDir = createProject(`
|
|
34
|
+
plugin prisma {
|
|
35
|
+
provider = '@core/prisma'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
model User {
|
|
39
|
+
id String @id @default(cuid())
|
|
40
|
+
}
|
|
41
|
+
`);
|
|
42
|
+
runCli('generate', workDir);
|
|
43
|
+
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('can generate a Prisma schema with custom output', () => {
|
|
47
|
+
const workDir = createProject(`
|
|
48
|
+
plugin prisma {
|
|
49
|
+
provider = '@core/prisma'
|
|
50
|
+
output = '../prisma/schema.prisma'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
model User {
|
|
54
|
+
id String @id @default(cuid())
|
|
55
|
+
}
|
|
56
|
+
`);
|
|
57
|
+
runCli('generate', workDir);
|
|
58
|
+
expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|