Skip to content

Commit 4f7df7e

Browse files
authored
fix: schema generation for unsupported field type (#64)
* fix: schema generation for unsupported field type * fix build * update * more tests * update
1 parent 72de807 commit 4f7df7e

File tree

12 files changed

+3289
-72
lines changed

12 files changed

+3289
-72
lines changed

packages/cli/test/db.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { execSync } from 'node:child_process';
21
import fs from 'node:fs';
32
import path from 'node:path';
43
import { describe, expect, it } from 'vitest';
5-
import { createProject } from './utils';
4+
import { createProject, runCli } from './utils';
65

76
const model = `
87
model User {
@@ -13,7 +12,7 @@ model User {
1312
describe('CLI db commands test', () => {
1413
it('should generate a database with db push', () => {
1514
const workDir = createProject(model);
16-
execSync('node node_modules/@zenstackhq/cli/bin/cli db push');
15+
runCli('db push', workDir);
1716
expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
1817
});
1918
});

packages/cli/test/generate.test.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { execSync } from 'node:child_process';
21
import fs from 'node:fs';
32
import path from 'node:path';
43
import { describe, expect, it } from 'vitest';
5-
import { createProject } from './utils';
4+
import { createProject, runCli } from './utils';
65

76
const model = `
87
model User {
@@ -13,33 +12,33 @@ model User {
1312
describe('CLI generate command test', () => {
1413
it('should generate a TypeScript schema', () => {
1514
const workDir = createProject(model);
16-
execSync('node node_modules/@zenstackhq/cli/bin/cli generate');
15+
runCli('generate', workDir);
1716
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
1817
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(false);
1918
});
2019

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

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

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

4039
it('should respect save prisma schema custom path option', () => {
4140
const workDir = createProject(model);
42-
execSync('node node_modules/@zenstackhq/cli/bin/cli generate --save-prisma-schema "../prisma/schema.prisma"');
41+
runCli('generate --save-prisma-schema "../prisma/schema.prisma"', workDir);
4342
expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true);
4443
});
4544
});

packages/cli/test/init.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
import { execSync } from 'node:child_process';
21
import fs from 'node:fs';
32
import path from 'node:path';
43
import tmp from 'tmp';
54
import { describe, expect, it } from 'vitest';
5+
import { runCli } from './utils';
66

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

packages/cli/test/migrate.test.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { execSync } from 'node:child_process';
21
import fs from 'node:fs';
32
import path from 'node:path';
43
import { describe, expect, it } from 'vitest';
5-
import { createProject } from './utils';
4+
import { createProject, runCli } from './utils';
65

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

2120
it('should reset the database with migrate reset', () => {
2221
const workDir = createProject(model);
23-
execSync('node node_modules/@zenstackhq/cli/bin/cli db push');
22+
runCli('db push', workDir);
2423
expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
25-
execSync('node node_modules/@zenstackhq/cli/bin/cli migrate reset --force');
24+
runCli('migrate reset --force', workDir);
2625
expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true);
2726
});
2827

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

3736
it('supports migrate status', () => {
38-
createProject(model);
39-
execSync('node node_modules/@zenstackhq/cli/bin/cli migrate dev --name init');
40-
execSync('node node_modules/@zenstackhq/cli/bin/cli migrate status');
37+
const workDir = createProject(model);
38+
runCli('migrate dev --name init', workDir);
39+
runCli('migrate status', workDir);
4140
});
4241
});

packages/cli/test/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createTestProject } from '@zenstackhq/testtools';
2+
import { execSync } from 'node:child_process';
23
import fs from 'node:fs';
34
import path from 'node:path';
45

@@ -13,6 +14,10 @@ export function createProject(zmodel: string, addPrelude = true) {
1314
fs.mkdirSync(path.join(workDir, 'zenstack'), { recursive: true });
1415
const schemaPath = path.join(workDir, 'zenstack/schema.zmodel');
1516
fs.writeFileSync(schemaPath, addPrelude ? `${ZMODEL_PRELUDE}\n\n${zmodel}` : zmodel);
16-
process.chdir(workDir);
1717
return workDir;
1818
}
19+
20+
export function runCli(command: string, cwd: string) {
21+
const cli = path.join(__dirname, '../dist/index.js');
22+
execSync(`node ${cli} ${command}`, { cwd });
23+
}

