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
5 changes: 2 additions & 3 deletions packages/cli/test/db.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
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';
import { createProject, runCli } from './utils';

const model = `
model User {
Expand All @@ -13,7 +12,7 @@ model User {
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');
runCli('db push', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
});
});
13 changes: 6 additions & 7 deletions packages/cli/test/generate.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
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';
import { createProject, runCli } from './utils';

const model = `
model User {
Expand All @@ -13,33 +12,33 @@ model User {
describe('CLI generate command test', () => {
it('should generate a TypeScript schema', () => {
const workDir = createProject(model);
execSync('node node_modules/@zenstackhq/cli/bin/cli generate');
runCli('generate', workDir);
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');
runCli('generate --output ./zen', workDir);
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');
runCli('generate --schema ./zenstack/foo.zmodel', workDir);
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');
runCli('generate --save-prisma-schema', workDir);
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"');
runCli('generate --save-prisma-schema "../prisma/schema.prisma"', workDir);
expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true);
});
});
9 changes: 3 additions & 6 deletions packages/cli/test/init.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
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';
import { runCli } from './utils';

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);
runCli('init', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.zmodel'))).toBe(true);
});
});
19 changes: 9 additions & 10 deletions packages/cli/test/migrate.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
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';
import { createProject, runCli } from './utils';

