diff --git a/package.json b/package.json index bcc824c4c..6bf6bd622 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.4.1", + "version": "3.4.2", "description": "ZenStack", "packageManager": "pnpm@10.23.0", "type": "module", diff --git a/packages/auth-adapters/better-auth/package.json b/packages/auth-adapters/better-auth/package.json index 563026f95..974d52acb 100644 --- a/packages/auth-adapters/better-auth/package.json +++ b/packages/auth-adapters/better-auth/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/better-auth", - "version": "3.4.1", + "version": "3.4.2", "description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.", "type": "module", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 3295a006b..c084b0403 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack CLI", "description": "FullStack database toolkit with built-in access control and automatic API generation.", - "version": "3.4.1", + "version": "3.4.2", "type": "module", "author": { "name": "ZenStack Team" diff --git a/packages/cli/src/actions/proxy.ts b/packages/cli/src/actions/proxy.ts index 8e0a7e362..5e4f9464d 100644 --- a/packages/cli/src/actions/proxy.ts +++ b/packages/cli/src/actions/proxy.ts @@ -69,7 +69,9 @@ export async function run(options: Options) { const dialect = await createDialect(provider, databaseUrl!, outputPath); - const jiti = createJiti(import.meta.url); + const fileUrl = typeof __filename !== 'undefined' ? __filename : import.meta.url; + + const jiti = createJiti(fileUrl); const schemaModule = (await jiti.import(path.join(outputPath, 'schema'))) as any; @@ -92,6 +94,7 @@ export async function run(options: Options) { dialect: dialect, log: log && log.length > 0 ? log : undefined, omit: Object.keys(omit).length > 0 ? omit : undefined, + skipValidationForComputedFields: true, }); // check whether the database is reachable @@ -195,7 +198,7 @@ async function createDialect(provider: string, databaseUrl: string, outputPath: } } -function startServer(client: ClientContract, schema: any, options: Options) { +export function createProxyApp(client: ClientContract, schema: any): express.Application { const app = express(); app.use(cors()); app.use(express.json({ limit: '5mb' })); @@ -213,6 +216,12 @@ function startServer(client: ClientContract, schema: any, options: Opt res.json({ ...schema, zenstackVersion: getVersion() }); }); + return app; +} + +function startServer(client: ClientContract, schema: any, options: Options) { + const app = createProxyApp(client, schema); + const server = app.listen(options.port, () => { console.log(`ZenStack proxy server is running on port: ${options.port}`); console.log(`You can visit ZenStack Studio at: ${colors.blue('https://studio.zenstack.dev')}`); diff --git a/packages/cli/test/proxy.test.ts b/packages/cli/test/proxy.test.ts new file mode 100644 index 000000000..840412f23 --- /dev/null +++ b/packages/cli/test/proxy.test.ts @@ -0,0 +1,170 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import http from 'node:http'; +import { afterEach, describe, expect, it } from 'vitest'; +import { createProxyApp } from '../src/actions/proxy'; + +describe('CLI proxy tests', () => { + let server: http.Server | undefined; + + afterEach(async () => { + await new Promise((resolve) => { + if (server) { + server.close(() => resolve()); + server = undefined; + } else { + resolve(); + } + }); + }); + + async function startAt(app: ReturnType): Promise { + return new Promise((resolve) => { + server = app.listen(0, () => { + const addr = server!.address() as { port: number }; + resolve(`http://localhost:${addr.port}`); + }); + }); + } + + it('should serve schema at /api/schema endpoint', async () => { + const zmodel = ` + model User { + id String @id @default(cuid()) + email String @unique + } + `; + + const client = await createTestClient(zmodel); + const app = createProxyApp(client, client.$schema); + const baseUrl = await startAt(app); + + const r = await fetch(`${baseUrl}/api/schema`); + expect(r.status).toBe(200); + + const body = await r.json(); + // schema fields are present + expect(body).toHaveProperty('models'); + expect(body.models).toHaveProperty('User'); + expect(body).toHaveProperty('provider'); + // zenstackVersion is injected by the proxy; when running tests directly + // from source (no built dist/) getVersion() returns undefined and the + // key is omitted from JSON — tolerate that, but if present it must be a string. + if ('zenstackVersion' in body) { + expect(typeof body.zenstackVersion).toBe('string'); + } + }); + + it('should omit computed fields from default query responses', async () => { + // postCount is a @computed field — the proxy must not try to SELECT it + // by default (it has no backing column in the DB). + const zmodel = ` + model User { + id String @id @default(cuid()) + name String + postCount Int @computed + } + `; + + // Mirror what proxy.ts does: build omit config from the schema, then + // create the client with skipValidationForComputedFields. + const client = await createTestClient(zmodel, { + skipValidationForComputedFields: true, + omit: { User: { postCount: true } }, + }); + + const app = createProxyApp(client, client.$schema); + const baseUrl = await startAt(app); + + // Create a user via the proxy API. + const createRes = await fetch(`${baseUrl}/api/model/user/create`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data: { name: 'Alice' } }), + }); + expect(createRes.status).toBe(201); + const created = await createRes.json(); + + // The regular fields should be present … + expect(created.data).toHaveProperty('id'); + expect(created.data).toHaveProperty('name', 'Alice'); + // … but the computed field must be absent in the default response. + expect(created.data).not.toHaveProperty('postCount'); + + // A findMany should behave the same way. + const listRes = await fetch(`${baseUrl}/api/model/user/findMany`); + expect(listRes.status).toBe(200); + const list = await listRes.json(); + expect(list.data).toHaveLength(1); + expect(list.data[0]).not.toHaveProperty('postCount'); + }); + + it('should handle sequential transaction calls', async () => { + const zmodel = ` + model User { + id String @id @default(cuid()) + email String @unique + posts Post[] + + @@allow('all', true) + } + + model Post { + id String @id @default(cuid()) + title String + author User? @relation(fields: [authorId], references: [id]) + authorId String? + + @@allow('all', true) + } + `; + + const client = await createTestClient(zmodel); + const app = createProxyApp(client, client.$schema); + const baseUrl = await startAt(app); + + const txRes = await fetch(`${baseUrl}/api/model/$transaction/sequential`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify([ + { + model: 'User', + op: 'create', + args: { data: { id: 'u1', email: 'alice@example.com' } }, + }, + { + model: 'Post', + op: 'create', + args: { data: { id: 'p1', title: 'Hello World', authorId: 'u1' } }, + }, + { + model: 'Post', + op: 'findMany', + args: { where: { authorId: 'u1' } }, + }, + ]), + }); + expect(txRes.status).toBe(200); + const tx = await txRes.json(); + + // Should return results for each operation in the transaction. + expect(Array.isArray(tx.data)).toBe(true); + expect(tx.data).toHaveLength(3); + + // First result: created user + expect(tx.data[0]).toMatchObject({ id: 'u1', email: 'alice@example.com' }); + // Second result: created post + expect(tx.data[1]).toMatchObject({ id: 'p1', title: 'Hello World', authorId: 'u1' }); + // Third result: findMany — should find the newly created post + expect(Array.isArray(tx.data[2])).toBe(true); + expect(tx.data[2]).toHaveLength(1); + expect(tx.data[2][0]).toMatchObject({ id: 'p1', title: 'Hello World' }); + + // Confirm persisted outside transaction too. + const userRes = await fetch( + `${baseUrl}/api/model/user/findUnique?q=${encodeURIComponent(JSON.stringify({ where: { id: 'u1' } }))}`, + ); + expect(userRes.status).toBe(200); + const user = await userRes.json(); + expect(user.data).toMatchObject({ id: 'u1', email: 'alice@example.com' }); + }); +}); diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts index 2496f3eab..4933cce22 100644 --- a/packages/cli/tsup.config.ts +++ b/packages/cli/tsup.config.ts @@ -10,4 +10,9 @@ export default defineConfig({ clean: true, dts: true, format: ['esm', 'cjs'], + esbuildOptions: (options) => { + options.logOverride = { + 'empty-import-meta': 'silent', + }; + }, }); diff --git a/packages/clients/client-helpers/package.json b/packages/clients/client-helpers/package.json index e425b3c21..d890e4a1f 100644 --- a/packages/clients/client-helpers/package.json +++ b/packages/clients/client-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/client-helpers", - "version": "3.4.1", + "version": "3.4.2", "description": "Helpers for implementing clients that consume ZenStack's CRUD service", "type": "module", "scripts": { diff --git a/packages/clients/tanstack-query/package.json b/packages/clients/tanstack-query/package.json index 0088814c1..75c769c71 100644 --- a/packages/clients/tanstack-query/package.json +++ b/packages/clients/tanstack-query/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/tanstack-query", - "version": "3.4.1", + "version": "3.4.2", "description": "TanStack Query Client for consuming ZenStack v3's CRUD service", "type": "module", "scripts": { diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index c8deaf729..c4280f4c2 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/common-helpers", - "version": "3.4.1", + "version": "3.4.2", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/config/eslint-config/package.json b/packages/config/eslint-config/package.json index 616be2206..5ed6a2fa6 100644 --- a/packages/config/eslint-config/package.json +++ b/packages/config/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.4.1", + "version": "3.4.2", "type": "module", "private": true, "license": "MIT" diff --git a/packages/config/typescript-config/package.json b/packages/config/typescript-config/package.json index 65aad51ac..594812ff1 100644 --- a/packages/config/typescript-config/package.json +++ b/packages/config/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.4.1", + "version": "3.4.2", "private": true, "license": "MIT" } diff --git a/packages/config/vitest-config/package.json b/packages/config/vitest-config/package.json index 6512cd99a..6cfa81d9e 100644 --- a/packages/config/vitest-config/package.json +++ b/packages/config/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.4.1", + "version": "3.4.2", "private": true, "license": "MIT", "exports": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index 95b12482e..15db4dd6d 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.4.1", + "version": "3.4.2", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 7d479764b..7c9163405 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack-v3", "publisher": "zenstack", - "version": "3.4.1", + "version": "3.4.2", "displayName": "ZenStack V3 Language Tools", "description": "VSCode extension for ZenStack (v3) ZModel language", "private": true, diff --git a/packages/language/package.json b/packages/language/package.json index 6c52e14cd..ad5237c09 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/language", "description": "ZenStack ZModel language specification", - "version": "3.4.1", + "version": "3.4.2", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/orm/package.json b/packages/orm/package.json index c42ebeb7c..94740879c 100644 --- a/packages/orm/package.json +++ b/packages/orm/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/orm", - "version": "3.4.1", + "version": "3.4.2", "description": "ZenStack ORM", "type": "module", "scripts": { diff --git a/packages/orm/src/client/client-impl.ts b/packages/orm/src/client/client-impl.ts index 1aa289aa4..1046d0ca8 100644 --- a/packages/orm/src/client/client-impl.ts +++ b/packages/orm/src/client/client-impl.ts @@ -75,7 +75,7 @@ export class ClientImpl { ...this.$options.functions, }; - if (!baseClient) { + if (!baseClient && !options.skipValidationForComputedFields) { // validate computed fields configuration once for the root client this.validateComputedFieldsConfig(); } diff --git a/packages/orm/src/client/options.ts b/packages/orm/src/client/options.ts index 4601598b2..111946d3b 100644 --- a/packages/orm/src/client/options.ts +++ b/packages/orm/src/client/options.ts @@ -203,6 +203,11 @@ export type ClientOptions = QueryOptions & { * Defaults to `true`. */ useCompactAliasNames?: boolean; + + /** + * Whether to skip validation for computed fields. + */ + skipValidationForComputedFields?: boolean; } & (HasComputedFields extends true ? { /** diff --git a/packages/plugins/policy/package.json b/packages/plugins/policy/package.json index 39bf7b7a8..328c836d5 100644 --- a/packages/plugins/policy/package.json +++ b/packages/plugins/policy/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/plugin-policy", - "version": "3.4.1", + "version": "3.4.2", "description": "ZenStack Policy Plugin", "type": "module", "scripts": { diff --git a/packages/schema/package.json b/packages/schema/package.json index 245e123e0..af505525d 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/schema", - "version": "3.4.1", + "version": "3.4.2", "description": "ZenStack Runtime Schema", "type": "module", "scripts": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2fc9387d2..9aef48c08 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.4.1", + "version": "3.4.2", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index a22f92cc2..e4064ab63 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "3.4.1", + "version": "3.4.2", "description": "ZenStack automatic CRUD API handlers and server adapters", "type": "module", "scripts": { diff --git a/packages/testtools/package.json b/packages/testtools/package.json index cf6813306..470d51fe7 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.4.1", + "version": "3.4.2", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/zod/package.json b/packages/zod/package.json index 8d3e80685..b20d9d039 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.4.1", + "version": "3.4.2", "description": "ZenStack Zod integration", "type": "module", "scripts": { diff --git a/samples/orm/package.json b/samples/orm/package.json index f8720682d..aa93377b2 100644 --- a/samples/orm/package.json +++ b/samples/orm/package.json @@ -1,6 +1,6 @@ { "name": "sample-orm", - "version": "3.4.1", + "version": "3.4.2", "description": "", "main": "index.js", "private": true, diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 1a810bc68..6a53fd728 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.4.1", + "version": "3.4.2", "private": true, "type": "module", "scripts": { diff --git a/tests/regression/package.json b/tests/regression/package.json index a8a1ad7e3..9c0c2b7e5 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -1,6 +1,6 @@ { "name": "regression", - "version": "3.4.1", + "version": "3.4.2", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/bun/package.json b/tests/runtimes/bun/package.json index ea34c857e..c933f0ddb 100644 --- a/tests/runtimes/bun/package.json +++ b/tests/runtimes/bun/package.json @@ -1,6 +1,6 @@ { "name": "bun-e2e", - "version": "3.4.1", + "version": "3.4.2", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/edge-runtime/package.json b/tests/runtimes/edge-runtime/package.json index 598bffd98..a1ca4b4f1 100644 --- a/tests/runtimes/edge-runtime/package.json +++ b/tests/runtimes/edge-runtime/package.json @@ -1,6 +1,6 @@ { "name": "edge-runtime-e2e", - "version": "3.4.1", + "version": "3.4.2", "private": true, "type": "module", "scripts": {