packages/sdk/src/schema/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export type FieldDef = {
5959
unique?: boolean;
6060
updatedAt?: boolean;
6161
attributes?: AttributeApplication[];
62-
default?: MappedBuiltinType | Expression;
62+
default?: MappedBuiltinType | Expression | unknown[];
6363
relation?: RelationInfo;
6464
foreignKeyFor?: string[];
6565
computed?: boolean;

packages/sdk/src/ts-schema-generator.ts

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -284,12 +284,7 @@ export class TsSchemaGenerator {
284284
}
285285

286286
private createDataModelFieldObject(field: DataModelField) {
287-
const objectFields = [
288-
ts.factory.createPropertyAssignment(
289-
'type',
290-
ts.factory.createStringLiteral(field.type.type ?? field.type.reference!.$refText),
291-
),
292-
];
287+
const objectFields = [ts.factory.createPropertyAssignment('type', this.generateFieldTypeLiteral(field))];
293288

294289
if (isIdField(field)) {
295290
objectFields.push(ts.factory.createPropertyAssignment('id', ts.factory.createTrue()));
@@ -323,9 +318,9 @@ export class TsSchemaGenerator {
323318
);
324319
}
325320

326-
const defaultValue = this.getMappedDefault(field);
321+
const defaultValue = this.getFieldMappedDefault(field);
327322
if (defaultValue !== undefined) {
328-
if (typeof defaultValue === 'object') {
323+
if (typeof defaultValue === 'object' && !Array.isArray(defaultValue)) {
329324
if ('call' in defaultValue) {
330325
objectFields.push(
331326
ts.factory.createPropertyAssignment(
@@ -371,18 +366,20 @@ export class TsSchemaGenerator {
371366
throw new Error(`Unsupported default value type for field ${field.name}`);
372367
}
373368
} else {
374-
objectFields.push(
375-
ts.factory.createPropertyAssignment(
376-
'default',
377-
typeof defaultValue === 'string'
378-
? ts.factory.createStringLiteral(defaultValue)
379-
: typeof defaultValue === 'number'
380-
? ts.factory.createNumericLiteral(defaultValue)
381-
: defaultValue === true
382-
? ts.factory.createTrue()
383-
: ts.factory.createFalse(),
384-
),
385-
);
369+
if (Array.isArray(defaultValue)) {
370+
objectFields.push(
371+
ts.factory.createPropertyAssignment(
372+
'default',
373+
ts.factory.createArrayLiteralExpression(
374+
defaultValue.map((item) => this.createLiteralNode(item as any)),
375+
),
376+
),
377+
);
378+
} else {
379+
objectFields.push(
380+
ts.factory.createPropertyAssignment('default', this.createLiteralNode(defaultValue)),
381+
);
382+
}
386383
}
387384
}
388385

@@ -438,37 +435,44 @@ export class TsSchemaGenerator {
438435
}
439436
}
440437

441-
private getMappedDefault(
438+
private getFieldMappedDefault(
442439
field: DataModelField,
443-
): string | number | boolean | { call: string; args: any[] } | { authMember: string[] } | undefined {
440+
): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | undefined {
444441
const defaultAttr = getAttribute(field, '@default');
445442
if (!defaultAttr) {
446443
return undefined;
447444
}
448-
449445
const defaultValue = defaultAttr.args[0]?.value;
450446
invariant(defaultValue, 'Expected a default value');
447+
return this.getMappedValue(defaultValue, field.type);
448+
}
451449

452-
if (isLiteralExpr(defaultValue)) {
453-
const lit = (defaultValue as LiteralExpr).value;
454-
return field.type.type === 'Boolean'
450+
private getMappedValue(
451+
expr: Expression,
452+
fieldType: DataModelFieldType,
453+
): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | undefined {
454+
if (isLiteralExpr(expr)) {
455+
const lit = (expr as LiteralExpr).value;
456+
return fieldType.type === 'Boolean'
455457
? (lit as boolean)
456-
: ['Int', 'Float', 'Decimal', 'BigInt'].includes(field.type.type!)
458+
: ['Int', 'Float', 'Decimal', 'BigInt'].includes(fieldType.type!)
457459
? Number(lit)
458460
: lit;
459-
} else if (isReferenceExpr(defaultValue) && isEnumField(defaultValue.target.ref)) {
460-
return defaultValue.target.ref.name;
461-
} else if (isInvocationExpr(defaultValue)) {
461+
} else if (isArrayExpr(expr)) {
462+
return expr.items.map((item) => this.getMappedValue(item, fieldType));
463+
} else if (isReferenceExpr(expr) && isEnumField(expr.target.ref)) {
464+
return expr.target.ref.name;
465+
} else if (isInvocationExpr(expr)) {
462466
return {
463-
call: defaultValue.function.$refText,
464-
args: defaultValue.args.map((arg) => this.getLiteral(arg.value)),
467+
call: expr.function.$refText,
468+
args: expr.args.map((arg) => this.getLiteral(arg.value)),
465469
};
466-
} else if (this.isAuthMemberAccess(defaultValue)) {
470+
} else if (this.isAuthMemberAccess(expr)) {
467471
return {
468-
authMember: this.getMemberAccessChain(defaultValue),
472+
authMember: this.getMemberAccessChain(expr),
469473
};
470474
} else {
471-
throw new Error(`Unsupported default value type for field ${field.name}`);
475+
throw new Error(`Unsupported default value type for ${expr.$type}`);
472476
}
473477
}
474478

@@ -682,8 +686,16 @@ export class TsSchemaGenerator {
682686
}
683687

684688
private generateFieldTypeLiteral(field: DataModelField): ts.Expression {
685-
invariant(field.type.type || field.type.reference, 'Field type must be a primitive or reference');
686-
return ts.factory.createStringLiteral(field.type.type ?? field.type.reference!.$refText);
689+
invariant(
690+
field.type.type || field.type.reference || field.type.unsupported,
691+
'Field type must be a primitive, reference, or Unsupported',
692+
);
693+
694+
return field.type.type
695+
? ts.factory.createStringLiteral(field.type.type)
696+
: field.type.reference
697+
? ts.factory.createStringLiteral(field.type.reference.$refText)
698+
: ts.factory.createStringLiteral('unknown');
687699
}
688700

689701
private createEnumObject(e: Enum) {

tests/e2e/cal.com/cal-com.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { generateTsSchema } from '@zenstackhq/testtools';
2-
import { describe, it } from 'vitest';
2+
import { describe, expect, it } from 'vitest';
33
import fs from 'node:fs';
44
import path from 'node:path';
55

66
describe('Cal.com e2e tests', () => {
77
it('has a working schema', async () => {
8-
const generated = await generateTsSchema(
9-
fs.readFileSync(path.join(__dirname, 'schema.zmodel'), 'utf8'),
10-
'postgresql',
11-
'cal-com',
12-
);
13-
console.log(generated);
8+
await expect(
9+
generateTsSchema(fs.readFileSync(path.join(__dirname, 'schema.zmodel'), 'utf8'), 'postgresql', 'cal-com'),
10+
).resolves.toBeTruthy();
1411
});
1512
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { generateTsSchema } from '@zenstackhq/testtools';
2+
import { describe, expect, it } from 'vitest';
3+
import fs from 'node:fs';
4+
import path from 'node:path';
5+
6+
describe('Formbricks e2e tests', () => {
7+
it('has a working schema', async () => {
8+
await expect(
9+
generateTsSchema(fs.readFileSync(path.join(__dirname, 'schema.zmodel'), 'utf8'), 'postgresql', 'cal-com'),
10+
).resolves.toBeTruthy();
11+
});
12+
});

0 commit comments

Comments
 (0)