const model = `
model User {
Expand All @@ -13,30 +12,30 @@ model User {
describe('CLI migrate commands test', () => {
it('should generate a database with migrate dev', () => {
const workDir = createProject(model);
execSync('node node_modules/@zenstackhq/cli/bin/cli migrate dev --name init');
runCli('migrate dev --name init', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/migrations'))).toBe(true);
});

it('should reset the database with migrate reset', () => {
const workDir = createProject(model);
execSync('node node_modules/@zenstackhq/cli/bin/cli db push');
runCli('db push', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
execSync('node node_modules/@zenstackhq/cli/bin/cli migrate reset --force');
runCli('migrate reset --force', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
});

it('should reset the database with migrate deploy', () => {
const workDir = createProject(model);
execSync('node node_modules/@zenstackhq/cli/bin/cli migrate dev --name init');
runCli('migrate dev --name init', workDir);
fs.rmSync(path.join(workDir, 'zenstack/dev.db'));
execSync('node node_modules/@zenstackhq/cli/bin/cli migrate deploy');
runCli('migrate deploy', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
});

it('supports migrate status', () => {
createProject(model);
execSync('node node_modules/@zenstackhq/cli/bin/cli migrate dev --name init');
execSync('node node_modules/@zenstackhq/cli/bin/cli migrate status');
const workDir = createProject(model);
runCli('migrate dev --name init', workDir);
runCli('migrate status', workDir);
});
});
7 changes: 6 additions & 1 deletion packages/cli/test/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createTestProject } from '@zenstackhq/testtools';
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

Expand All @@ -13,6 +14,10 @@ export function createProject(zmodel: string, addPrelude = true) {
fs.mkdirSync(path.join(workDir, 'zenstack'), { recursive: true });
const schemaPath = path.join(workDir, 'zenstack/schema.zmodel');
fs.writeFileSync(schemaPath, addPrelude ? `${ZMODEL_PRELUDE}\n\n${zmodel}` : zmodel);
process.chdir(workDir);
return workDir;
}

export function runCli(command: string, cwd: string) {
const cli = path.join(__dirname, '../dist/index.js');
execSync(`node ${cli} ${command}`, { cwd });
}
2 changes: 1 addition & 1 deletion packages/sdk/src/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export type FieldDef = {
unique?: boolean;
updatedAt?: boolean;
attributes?: AttributeApplication[];
default?: MappedBuiltinType | Expression;
default?: MappedBuiltinType | Expression | unknown[];
relation?: RelationInfo;
foreignKeyFor?: string[];
computed?: boolean;
Expand Down
86 changes: 49 additions & 37 deletions packages/sdk/src/ts-schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,7 @@ export class TsSchemaGenerator {
}

private createDataModelFieldObject(field: DataModelField) {
const objectFields = [
ts.factory.createPropertyAssignment(
'type',
ts.factory.createStringLiteral(field.type.type ?? field.type.reference!.$refText),
),
];
const objectFields = [ts.factory.createPropertyAssignment('type', this.generateFieldTypeLiteral(field))];

if (isIdField(field)) {
objectFields.push(ts.factory.createPropertyAssignment('id', ts.factory.createTrue()));
Expand Down Expand Up @@ -323,9 +318,9 @@ export class TsSchemaGenerator {
);
}

const defaultValue = this.getMappedDefault(field);
const defaultValue = this.getFieldMappedDefault(field);
if (defaultValue !== undefined) {
if (typeof defaultValue === 'object') {
if (typeof defaultValue === 'object' && !Array.isArray(defaultValue)) {
if ('call' in defaultValue) {
objectFields.push(
ts.factory.createPropertyAssignment(
Expand Down Expand Up @@ -371,18 +366,20 @@ export class TsSchemaGenerator {
throw new Error(`Unsupported default value type for field ${field.name}`);
}
} else {
objectFields.push(
ts.factory.createPropertyAssignment(
'default',
typeof defaultValue === 'string'
? ts.factory.createStringLiteral(defaultValue)
: typeof defaultValue === 'number'
? ts.factory.createNumericLiteral(defaultValue)
: defaultValue === true
? ts.factory.createTrue()
: ts.factory.createFalse(),
),
);
if (Array.isArray(defaultValue)) {
objectFields.push(
ts.factory.createPropertyAssignment(
'default',
ts.factory.createArrayLiteralExpression(
defaultValue.map((item) => this.createLiteralNode(item as any)),
),
),
);
} else {
objectFields.push(
ts.factory.createPropertyAssignment('default', this.createLiteralNode(defaultValue)),
);
}
}
}

Expand Down Expand Up @@ -438,37 +435,44 @@ export class TsSchemaGenerator {
}
}

private getMappedDefault(
private getFieldMappedDefault(
field: DataModelField,
): string | number | boolean | { call: string; args: any[] } | { authMember: string[] } | undefined {
): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | undefined {
const defaultAttr = getAttribute(field, '@default');
if (!defaultAttr) {
return undefined;
}

const defaultValue = defaultAttr.args[0]?.value;
invariant(defaultValue, 'Expected a default value');
return this.getMappedValue(defaultValue, field.type);
}

if (isLiteralExpr(defaultValue)) {
const lit = (defaultValue as LiteralExpr).value;
return field.type.type === 'Boolean'
private getMappedValue(
expr: Expression,
fieldType: DataModelFieldType,
): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | undefined {
if (isLiteralExpr(expr)) {
const lit = (expr as LiteralExpr).value;
return fieldType.type === 'Boolean'
? (lit as boolean)
: ['Int', 'Float', 'Decimal', 'BigInt'].includes(field.type.type!)
: ['Int', 'Float', 'Decimal', 'BigInt'].includes(fieldType.type!)
? Number(lit)
: lit;
} else if (isReferenceExpr(defaultValue) && isEnumField(defaultValue.target.ref)) {
return defaultValue.target.ref.name;
} else if (isInvocationExpr(defaultValue)) {
} else if (isArrayExpr(expr)) {
return expr.items.map((item) => this.getMappedValue(item, fieldType));
} else if (isReferenceExpr(expr) && isEnumField(expr.target.ref)) {
return expr.target.ref.name;
} else if (isInvocationExpr(expr)) {
return {
call: defaultValue.function.$refText,
args: defaultValue.args.map((arg) => this.getLiteral(arg.value)),
call: expr.function.$refText,
args: expr.args.map((arg) => this.getLiteral(arg.value)),
};
} else if (this.isAuthMemberAccess(defaultValue)) {
} else if (this.isAuthMemberAccess(expr)) {
return {
authMember: this.getMemberAccessChain(defaultValue),
authMember: this.getMemberAccessChain(expr),
};
} else {
throw new Error(`Unsupported default value type for field ${field.name}`);
throw new Error(`Unsupported default value type for ${expr.$type}`);
}
}

Expand Down Expand Up @@ -682,8 +686,16 @@ export class TsSchemaGenerator {
}

private generateFieldTypeLiteral(field: DataModelField): ts.Expression {
invariant(field.type.type || field.type.reference, 'Field type must be a primitive or reference');
return ts.factory.createStringLiteral(field.type.type ?? field.type.reference!.$refText);
invariant(
field.type.type || field.type.reference || field.type.unsupported,
'Field type must be a primitive, reference, or Unsupported',
);

return field.type.type
? ts.factory.createStringLiteral(field.type.type)
: field.type.reference
? ts.factory.createStringLiteral(field.type.reference.$refText)
: ts.factory.createStringLiteral('unknown');
}

private createEnumObject(e: Enum) {
Expand Down
11 changes: 4 additions & 7 deletions tests/e2e/cal.com/cal-com.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { generateTsSchema } from '@zenstackhq/testtools';
import { describe, it } from 'vitest';
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';

describe('Cal.com e2e tests', () => {
it('has a working schema', async () => {
const generated = await generateTsSchema(
fs.readFileSync(path.join(__dirname, 'schema.zmodel'), 'utf8'),
'postgresql',
'cal-com',
);
console.log(generated);
await expect(
generateTsSchema(fs.readFileSync(path.join(__dirname, 'schema.zmodel'), 'utf8'), 'postgresql', 'cal-com'),
).resolves.toBeTruthy();
});
});
12 changes: 12 additions & 0 deletions tests/e2e/formbricks/formbricks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { generateTsSchema } from '@zenstackhq/testtools';
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';

describe('Formbricks e2e tests', () => {
it('has a working schema', async () => {
await expect(
generateTsSchema(fs.readFileSync(path.join(__dirname, 'schema.zmodel'), 'utf8'), 'postgresql', 'cal-com'),
).resolves.toBeTruthy();
});
});
Loading