Skip to content

Commit 966018f

Browse files
authored
fix: add regression project and optimize ts schema generation (#247)
* fix: add regression project and optimize ts schema generation * address review comments
1 parent 3d2e199 commit 966018f

File tree

12 files changed

+276
-57
lines changed

12 files changed

+276
-57
lines changed

packages/runtime/test/scripts/generate.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { fileURLToPath } from 'node:url';
88
const dir = path.dirname(fileURLToPath(import.meta.url));
99

1010
async function main() {
11-
// glob all zmodel files in "e2e" directory
1211
const zmodelFiles = glob.sync(path.resolve(dir, '../schemas/**/*.zmodel'));
1312
for (const file of zmodelFiles) {
1413
console.log(`Generating TS schema for: ${file}`);

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

Lines changed: 55 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,14 @@ import {
5252
} from './model-utils';
5353

5454
export class TsSchemaGenerator {
55+
private usedExpressionUtils = false;
56+
5557
async generate(model: Model, outputDir: string) {
5658
fs.mkdirSync(outputDir, { recursive: true });
5759

60+
// Reset the flag for each generation
61+
this.usedExpressionUtils = false;
62+
5863
// the schema itself
5964
this.generateSchema(model, outputDir);
6065

@@ -82,6 +87,10 @@ export class TsSchemaGenerator {
8287
(d) => isDataModel(d) && d.fields.some((f) => hasAttribute(f, '@computed')),
8388
);
8489

90+
// Generate schema content first to determine if ExpressionUtils is needed
91+
const schemaObject = this.createSchemaObject(model);
92+
93+
// Now generate the import declaration with the correct imports
8594
const runtimeImportDecl = ts.factory.createImportDeclaration(
8695
undefined,
8796
ts.factory.createImportClause(
@@ -98,7 +107,15 @@ export class TsSchemaGenerator {
98107
),
99108
]
100109
: []),
101-
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier('ExpressionUtils')),
110+
...(this.usedExpressionUtils
111+
? [
112+
ts.factory.createImportSpecifier(
113+
false,
114+
undefined,
115+
ts.factory.createIdentifier('ExpressionUtils'),
116+
),
117+
]
118+
: []),
102119
]),
103120
),
104121
ts.factory.createStringLiteral('@zenstackhq/runtime/schema'),
@@ -114,10 +131,7 @@ export class TsSchemaGenerator {
114131
undefined,
115132
undefined,
116133
ts.factory.createSatisfiesExpression(
117-
ts.factory.createAsExpression(
118-
this.createSchemaObject(model),
119-
ts.factory.createTypeReferenceNode('const'),
120-
),
134+
ts.factory.createAsExpression(schemaObject, ts.factory.createTypeReferenceNode('const')),
121135
ts.factory.createTypeReferenceNode('SchemaDef'),
122136
),
123137
),
@@ -137,6 +151,15 @@ export class TsSchemaGenerator {
137151
statements.push(typeDeclaration);
138152
}
139153

154+
private createExpressionUtilsCall(method: string, args?: ts.Expression[]): ts.CallExpression {
155+
this.usedExpressionUtils = true;
156+
return ts.factory.createCallExpression(
157+
ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('ExpressionUtils'), method),
158+
undefined,
159+
args || [],
160+
);
161+
}
162+
140163
private createSchemaObject(model: Model) {
141164
const properties: ts.PropertyAssignment[] = [
142165
// provider
@@ -477,40 +500,28 @@ export class TsSchemaGenerator {
477500
ts.factory.createPropertyAssignment(
478501
'default',
479502

480-
ts.factory.createCallExpression(
481-
ts.factory.createIdentifier('ExpressionUtils.call'),
482-
undefined,
483-
[
484-
ts.factory.createStringLiteral(defaultValue.call),
485-
...(defaultValue.args.length > 0
486-
? [
487-
ts.factory.createArrayLiteralExpression(
488-
defaultValue.args.map((arg) => this.createLiteralNode(arg)),
489-
),
490-
]
491-
: []),
492-
],
493-
),
503+
this.createExpressionUtilsCall('call', [
504+
ts.factory.createStringLiteral(defaultValue.call),
505+
...(defaultValue.args.length > 0
506+
? [
507+
ts.factory.createArrayLiteralExpression(
508+
defaultValue.args.map((arg) => this.createLiteralNode(arg)),
509+
),
510+
]
511+
: []),
512+
]),
494513
),
495514
);
496515
} else if ('authMember' in defaultValue) {
497516
objectFields.push(
498517
ts.factory.createPropertyAssignment(
499518
'default',
500-
ts.factory.createCallExpression(
501-
ts.factory.createIdentifier('ExpressionUtils.member'),
502-
undefined,
503-
[
504-
ts.factory.createCallExpression(
505-
ts.factory.createIdentifier('ExpressionUtils.call'),
506-
undefined,
507-
[ts.factory.createStringLiteral('auth')],
508-
),
509-
ts.factory.createArrayLiteralExpression(
510-
defaultValue.authMember.map((m) => ts.factory.createStringLiteral(m)),
511-
),
512-
],
513-
),
519+
this.createExpressionUtilsCall('member', [
520+
this.createExpressionUtilsCall('call', [ts.factory.createStringLiteral('auth')]),
521+
ts.factory.createArrayLiteralExpression(
522+
defaultValue.authMember.map((m) => ts.factory.createStringLiteral(m)),
523+
),
524+
]),
514525
),
515526
);
516527
} else {
@@ -1015,7 +1026,7 @@ export class TsSchemaGenerator {
10151026
}
10161027

10171028
private createThisExpression() {
1018-
return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils._this'), undefined, []);
1029+
return this.createExpressionUtilsCall('_this');
10191030
}
10201031

10211032
private createMemberExpression(expr: MemberAccessExpr) {
@@ -1034,39 +1045,37 @@ export class TsSchemaGenerator {
10341045
ts.factory.createArrayLiteralExpression(members.map((m) => ts.factory.createStringLiteral(m))),
10351046
];
10361047

1037-
return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.member'), undefined, args);
1048+
return this.createExpressionUtilsCall('member', args);
10381049
}
10391050

10401051
private createNullExpression() {
1041-
return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils._null'), undefined, []);
1052+
return this.createExpressionUtilsCall('_null');
10421053
}
10431054

10441055
private createBinaryExpression(expr: BinaryExpr) {
1045-
return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.binary'), undefined, [
1056+
return this.createExpressionUtilsCall('binary', [
10461057
this.createExpression(expr.left),
10471058
this.createLiteralNode(expr.operator),
10481059
this.createExpression(expr.right),
10491060
]);
10501061
}
10511062

10521063
private createUnaryExpression(expr: UnaryExpr) {
1053-
return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.unary'), undefined, [
1064+
return this.createExpressionUtilsCall('unary', [
10541065
this.createLiteralNode(expr.operator),
10551066
this.createExpression(expr.operand),
10561067
]);
10571068
}
10581069

10591070
private createArrayExpression(expr: ArrayExpr): any {
1060-
return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.array'), undefined, [
1071+
return this.createExpressionUtilsCall('array', [
10611072
ts.factory.createArrayLiteralExpression(expr.items.map((item) => this.createExpression(item))),
10621073
]);
10631074
}
10641075

10651076
private createRefExpression(expr: ReferenceExpr): any {
10661077
if (isDataField(expr.target.ref)) {
1067-
return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.field'), undefined, [
1068-
this.createLiteralNode(expr.target.$refText),
1069-
]);
1078+
return this.createExpressionUtilsCall('field', [this.createLiteralNode(expr.target.$refText)]);
10701079
} else if (isEnumField(expr.target.ref)) {
10711080
return this.createLiteralExpression('StringLiteral', expr.target.$refText);
10721081
} else {
@@ -1075,7 +1084,7 @@ export class TsSchemaGenerator {
10751084
}
10761085

10771086
private createCallExpression(expr: InvocationExpr) {
1078-
return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.call'), undefined, [
1087+
return this.createExpressionUtilsCall('call', [
10791088
ts.factory.createStringLiteral(expr.function.$refText),
10801089
...(expr.args.length > 0
10811090
? [ts.factory.createArrayLiteralExpression(expr.args.map((arg) => this.createExpression(arg.value)))]
@@ -1085,21 +1094,11 @@ export class TsSchemaGenerator {
10851094

10861095
private createLiteralExpression(type: string, value: string | boolean) {
10871096
return match(type)
1088-
.with('BooleanLiteral', () =>
1089-
ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.literal'), undefined, [
1090-
this.createLiteralNode(value),
1091-
]),
1092-
)
1097+
.with('BooleanLiteral', () => this.createExpressionUtilsCall('literal', [this.createLiteralNode(value)]))
10931098
.with('NumberLiteral', () =>
1094-
ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.literal'), undefined, [
1095-
ts.factory.createIdentifier(value as string),
1096-
]),
1097-
)
1098-
.with('StringLiteral', () =>
1099-
ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.literal'), undefined, [
1100-
this.createLiteralNode(value),
1101-
]),
1099+
this.createExpressionUtilsCall('literal', [ts.factory.createIdentifier(value as string)]),
11021100
)
1101+
.with('StringLiteral', () => this.createExpressionUtilsCall('literal', [this.createLiteralNode(value)]))
11031102
.otherwise(() => {
11041103
throw new Error(`Unsupported literal type: ${type}`);
11051104
});

pnpm-lock.yaml

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/regression/generate.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { loadDocument } from '@zenstackhq/language';
2+
import { TsSchemaGenerator } from '@zenstackhq/sdk';
3+
import { glob } from 'glob';
4+
import fs from 'node:fs';
5+
import path from 'node:path';
6+
import { fileURLToPath } from 'node:url';
7+
8+
const dir = path.dirname(fileURLToPath(import.meta.url));
9+
10+
async function main() {
11+
const zmodelFiles = glob.sync(path.resolve(dir, './test/**/*.zmodel'));
12+
for (const file of zmodelFiles) {
13+
console.log(`Generating TS schema for: ${file}`);
14+
await generate(file);
15+
}
16+
}
17+
18+
async function generate(schemaPath: string) {
19+
const generator = new TsSchemaGenerator();
20+
const outputDir = path.dirname(schemaPath);
21+
const tsPath = path.join(outputDir, 'schema.ts');
22+
const pluginModelFiles = glob.sync(path.resolve(dir, '../../packages/runtime/dist/**/plugin.zmodel'));
23+
const result = await loadDocument(schemaPath, pluginModelFiles);
24+
if (!result.success) {
25+
throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`);
26+
}
27+
await generator.generate(result.model, outputDir);
28+
}
29+
30+
main();

tests/regression/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "regression",
3+
"version": "3.0.0-beta.3",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"generate": "tsx generate.ts",
8+
"test": "pnpm generate && tsc && vitest run"
9+
},
10+
"dependencies": {
11+
"@zenstackhq/testtools": "workspace:*"
12+
},
13+
"devDependencies": {
14+
"@zenstackhq/cli": "workspace:*",
15+
"@zenstackhq/sdk": "workspace:*",
16+
"@zenstackhq/language": "workspace:*",
17+
"@zenstackhq/runtime": "workspace:*",
18+
"@zenstackhq/typescript-config": "workspace:*",
19+
"@zenstackhq/vitest-config": "workspace:*"
20+
}
21+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//////////////////////////////////////////////////////////////////////////////////////////////
2+
// DO NOT MODIFY THIS FILE //
3+
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
4+
//////////////////////////////////////////////////////////////////////////////////////////////
5+
6+
/* eslint-disable */
7+
8+
import { type SchemaType as $Schema } from "./schema";
9+
import type { FindManyArgs as $FindManyArgs, FindUniqueArgs as $FindUniqueArgs, FindFirstArgs as $FindFirstArgs, CreateArgs as $CreateArgs, CreateManyArgs as $CreateManyArgs, CreateManyAndReturnArgs as $CreateManyAndReturnArgs, UpdateArgs as $UpdateArgs, UpdateManyArgs as $UpdateManyArgs, UpdateManyAndReturnArgs as $UpdateManyAndReturnArgs, UpsertArgs as $UpsertArgs, DeleteArgs as $DeleteArgs, DeleteManyArgs as $DeleteManyArgs, CountArgs as $CountArgs, AggregateArgs as $AggregateArgs, GroupByArgs as $GroupByArgs, WhereInput as $WhereInput, SelectInput as $SelectInput, IncludeInput as $IncludeInput, OmitInput as $OmitInput } from "@zenstackhq/runtime";
10+
import type { SimplifiedModelResult as $SimplifiedModelResult, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/runtime";
11+
export type FooFindManyArgs = $FindManyArgs<$Schema, "Foo">;
12+
export type FooFindUniqueArgs = $FindUniqueArgs<$Schema, "Foo">;
13+
export type FooFindFirstArgs = $FindFirstArgs<$Schema, "Foo">;
14+
export type FooCreateArgs = $CreateArgs<$Schema, "Foo">;
15+
export type FooCreateManyArgs = $CreateManyArgs<$Schema, "Foo">;
16+
export type FooCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Foo">;
17+
export type FooUpdateArgs = $UpdateArgs<$Schema, "Foo">;
18+
export type FooUpdateManyArgs = $UpdateManyArgs<$Schema, "Foo">;
19+
export type FooUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Foo">;
20+
export type FooUpsertArgs = $UpsertArgs<$Schema, "Foo">;
21+
export type FooDeleteArgs = $DeleteArgs<$Schema, "Foo">;
22+
export type FooDeleteManyArgs = $DeleteManyArgs<$Schema, "Foo">;
23+
export type FooCountArgs = $CountArgs<$Schema, "Foo">;
24+
export type FooAggregateArgs = $AggregateArgs<$Schema, "Foo">;
25+
export type FooGroupByArgs = $GroupByArgs<$Schema, "Foo">;
26+
export type FooWhereInput = $WhereInput<$Schema, "Foo">;
27+
export type FooSelect = $SelectInput<$Schema, "Foo">;
28+
export type FooInclude = $IncludeInput<$Schema, "Foo">;
29+
export type FooOmit = $OmitInput<$Schema, "Foo">;
30+
export type FooGetPayload<Args extends $SelectIncludeOmit<$Schema, "Foo", true>> = $SimplifiedModelResult<$Schema, "Foo", Args>;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//////////////////////////////////////////////////////////////////////////////////////////////
2+
// DO NOT MODIFY THIS FILE //
3+
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
4+
//////////////////////////////////////////////////////////////////////////////////////////////
5+
6+
/* eslint-disable */
7+
8+
import { schema as $schema, type SchemaType as $Schema } from "./schema";
9+
import { type ModelResult as $ModelResult, type TypeDefResult as $TypeDefResult } from "@zenstackhq/runtime";
10+
export type Foo = $ModelResult<$Schema, "Foo">;
11+
export type Configuration = $TypeDefResult<$Schema, "Configuration">;
12+
export const ShirtColor = $schema.enums.ShirtColor;
13+
export type ShirtColor = (typeof ShirtColor)[keyof typeof ShirtColor];
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { describe, it } from 'vitest';
2+
import { type Configuration, ShirtColor } from './models';
3+
4+
describe('Issue 204 regression tests', () => {
5+
it('tests issue 204', () => {
6+
const config: Configuration = { teamColors: [ShirtColor.Black, ShirtColor.Blue] };
7+
console.log(config.teamColors?.[0]);
8+
const config1: Configuration = {};
9+
console.log(config1);
10+
});
11+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
datasource db {
2+
provider = "sqlite"
3+
url = "file:./dev.db"
4+
}
5+
6+
enum ShirtColor {
7+
Black
8+
White
9+
Red
10+
Green
11+
Blue
12+
}
13+
14+
type Configuration {
15+
teamColors ShirtColor[]? // This should be an optional array
16+
}
17+
18+
model Foo {
19+
id Int @id
20+
config Configuration @json
21+
}

0 commit comments

Comments
 (0)