diff --git a/packages/runtime/test/scripts/generate.ts b/packages/runtime/test/scripts/generate.ts index a1393e30..7e5f1293 100644 --- a/packages/runtime/test/scripts/generate.ts +++ b/packages/runtime/test/scripts/generate.ts @@ -8,7 +8,6 @@ import { fileURLToPath } from 'node:url'; const dir = path.dirname(fileURLToPath(import.meta.url)); async function main() { - // glob all zmodel files in "e2e" directory const zmodelFiles = glob.sync(path.resolve(dir, '../schemas/**/*.zmodel')); for (const file of zmodelFiles) { console.log(`Generating TS schema for: ${file}`); diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index d2e8ba64..1d558300 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -52,9 +52,14 @@ import { } from './model-utils'; export class TsSchemaGenerator { + private usedExpressionUtils = false; + async generate(model: Model, outputDir: string) { fs.mkdirSync(outputDir, { recursive: true }); + // Reset the flag for each generation + this.usedExpressionUtils = false; + // the schema itself this.generateSchema(model, outputDir); @@ -82,6 +87,10 @@ export class TsSchemaGenerator { (d) => isDataModel(d) && d.fields.some((f) => hasAttribute(f, '@computed')), ); + // Generate schema content first to determine if ExpressionUtils is needed + const schemaObject = this.createSchemaObject(model); + + // Now generate the import declaration with the correct imports const runtimeImportDecl = ts.factory.createImportDeclaration( undefined, ts.factory.createImportClause( @@ -98,7 +107,15 @@ export class TsSchemaGenerator { ), ] : []), - ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier('ExpressionUtils')), + ...(this.usedExpressionUtils + ? [ + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier('ExpressionUtils'), + ), + ] + : []), ]), ), ts.factory.createStringLiteral('@zenstackhq/runtime/schema'), @@ -114,10 +131,7 @@ export class TsSchemaGenerator { undefined, undefined, ts.factory.createSatisfiesExpression( - ts.factory.createAsExpression( - this.createSchemaObject(model), - ts.factory.createTypeReferenceNode('const'), - ), + ts.factory.createAsExpression(schemaObject, ts.factory.createTypeReferenceNode('const')), ts.factory.createTypeReferenceNode('SchemaDef'), ), ), @@ -137,6 +151,15 @@ export class TsSchemaGenerator { statements.push(typeDeclaration); } + private createExpressionUtilsCall(method: string, args?: ts.Expression[]): ts.CallExpression { + this.usedExpressionUtils = true; + return ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('ExpressionUtils'), method), + undefined, + args || [], + ); + } + private createSchemaObject(model: Model) { const properties: ts.PropertyAssignment[] = [ // provider @@ -477,40 +500,28 @@ export class TsSchemaGenerator { ts.factory.createPropertyAssignment( 'default', - ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.call'), - undefined, - [ - ts.factory.createStringLiteral(defaultValue.call), - ...(defaultValue.args.length > 0 - ? [ - ts.factory.createArrayLiteralExpression( - defaultValue.args.map((arg) => this.createLiteralNode(arg)), - ), - ] - : []), - ], - ), + this.createExpressionUtilsCall('call', [ + ts.factory.createStringLiteral(defaultValue.call), + ...(defaultValue.args.length > 0 + ? [ + ts.factory.createArrayLiteralExpression( + defaultValue.args.map((arg) => this.createLiteralNode(arg)), + ), + ] + : []), + ]), ), ); } else if ('authMember' in defaultValue) { objectFields.push( ts.factory.createPropertyAssignment( 'default', - ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.member'), - undefined, - [ - ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.call'), - undefined, - [ts.factory.createStringLiteral('auth')], - ), - ts.factory.createArrayLiteralExpression( - defaultValue.authMember.map((m) => ts.factory.createStringLiteral(m)), - ), - ], - ), + this.createExpressionUtilsCall('member', [ + this.createExpressionUtilsCall('call', [ts.factory.createStringLiteral('auth')]), + ts.factory.createArrayLiteralExpression( + defaultValue.authMember.map((m) => ts.factory.createStringLiteral(m)), + ), + ]), ), ); } else { @@ -1015,7 +1026,7 @@ export class TsSchemaGenerator { } private createThisExpression() { - return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils._this'), undefined, []); + return this.createExpressionUtilsCall('_this'); } private createMemberExpression(expr: MemberAccessExpr) { @@ -1034,15 +1045,15 @@ export class TsSchemaGenerator { ts.factory.createArrayLiteralExpression(members.map((m) => ts.factory.createStringLiteral(m))), ]; - return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.member'), undefined, args); + return this.createExpressionUtilsCall('member', args); } private createNullExpression() { - return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils._null'), undefined, []); + return this.createExpressionUtilsCall('_null'); } private createBinaryExpression(expr: BinaryExpr) { - return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.binary'), undefined, [ + return this.createExpressionUtilsCall('binary', [ this.createExpression(expr.left), this.createLiteralNode(expr.operator), this.createExpression(expr.right), @@ -1050,23 +1061,21 @@ export class TsSchemaGenerator { } private createUnaryExpression(expr: UnaryExpr) { - return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.unary'), undefined, [ + return this.createExpressionUtilsCall('unary', [ this.createLiteralNode(expr.operator), this.createExpression(expr.operand), ]); } private createArrayExpression(expr: ArrayExpr): any { - return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.array'), undefined, [ + return this.createExpressionUtilsCall('array', [ ts.factory.createArrayLiteralExpression(expr.items.map((item) => this.createExpression(item))), ]); } private createRefExpression(expr: ReferenceExpr): any { if (isDataField(expr.target.ref)) { - return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.field'), undefined, [ - this.createLiteralNode(expr.target.$refText), - ]); + return this.createExpressionUtilsCall('field', [this.createLiteralNode(expr.target.$refText)]); } else if (isEnumField(expr.target.ref)) { return this.createLiteralExpression('StringLiteral', expr.target.$refText); } else { @@ -1075,7 +1084,7 @@ export class TsSchemaGenerator { } private createCallExpression(expr: InvocationExpr) { - return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.call'), undefined, [ + return this.createExpressionUtilsCall('call', [ ts.factory.createStringLiteral(expr.function.$refText), ...(expr.args.length > 0 ? [ts.factory.createArrayLiteralExpression(expr.args.map((arg) => this.createExpression(arg.value)))] @@ -1085,21 +1094,11 @@ export class TsSchemaGenerator { private createLiteralExpression(type: string, value: string | boolean) { return match(type) - .with('BooleanLiteral', () => - ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.literal'), undefined, [ - this.createLiteralNode(value), - ]), - ) + .with('BooleanLiteral', () => this.createExpressionUtilsCall('literal', [this.createLiteralNode(value)])) .with('NumberLiteral', () => - ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.literal'), undefined, [ - ts.factory.createIdentifier(value as string), - ]), - ) - .with('StringLiteral', () => - ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.literal'), undefined, [ - this.createLiteralNode(value), - ]), + this.createExpressionUtilsCall('literal', [ts.factory.createIdentifier(value as string)]), ) + .with('StringLiteral', () => this.createExpressionUtilsCall('literal', [this.createLiteralNode(value)])) .otherwise(() => { throw new Error(`Unsupported literal type: ${type}`); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f67f9b78..c339a56b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -485,6 +485,31 @@ importers: specifier: workspace:* version: link:../../packages/vitest-config + tests/regression: + dependencies: + '@zenstackhq/testtools': + specifier: workspace:* + version: link:../../packages/testtools + devDependencies: + '@zenstackhq/cli': + specifier: workspace:* + version: link:../../packages/cli + '@zenstackhq/language': + specifier: workspace:* + version: link:../../packages/language + '@zenstackhq/runtime': + specifier: workspace:* + version: link:../../packages/runtime + '@zenstackhq/sdk': + specifier: workspace:* + version: link:../../packages/sdk + '@zenstackhq/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config + '@zenstackhq/vitest-config': + specifier: workspace:* + version: link:../../packages/vitest-config + packages: '@chevrotain/cst-dts-gen@11.0.3': diff --git a/tests/regression/generate.ts b/tests/regression/generate.ts new file mode 100644 index 00000000..86993b96 --- /dev/null +++ b/tests/regression/generate.ts @@ -0,0 +1,30 @@ +import { loadDocument } from '@zenstackhq/language'; +import { TsSchemaGenerator } from '@zenstackhq/sdk'; +import { glob } from 'glob'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const dir = path.dirname(fileURLToPath(import.meta.url)); + +async function main() { + const zmodelFiles = glob.sync(path.resolve(dir, './test/**/*.zmodel')); + for (const file of zmodelFiles) { + console.log(`Generating TS schema for: ${file}`); + await generate(file); + } +} + +async function generate(schemaPath: string) { + const generator = new TsSchemaGenerator(); + const outputDir = path.dirname(schemaPath); + const tsPath = path.join(outputDir, 'schema.ts'); + const pluginModelFiles = glob.sync(path.resolve(dir, '../../packages/runtime/dist/**/plugin.zmodel')); + const result = await loadDocument(schemaPath, pluginModelFiles); + if (!result.success) { + throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`); + } + await generator.generate(result.model, outputDir); +} + +main(); diff --git a/tests/regression/package.json b/tests/regression/package.json new file mode 100644 index 00000000..1d54ca4f --- /dev/null +++ b/tests/regression/package.json @@ -0,0 +1,21 @@ +{ + "name": "regression", + "version": "3.0.0-beta.3", + "private": true, + "type": "module", + "scripts": { + "generate": "tsx generate.ts", + "test": "pnpm generate && tsc && vitest run" + }, + "dependencies": { + "@zenstackhq/testtools": "workspace:*" + }, + "devDependencies": { + "@zenstackhq/cli": "workspace:*", + "@zenstackhq/sdk": "workspace:*", + "@zenstackhq/language": "workspace:*", + "@zenstackhq/runtime": "workspace:*", + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/vitest-config": "workspace:*" + } +} diff --git a/tests/regression/test/issue-204/input.ts b/tests/regression/test/issue-204/input.ts new file mode 100644 index 00000000..3916c070 --- /dev/null +++ b/tests/regression/test/issue-204/input.ts @@ -0,0 +1,30 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema"; +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"; +import type { SimplifiedModelResult as $SimplifiedModelResult, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/runtime"; +export type FooFindManyArgs = $FindManyArgs<$Schema, "Foo">; +export type FooFindUniqueArgs = $FindUniqueArgs<$Schema, "Foo">; +export type FooFindFirstArgs = $FindFirstArgs<$Schema, "Foo">; +export type FooCreateArgs = $CreateArgs<$Schema, "Foo">; +export type FooCreateManyArgs = $CreateManyArgs<$Schema, "Foo">; +export type FooCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Foo">; +export type FooUpdateArgs = $UpdateArgs<$Schema, "Foo">; +export type FooUpdateManyArgs = $UpdateManyArgs<$Schema, "Foo">; +export type FooUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Foo">; +export type FooUpsertArgs = $UpsertArgs<$Schema, "Foo">; +export type FooDeleteArgs = $DeleteArgs<$Schema, "Foo">; +export type FooDeleteManyArgs = $DeleteManyArgs<$Schema, "Foo">; +export type FooCountArgs = $CountArgs<$Schema, "Foo">; +export type FooAggregateArgs = $AggregateArgs<$Schema, "Foo">; +export type FooGroupByArgs = $GroupByArgs<$Schema, "Foo">; +export type FooWhereInput = $WhereInput<$Schema, "Foo">; +export type FooSelect = $SelectInput<$Schema, "Foo">; +export type FooInclude = $IncludeInput<$Schema, "Foo">; +export type FooOmit = $OmitInput<$Schema, "Foo">; +export type FooGetPayload> = $SimplifiedModelResult<$Schema, "Foo", Args>; diff --git a/tests/regression/test/issue-204/models.ts b/tests/regression/test/issue-204/models.ts new file mode 100644 index 00000000..c03d254e --- /dev/null +++ b/tests/regression/test/issue-204/models.ts @@ -0,0 +1,13 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { schema as $schema, type SchemaType as $Schema } from "./schema"; +import { type ModelResult as $ModelResult, type TypeDefResult as $TypeDefResult } from "@zenstackhq/runtime"; +export type Foo = $ModelResult<$Schema, "Foo">; +export type Configuration = $TypeDefResult<$Schema, "Configuration">; +export const ShirtColor = $schema.enums.ShirtColor; +export type ShirtColor = (typeof ShirtColor)[keyof typeof ShirtColor]; diff --git a/tests/regression/test/issue-204/regression.test.ts b/tests/regression/test/issue-204/regression.test.ts new file mode 100644 index 00000000..24a43e3b --- /dev/null +++ b/tests/regression/test/issue-204/regression.test.ts @@ -0,0 +1,11 @@ +import { describe, it } from 'vitest'; +import { type Configuration, ShirtColor } from './models'; + +describe('Issue 204 regression tests', () => { + it('tests issue 204', () => { + const config: Configuration = { teamColors: [ShirtColor.Black, ShirtColor.Blue] }; + console.log(config.teamColors?.[0]); + const config1: Configuration = {}; + console.log(config1); + }); +}); diff --git a/tests/regression/test/issue-204/regression.zmodel b/tests/regression/test/issue-204/regression.zmodel new file mode 100644 index 00000000..95309329 --- /dev/null +++ b/tests/regression/test/issue-204/regression.zmodel @@ -0,0 +1,21 @@ +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +enum ShirtColor { + Black + White + Red + Green + Blue +} + +type Configuration { + teamColors ShirtColor[]? // This should be an optional array +} + +model Foo { + id Int @id + config Configuration @json +} diff --git a/tests/regression/test/issue-204/schema.ts b/tests/regression/test/issue-204/schema.ts new file mode 100644 index 00000000..b214a272 --- /dev/null +++ b/tests/regression/test/issue-204/schema.ts @@ -0,0 +1,59 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef } from "@zenstackhq/runtime/schema"; +export const schema = { + provider: { + type: "sqlite" + }, + models: { + Foo: { + name: "Foo", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }] + }, + config: { + name: "config", + type: "Configuration", + attributes: [{ name: "@json" }] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + } + }, + typeDefs: { + Configuration: { + name: "Configuration", + fields: { + teamColors: { + name: "teamColors", + type: "ShirtColor", + optional: true, + array: true + } + } + } + }, + enums: { + ShirtColor: { + Black: "Black", + White: "White", + Red: "Red", + Green: "Green", + Blue: "Blue" + } + }, + plugins: {} +} as const satisfies SchemaDef; +export type SchemaType = typeof schema; diff --git a/tests/regression/tsconfig.json b/tests/regression/tsconfig.json new file mode 100644 index 00000000..f3a2dbcb --- /dev/null +++ b/tests/regression/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@zenstackhq/typescript-config/base.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/tests/regression/vitest.config.ts b/tests/regression/vitest.config.ts new file mode 100644 index 00000000..75a9f709 --- /dev/null +++ b/tests/regression/vitest.config.ts @@ -0,0 +1,4 @@ +import base from '@zenstackhq/vitest-config/base'; +import { defineConfig, mergeConfig } from 'vitest/config'; + +export default mergeConfig(base, defineConfig({}));