diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index a4a01cb8..b8aedcb5 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -13,6 +13,8 @@ type Options = { schema?: string; output?: string; silent: boolean; + lite: boolean; + liteOnly: boolean; }; /** @@ -88,10 +90,15 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string, } } - const defaultPlugins = [corePlugins['typescript']].reverse(); - defaultPlugins.forEach((d) => { - if (!processedPlugins.some((p) => p.cliPlugin === d)) { - processedPlugins.push({ cliPlugin: d, pluginOptions: {} }); + const defaultPlugins = [ + { + plugin: corePlugins['typescript'], + options: { lite: options.lite, liteOnly: options.liteOnly }, + }, + ]; + defaultPlugins.forEach(({ plugin, options }) => { + if (!processedPlugins.some((p) => p.cliPlugin === plugin)) { + processedPlugins.push({ cliPlugin: plugin, pluginOptions: options }); } }); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f55fe6b4..a4119dfe 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -59,6 +59,8 @@ function createProgram() { .addOption(schemaOption) .addOption(noVersionCheckOption) .addOption(new Option('-o, --output ', 'default output directory for code generation')) + .addOption(new Option('--lite', 'also generate a lite version of schema without attributes').default(false)) + .addOption(new Option('--lite-only', 'only generate lite version of schema without attributes').default(false)) .addOption(new Option('--silent', 'suppress all output except errors').default(false)) .action(generateAction); diff --git a/packages/cli/src/plugins/typescript.ts b/packages/cli/src/plugins/typescript.ts index 4fd5006f..25f950ab 100644 --- a/packages/cli/src/plugins/typescript.ts +++ b/packages/cli/src/plugins/typescript.ts @@ -7,6 +7,7 @@ const plugin: CliPlugin = { name: 'TypeScript Schema Generator', statusText: 'Generating TypeScript schema', async generate({ model, defaultOutputPath, pluginOptions }) { + // output path let outDir = defaultOutputPath; if (typeof pluginOptions['output'] === 'string') { outDir = path.resolve(defaultOutputPath, pluginOptions['output']); @@ -14,7 +15,14 @@ const plugin: CliPlugin = { fs.mkdirSync(outDir, { recursive: true }); } } - await new TsSchemaGenerator().generate(model, outDir); + + // lite mode + const lite = pluginOptions['lite'] === true; + + // liteOnly mode + const liteOnly = pluginOptions['liteOnly'] === true; + + await new TsSchemaGenerator().generate(model, { outDir, lite, liteOnly }); }, }; diff --git a/packages/cli/test/generate.test.ts b/packages/cli/test/generate.test.ts index 701fe4f0..738cc9c7 100644 --- a/packages/cli/test/generate.test.ts +++ b/packages/cli/test/generate.test.ts @@ -44,4 +44,18 @@ describe('CLI generate command test', () => { runCli('generate', workDir); expect(fs.existsSync(path.join(workDir, 'bar/schema.ts'))).toBe(true); }); + + it('should respect lite option', () => { + const workDir = createProject(model); + runCli('generate --lite', workDir); + expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true); + expect(fs.existsSync(path.join(workDir, 'zenstack/schema-lite.ts'))).toBe(true); + }); + + it('should respect liteOnly option', () => { + const workDir = createProject(model); + runCli('generate --lite-only', workDir); + expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(false); + expect(fs.existsSync(path.join(workDir, 'zenstack/schema-lite.ts'))).toBe(true); + }); }); diff --git a/packages/cli/test/ts-schema-gen.test.ts b/packages/cli/test/ts-schema-gen.test.ts index 7a6ece60..2673567d 100644 --- a/packages/cli/test/ts-schema-gen.test.ts +++ b/packages/cli/test/ts-schema-gen.test.ts @@ -420,4 +420,26 @@ model User { }, }); }); + + it('supports lite schema generation', async () => { + const { schemaLite } = await generateTsSchema( + ` +model User { + id String @id @default(uuid()) + name String + email String @unique + + @@map('users') +} + `, + undefined, + undefined, + undefined, + true, + ); + + expect(schemaLite!.models.User.attributes).toBeUndefined(); + expect(schemaLite!.models.User.fields.id.attributes).toBeUndefined(); + expect(schemaLite!.models.User.fields.email.attributes).toBeUndefined(); + }); }); diff --git a/packages/clients/tanstack-query/package.json b/packages/clients/tanstack-query/package.json index 269a024c..3fc4bebe 100644 --- a/packages/clients/tanstack-query/package.json +++ b/packages/clients/tanstack-query/package.json @@ -6,12 +6,13 @@ "type": "module", "private": true, "scripts": { - "build": "tsc --noEmit && tsup-node", + "build": "tsc --noEmit && tsup-node && pnpm test:generate && pnpm test:typecheck", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "test": "vitest run", "pack": "pnpm pack", - "test:generate": "tsx scripts/generate.ts" + "test:generate": "tsx scripts/generate.ts", + "test:typecheck": "tsc --noEmit --project tsconfig.test.json" }, "keywords": [ "tanstack-query", diff --git a/packages/clients/tanstack-query/scripts/generate.ts b/packages/clients/tanstack-query/scripts/generate.ts index f4f0c03b..3801f157 100644 --- a/packages/clients/tanstack-query/scripts/generate.ts +++ b/packages/clients/tanstack-query/scripts/generate.ts @@ -16,12 +16,12 @@ async function main() { async function generate(schemaPath: string) { const generator = new TsSchemaGenerator(); - const outputDir = path.dirname(schemaPath); + const outDir = path.dirname(schemaPath); const result = await loadDocument(schemaPath); if (!result.success) { throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`); } - await generator.generate(result.model, outputDir); + await generator.generate(result.model, { outDir, liteOnly: true }); } main(); diff --git a/packages/clients/tanstack-query/test/react-query.test.tsx b/packages/clients/tanstack-query/test/react-query.test.tsx index fef6a62a..8c68d71e 100644 --- a/packages/clients/tanstack-query/test/react-query.test.tsx +++ b/packages/clients/tanstack-query/test/react-query.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { afterEach, describe, expect, it } from 'vitest'; import { QuerySettingsProvider, useClientQueries } from '../src/react'; import { getQueryKey } from '../src/utils/common'; -import { schema } from './schemas/basic/schema'; +import { schema } from './schemas/basic/schema-lite'; const BASE_URL = 'http://localhost'; @@ -18,7 +18,7 @@ describe('React Query Test', () => { const queryClient = new QueryClient({ defaultOptions: { queries: { - experimental_prefetchInRender: true, + retry: false, }, }, }); @@ -66,9 +66,45 @@ describe('React Query Test', () => { const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); expect(cacheData).toMatchObject(data); }); + + nock(makeUrl('User', 'findFirst', queryArgs)) + .get(/.*/) + .reply(404, () => { + return { error: 'Not Found' }; + }); + const { result: errorResult } = renderHook(() => useClientQueries(schema).user.useFindFirst(queryArgs), { + wrapper, + }); + await waitFor(() => { + expect(errorResult.current.isError).toBe(true); + }); + }); + + it('works with suspense query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: '1' } }; + const data = { id: '1', name: 'foo' }; + + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, { + data, + }); + + const { result } = renderHook(() => useClientQueries(schema).user.useSuspenseFindUnique(queryArgs), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data).toMatchObject(data); + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); + expect(cacheData).toMatchObject(data); + }); }); - it('infinite query', async () => { + it('works with infinite query', async () => { const { queryClient, wrapper } = createWrapper(); const queryArgs = { where: { id: '1' } }; @@ -104,7 +140,43 @@ describe('React Query Test', () => { }); }); - it('independent mutation and query', async () => { + it('works with suspense infinite query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: '1' } }; + const data = [{ id: '1', name: 'foo' }]; + + nock(makeUrl('User', 'findMany', queryArgs)) + .get(/.*/) + .reply(200, () => ({ + data, + })); + + const { result } = renderHook( + () => + useClientQueries(schema).user.useSuspenseInfiniteFindMany(queryArgs, { + getNextPageParam: () => null, + }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + const resultData = result.current.data!; + expect(resultData.pages).toHaveLength(1); + expect(resultData.pages[0]).toMatchObject(data); + expect(resultData?.pageParams).toHaveLength(1); + expect(resultData?.pageParams[0]).toMatchObject(queryArgs); + expect(result.current.hasNextPage).toBe(false); + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', queryArgs, { infinite: true, optimisticUpdate: false }), + ); + expect(cacheData.pages[0]).toMatchObject(data); + }); + }); + + it('works with independent mutation and query', async () => { const { wrapper } = createWrapper(); const queryArgs = { where: { id: '1' } }; @@ -144,7 +216,7 @@ describe('React Query Test', () => { }); }); - it('create and invalidation', async () => { + it('works with create and invalidation', async () => { const { queryClient, wrapper } = createWrapper(); const data: any[] = []; @@ -180,7 +252,7 @@ describe('React Query Test', () => { }); }); - it('create and no invalidation', async () => { + it('works with create and no invalidation', async () => { const { queryClient, wrapper } = createWrapper(); const data: any[] = []; @@ -216,7 +288,7 @@ describe('React Query Test', () => { }); }); - it('optimistic create single', async () => { + it('works with optimistic create single', async () => { const { queryClient, wrapper } = createWrapper(); const data: any[] = []; @@ -266,7 +338,7 @@ describe('React Query Test', () => { }); }); - it('optimistic create updating nested query', async () => { + it('works with optimistic create updating nested query', async () => { const { queryClient, wrapper } = createWrapper(); const data: any[] = [{ id: '1', name: 'user1', posts: [] }]; @@ -326,7 +398,7 @@ describe('React Query Test', () => { }); }); - it('optimistic create updating deeply nested query', async () => { + it('works with optimistic create updating deeply nested query', async () => { const { queryClient, wrapper } = createWrapper(); // populate the cache with a user @@ -468,7 +540,7 @@ describe('React Query Test', () => { }); }); - it('optimistic update with optional one-to-many relationship', async () => { + it('works with optimistic update with optional one-to-many relationship', async () => { const { queryClient, wrapper } = createWrapper(); // populate the cache with a post, with an optional category relationship @@ -556,7 +628,7 @@ describe('React Query Test', () => { }); }); - it('optimistic update with nested optional one-to-many relationship', async () => { + it('works with optimistic update with nested optional one-to-many relationship', async () => { const { queryClient, wrapper } = createWrapper(); // populate the cache with a user and a post, with an optional category @@ -652,7 +724,7 @@ describe('React Query Test', () => { }); }); - it('optimistic nested create updating query', async () => { + it('works with optimistic nested create updating query', async () => { const { queryClient, wrapper } = createWrapper(); const data: any[] = []; @@ -704,7 +776,7 @@ describe('React Query Test', () => { }); }); - it('optimistic create many', async () => { + it('works with optimistic create many', async () => { const { queryClient, wrapper } = createWrapper(); const data: any[] = []; @@ -753,7 +825,7 @@ describe('React Query Test', () => { }); }); - it('update and invalidation', async () => { + it('works with update and invalidation', async () => { const { queryClient, wrapper } = createWrapper(); const queryArgs = { where: { id: '1' } }; @@ -792,7 +864,7 @@ describe('React Query Test', () => { }); }); - it('update and no invalidation', async () => { + it('works with update and no invalidation', async () => { const { queryClient, wrapper } = createWrapper(); const queryArgs = { where: { id: '1' } }; @@ -829,7 +901,7 @@ describe('React Query Test', () => { }); }); - it('optimistic update simple', async () => { + it('works with optimistic update simple', async () => { const { queryClient, wrapper } = createWrapper(); const queryArgs = { where: { id: '1' } }; @@ -877,7 +949,7 @@ describe('React Query Test', () => { }); }); - it('optimistic update updating nested query', async () => { + it('works with optimistic update updating nested query', async () => { const { queryClient, wrapper } = createWrapper(); const queryArgs = { where: { id: '1' }, include: { posts: true } }; @@ -928,7 +1000,7 @@ describe('React Query Test', () => { }); }); - it('optimistic nested update updating query', async () => { + it('works with optimistic nested update updating query', async () => { const { queryClient, wrapper } = createWrapper(); const queryArgs = { where: { id: 'p1' } }; @@ -979,7 +1051,7 @@ describe('React Query Test', () => { }); }); - it('optimistic upsert - create simple', async () => { + it('works with optimistic upsert - create simple', async () => { const { queryClient, wrapper } = createWrapper(); const data: any[] = []; @@ -1031,7 +1103,7 @@ describe('React Query Test', () => { }); }); - it('optimistic upsert - create updating nested query', async () => { + it('works with optimistic upsert - create updating nested query', async () => { const { queryClient, wrapper } = createWrapper(); const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] }; @@ -1084,7 +1156,7 @@ describe('React Query Test', () => { }); }); - it('optimistic upsert - nested create updating query', async () => { + it('works with optimistic upsert - nested create updating query', async () => { const { queryClient, wrapper } = createWrapper(); const data: any = [{ id: 'p1', title: 'post1' }]; @@ -1143,7 +1215,7 @@ describe('React Query Test', () => { }); }); - it('optimistic upsert - update simple', async () => { + it('works with optimistic upsert - update simple', async () => { const { queryClient, wrapper } = createWrapper(); const queryArgs = { where: { id: '1' } }; @@ -1189,7 +1261,7 @@ describe('React Query Test', () => { }); }); - it('optimistic upsert - update updating nested query', async () => { + it('works with optimistic upsert - update updating nested query', async () => { const { queryClient, wrapper } = createWrapper(); const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] }; @@ -1242,7 +1314,7 @@ describe('React Query Test', () => { }); }); - it('optimistic upsert - nested update updating query', async () => { + it('works with optimistic upsert - nested update updating query', async () => { const { queryClient, wrapper } = createWrapper(); const data: any = [{ id: 'p1', title: 'post1' }]; @@ -1301,7 +1373,7 @@ describe('React Query Test', () => { }); }); - it('delete and invalidation', async () => { + it('works with delete and invalidation', async () => { const { queryClient, wrapper } = createWrapper(); const data: any[] = [{ id: '1', name: 'foo' }]; @@ -1337,7 +1409,7 @@ describe('React Query Test', () => { }); }); - it('optimistic delete simple', async () => { + it('works with optimistic delete simple', async () => { const { queryClient, wrapper } = createWrapper(); const data: any[] = [{ id: '1', name: 'foo' }]; @@ -1382,7 +1454,7 @@ describe('React Query Test', () => { }); }); - it('optimistic delete nested query', async () => { + it('works with optimistic delete nested query', async () => { const { queryClient, wrapper } = createWrapper(); const data: any = { id: '1', name: 'foo', posts: [{ id: 'p1', title: 'post1' }] }; @@ -1438,7 +1510,7 @@ describe('React Query Test', () => { }); }); - it('optimistic nested delete update query', async () => { + it('works with optimistic nested delete update query', async () => { const { queryClient, wrapper } = createWrapper(); const data: any = [ @@ -1507,7 +1579,7 @@ describe('React Query Test', () => { nock(makeUrl('Post', 'update')) .put(/.*/) .reply(200, () => { - data.posts[0].title = 'post2'; + data.posts[0]!.title = 'post2'; return data; }); diff --git a/packages/clients/tanstack-query/test/schemas/basic/input.ts b/packages/clients/tanstack-query/test/schemas/basic/input.ts index ad01c7b9..e7b6da66 100644 --- a/packages/clients/tanstack-query/test/schemas/basic/input.ts +++ b/packages/clients/tanstack-query/test/schemas/basic/input.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { type SchemaType as $Schema } from "./schema"; +import { type SchemaType as $Schema } from "./schema-lite"; 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/orm"; import type { SimplifiedModelResult as $SimplifiedModelResult, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/orm"; export type UserFindManyArgs = $FindManyArgs<$Schema, "User">; diff --git a/packages/clients/tanstack-query/test/schemas/basic/models.ts b/packages/clients/tanstack-query/test/schemas/basic/models.ts index 7a4b7eb4..da3e5d00 100644 --- a/packages/clients/tanstack-query/test/schemas/basic/models.ts +++ b/packages/clients/tanstack-query/test/schemas/basic/models.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { type SchemaType as $Schema } from "./schema"; +import { type SchemaType as $Schema } from "./schema-lite"; import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Post = $ModelResult<$Schema, "Post">; diff --git a/packages/clients/tanstack-query/test/schemas/basic/schema.ts b/packages/clients/tanstack-query/test/schemas/basic/schema-lite.ts similarity index 78% rename from packages/clients/tanstack-query/test/schemas/basic/schema.ts rename to packages/clients/tanstack-query/test/schemas/basic/schema-lite.ts index b56d0b5e..347ccbe1 100644 --- a/packages/clients/tanstack-query/test/schemas/basic/schema.ts +++ b/packages/clients/tanstack-query/test/schemas/basic/schema-lite.ts @@ -18,14 +18,12 @@ export const schema = { name: "id", type: "String", id: true, - attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], default: ExpressionUtils.call("cuid") }, email: { name: "email", type: "String", - unique: true, - attributes: [{ name: "@unique" }] + unique: true }, name: { name: "name", @@ -52,7 +50,6 @@ export const schema = { name: "id", type: "String", id: true, - attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], default: ExpressionUtils.call("cuid") }, title: { @@ -63,7 +60,6 @@ export const schema = { name: "owner", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], relation: { opposite: "posts", fields: ["ownerId"], references: ["id"] } }, ownerId: { @@ -78,7 +74,6 @@ export const schema = { name: "category", type: "Category", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("categoryId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], relation: { opposite: "posts", fields: ["categoryId"], references: ["id"] } }, categoryId: { @@ -102,14 +97,12 @@ export const schema = { name: "id", type: "String", id: true, - attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], default: ExpressionUtils.call("cuid") }, name: { name: "name", type: "String", - unique: true, - attributes: [{ name: "@unique" }] + unique: true }, posts: { name: "posts", diff --git a/packages/clients/tanstack-query/tsconfig.json b/packages/clients/tanstack-query/tsconfig.json index 53cf6cf5..9dd323f1 100644 --- a/packages/clients/tanstack-query/tsconfig.json +++ b/packages/clients/tanstack-query/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "include": ["src/**/*.ts", "test/**/*.ts"], + "include": ["src/**/*.ts"], "compilerOptions": { "lib": ["ESNext"] } diff --git a/packages/clients/tanstack-query/tsconfig.test.json b/packages/clients/tanstack-query/tsconfig.test.json new file mode 100644 index 00000000..551979d5 --- /dev/null +++ b/packages/clients/tanstack-query/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "@zenstackhq/typescript-config/base.json", + "include": ["src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"], + "compilerOptions": { + "lib": ["ESNext"], + "jsx": "react" + } +} diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index c2a02435..f56b4e23 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -51,44 +51,70 @@ import { isUniqueField, } from './model-utils'; +export type TsSchemaGeneratorOptions = { + outDir: string; + lite?: boolean; + liteOnly?: boolean; +}; + export class TsSchemaGenerator { private usedExpressionUtils = false; - async generate(model: Model, outputDir: string) { - fs.mkdirSync(outputDir, { recursive: true }); + async generate(model: Model, options: TsSchemaGeneratorOptions) { + fs.mkdirSync(options.outDir, { recursive: true }); // Reset the flag for each generation this.usedExpressionUtils = false; // the schema itself - this.generateSchema(model, outputDir); + this.generateSchema(model, options); // the model types - this.generateModelsAndTypeDefs(model, outputDir); + this.generateModelsAndTypeDefs(model, options); // the input types - this.generateInputTypes(model, outputDir); + this.generateInputTypes(model, options); } - private generateSchema(model: Model, outputDir: string) { - const statements: ts.Statement[] = []; - this.generateSchemaStatements(model, statements); - this.generateBannerComments(statements); + private generateSchema(model: Model, options: TsSchemaGeneratorOptions) { + const targets: { lite: boolean; file: string }[] = []; + if (!options.liteOnly) { + targets.push({ lite: false, file: 'schema.ts' }); + } + if (options.lite || options.liteOnly) { + targets.push({ lite: true, file: 'schema-lite.ts' }); + } - const schemaOutputFile = path.join(outputDir, 'schema.ts'); - const sourceFile = ts.createSourceFile(schemaOutputFile, '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); - const printer = ts.createPrinter(); - const result = printer.printList(ts.ListFormat.MultiLine, ts.factory.createNodeArray(statements), sourceFile); - fs.writeFileSync(schemaOutputFile, result); + for (const { lite, file } of targets) { + const statements: ts.Statement[] = []; + this.generateSchemaStatements(model, statements, lite); + this.generateBannerComments(statements); + + const schemaOutputFile = path.join(options.outDir, file); + const sourceFile = ts.createSourceFile( + schemaOutputFile, + '', + ts.ScriptTarget.ESNext, + false, + ts.ScriptKind.TS, + ); + const printer = ts.createPrinter(); + const result = printer.printList( + ts.ListFormat.MultiLine, + ts.factory.createNodeArray(statements), + sourceFile, + ); + fs.writeFileSync(schemaOutputFile, result); + } } - private generateSchemaStatements(model: Model, statements: ts.Statement[]) { + private generateSchemaStatements(model: Model, statements: ts.Statement[], lite: boolean) { const hasComputedFields = model.declarations.some( (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); + const schemaObject = this.createSchemaObject(model, lite); // Now generate the import declaration with the correct imports const runtimeImportDecl = ts.factory.createImportDeclaration( @@ -160,17 +186,17 @@ export class TsSchemaGenerator { ); } - private createSchemaObject(model: Model) { + private createSchemaObject(model: Model, lite: boolean): ts.Expression { const properties: ts.PropertyAssignment[] = [ // provider ts.factory.createPropertyAssignment('provider', this.createProviderObject(model)), // models - ts.factory.createPropertyAssignment('models', this.createModelsObject(model)), + ts.factory.createPropertyAssignment('models', this.createModelsObject(model, lite)), // typeDefs ...(model.declarations.some(isTypeDef) - ? [ts.factory.createPropertyAssignment('typeDefs', this.createTypeDefsObject(model))] + ? [ts.factory.createPropertyAssignment('typeDefs', this.createTypeDefsObject(model, lite))] : []), ]; @@ -216,33 +242,35 @@ export class TsSchemaGenerator { ); } - private createModelsObject(model: Model) { + private createModelsObject(model: Model, lite: boolean): ts.Expression { return ts.factory.createObjectLiteralExpression( model.declarations .filter((d): d is DataModel => isDataModel(d) && !hasAttribute(d, '@@ignore')) - .map((dm) => ts.factory.createPropertyAssignment(dm.name, this.createDataModelObject(dm))), + .map((dm) => ts.factory.createPropertyAssignment(dm.name, this.createDataModelObject(dm, lite))), true, ); } - private createTypeDefsObject(model: Model): ts.Expression { + private createTypeDefsObject(model: Model, lite: boolean): ts.Expression { return ts.factory.createObjectLiteralExpression( model.declarations .filter((d): d is TypeDef => isTypeDef(d)) - .map((td) => ts.factory.createPropertyAssignment(td.name, this.createTypeDefObject(td))), + .map((td) => ts.factory.createPropertyAssignment(td.name, this.createTypeDefObject(td, lite))), true, ); } - private createDataModelObject(dm: DataModel) { + private createDataModelObject(dm: DataModel, lite: boolean) { const allFields = getAllFields(dm); - const allAttributes = getAllAttributes(dm).filter((attr) => { - // exclude `@@delegate` attribute from base model - if (attr.decl.$refText === '@@delegate' && attr.$container !== dm) { - return false; - } - return true; - }); + const allAttributes = lite + ? [] // in lite mode, skip all model-level attributes + : getAllAttributes(dm).filter((attr) => { + // exclude `@@delegate` attribute from base model + if (attr.decl.$refText === '@@delegate' && attr.$container !== dm) { + return false; + } + return true; + }); const subModels = this.getSubModels(dm); const fields: ts.PropertyAssignment[] = [ @@ -264,7 +292,7 @@ export class TsSchemaGenerator { 'fields', ts.factory.createObjectLiteralExpression( allFields.map((field) => - ts.factory.createPropertyAssignment(field.name, this.createDataFieldObject(field, dm)), + ts.factory.createPropertyAssignment(field.name, this.createDataFieldObject(field, dm, lite)), ), true, ), @@ -332,7 +360,7 @@ export class TsSchemaGenerator { .map((d) => d.name); } - private createTypeDefObject(td: TypeDef): ts.Expression { + private createTypeDefObject(td: TypeDef, lite: boolean): ts.Expression { const allFields = getAllFields(td); const allAttributes = getAllAttributes(td); @@ -345,7 +373,10 @@ export class TsSchemaGenerator { 'fields', ts.factory.createObjectLiteralExpression( allFields.map((field) => - ts.factory.createPropertyAssignment(field.name, this.createDataFieldObject(field, undefined)), + ts.factory.createPropertyAssignment( + field.name, + this.createDataFieldObject(field, undefined, lite), + ), ), true, ), @@ -432,7 +463,7 @@ export class TsSchemaGenerator { return result; } - private createDataFieldObject(field: DataField, contextModel: DataModel | undefined) { + private createDataFieldObject(field: DataField, contextModel: DataModel | undefined, lite: boolean) { const objectFields = [ // name ts.factory.createPropertyAssignment('name', ts.factory.createStringLiteral(field.name)), @@ -482,8 +513,8 @@ export class TsSchemaGenerator { objectFields.push(ts.factory.createPropertyAssignment('isDiscriminator', ts.factory.createTrue())); } - // attributes - if (field.attributes.length > 0) { + // attributes, only when not in lite mode + if (!lite && field.attributes.length > 0) { objectFields.push( ts.factory.createPropertyAssignment( 'attributes', @@ -1110,11 +1141,11 @@ export class TsSchemaGenerator { }); } - private generateModelsAndTypeDefs(model: Model, outputDir: string) { + private generateModelsAndTypeDefs(model: Model, options: TsSchemaGeneratorOptions) { const statements: ts.Statement[] = []; // generate: import { schema as $schema, type SchemaType as $Schema } from './schema'; - statements.push(this.generateSchemaImport(model, true, true)); + statements.push(this.generateSchemaImport(model, true, true, !!(options.lite || options.liteOnly))); // generate: import type { ModelResult as $ModelResult } from '@zenstackhq/orm'; statements.push( @@ -1230,14 +1261,14 @@ export class TsSchemaGenerator { this.generateBannerComments(statements); // write to file - const outputFile = path.join(outputDir, 'models.ts'); + const outputFile = path.join(options.outDir, 'models.ts'); const sourceFile = ts.createSourceFile(outputFile, '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); const printer = ts.createPrinter(); const result = printer.printList(ts.ListFormat.MultiLine, ts.factory.createNodeArray(statements), sourceFile); fs.writeFileSync(outputFile, result); } - private generateSchemaImport(model: Model, schemaObject: boolean, schemaType: boolean) { + private generateSchemaImport(model: Model, schemaObject: boolean, schemaType: boolean, useLite: boolean) { const importSpecifiers = []; if (schemaObject) { @@ -1266,7 +1297,7 @@ export class TsSchemaGenerator { return ts.factory.createImportDeclaration( undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports(importSpecifiers)), - ts.factory.createStringLiteral('./schema'), + ts.factory.createStringLiteral(useLite ? './schema-lite' : './schema'), ); } @@ -1282,12 +1313,12 @@ export class TsSchemaGenerator { ); } - private generateInputTypes(model: Model, outputDir: string) { + private generateInputTypes(model: Model, options: TsSchemaGeneratorOptions) { const dataModels = model.declarations.filter(isDataModel); const statements: ts.Statement[] = []; // generate: import { SchemaType as $Schema } from './schema'; - statements.push(this.generateSchemaImport(model, false, true)); + statements.push(this.generateSchemaImport(model, false, true, !!(options.lite || options.liteOnly))); // generate: import { CreateArgs as $CreateArgs, ... } from '@zenstackhq/orm'; const inputTypes = [ @@ -1410,7 +1441,7 @@ export class TsSchemaGenerator { this.generateBannerComments(statements); // write to file - const outputFile = path.join(outputDir, 'input.ts'); + const outputFile = path.join(options.outDir, 'input.ts'); const sourceFile = ts.createSourceFile(outputFile, '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); const printer = ts.createPrinter(); const result = printer.printList(ts.ListFormat.MultiLine, ts.factory.createNodeArray(statements), sourceFile); diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 3506d11f..c805cb95 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -1,6 +1,6 @@ import { invariant } from '@zenstackhq/common-helpers'; -import { TsSchemaGenerator } from '@zenstackhq/sdk'; import type { SchemaDef } from '@zenstackhq/schema'; +import { TsSchemaGenerator } from '@zenstackhq/sdk'; import { execSync } from 'node:child_process'; import crypto from 'node:crypto'; import fs from 'node:fs'; @@ -37,6 +37,7 @@ export async function generateTsSchema( provider: 'sqlite' | 'postgresql' = 'sqlite', dbUrl?: string, extraSourceFiles?: Record, + withLiteSchema?: boolean, ) { const workDir = createTestProject(); @@ -50,7 +51,7 @@ export async function generateTsSchema( } const generator = new TsSchemaGenerator(); - await generator.generate(result.model, workDir); + await generator.generate(result.model, { outDir: workDir, lite: withLiteSchema }); if (extraSourceFiles) { for (const [fileName, content] of Object.entries(extraSourceFiles)) { @@ -72,7 +73,14 @@ async function compileAndLoad(workDir: string) { // load the schema module const module = await import(path.join(workDir, 'schema.js')); - return { workDir, schema: module.schema as SchemaDef }; + + let moduleLite: any; + try { + moduleLite = await import(path.join(workDir, 'schema-lite.js')); + } catch { + // ignore + } + return { workDir, schema: module.schema as SchemaDef, schemaLite: moduleLite?.schema as SchemaDef | undefined }; } export function generateTsSchemaFromFile(filePath: string) { @@ -87,7 +95,7 @@ export async function generateTsSchemaInPlace(schemaPath: string) { throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`); } const generator = new TsSchemaGenerator(); - await generator.generate(result.model, workDir); + await generator.generate(result.model, { outDir: workDir }); return compileAndLoad(workDir); } diff --git a/samples/next.js/app/page.tsx b/samples/next.js/app/page.tsx index b411313c..86300fbe 100644 --- a/samples/next.js/app/page.tsx +++ b/samples/next.js/app/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { schema } from '@/zenstack/schema'; +import { schema } from '@/zenstack/schema-lite'; import { useClientQueries } from '@zenstackhq/tanstack-query/react'; import { LoremIpsum } from 'lorem-ipsum'; import Image from 'next/image'; diff --git a/samples/next.js/package.json b/samples/next.js/package.json index 5dcd3e4f..911fbb0a 100644 --- a/samples/next.js/package.json +++ b/samples/next.js/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "generate": "zen generate", + "generate": "zen generate --lite", "db:init": "pnpm generate && zen db push && npx tsx zenstack/seed.ts", "dev": "next dev", "build": "pnpm generate && next build", diff --git a/samples/next.js/zenstack/input.ts b/samples/next.js/zenstack/input.ts index 6c876632..31122290 100644 --- a/samples/next.js/zenstack/input.ts +++ b/samples/next.js/zenstack/input.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { type SchemaType as $Schema } from "./schema"; +import { type SchemaType as $Schema } from "./schema-lite"; 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/orm"; import type { SimplifiedModelResult as $SimplifiedModelResult, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/orm"; export type UserFindManyArgs = $FindManyArgs<$Schema, "User">; diff --git a/samples/next.js/zenstack/models.ts b/samples/next.js/zenstack/models.ts index 80be7341..3314c7d4 100644 --- a/samples/next.js/zenstack/models.ts +++ b/samples/next.js/zenstack/models.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { type SchemaType as $Schema } from "./schema"; +import { type SchemaType as $Schema } from "./schema-lite"; import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; /** * User model diff --git a/samples/next.js/zenstack/schema-lite.ts b/samples/next.js/zenstack/schema-lite.ts new file mode 100644 index 00000000..87909373 --- /dev/null +++ b/samples/next.js/zenstack/schema-lite.ts @@ -0,0 +1,106 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, ExpressionUtils } from "@zenstackhq/orm/schema"; +export const schema = { + provider: { + type: "sqlite" + }, + models: { + User: { + name: "User", + fields: { + id: { + name: "id", + type: "String", + id: true, + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true + }, + email: { + name: "email", + type: "String", + unique: true + }, + name: { + name: "name", + type: "String", + optional: true + }, + posts: { + name: "posts", + type: "Post", + array: true, + relation: { opposite: "author" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" }, + email: { type: "String" } + } + }, + Post: { + name: "Post", + fields: { + id: { + name: "id", + type: "String", + id: true, + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true + }, + title: { + name: "title", + type: "String" + }, + published: { + name: "published", + type: "Boolean", + default: false + }, + author: { + name: "author", + type: "User", + relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } + }, + authorId: { + name: "authorId", + type: "String", + foreignKeyFor: [ + "author" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + } + }, + authType: "User", + plugins: {} +} as const satisfies SchemaDef; +export type SchemaType = typeof schema; diff --git a/tests/e2e/orm/scripts/generate.ts b/tests/e2e/orm/scripts/generate.ts index 9d59db73..51ce6b5b 100644 --- a/tests/e2e/orm/scripts/generate.ts +++ b/tests/e2e/orm/scripts/generate.ts @@ -17,7 +17,7 @@ async function main() { async function generate(schemaPath: string) { const generator = new TsSchemaGenerator(); - const outputDir = path.dirname(schemaPath); + const outDir = path.dirname(schemaPath); // isomorphic __dirname const _dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); @@ -29,7 +29,7 @@ async function generate(schemaPath: string) { if (!result.success) { throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`); } - await generator.generate(result.model as Model, outputDir); + await generator.generate(result.model as Model, { outDir }); } main(); diff --git a/tests/regression/generate.ts b/tests/regression/generate.ts index 07cbab0d..1a0959ed 100644 --- a/tests/regression/generate.ts +++ b/tests/regression/generate.ts @@ -16,12 +16,12 @@ async function main() { async function generate(schemaPath: string) { const generator = new TsSchemaGenerator(); - const outputDir = path.dirname(schemaPath); + const outDir = path.dirname(schemaPath); const result = await loadDocument(schemaPath); if (!result.success) { throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`); } - await generator.generate(result.model, outputDir); + await generator.generate(result.model, { outDir }); } main();