From df9b35ea644eb41e47c276f11c78e5cb1ff07460 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:14:51 -0700 Subject: [PATCH 1/4] [CI] Bump version 3.5.6 (#2560) Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- package.json | 2 +- packages/auth-adapters/better-auth/package.json | 2 +- packages/cli/package.json | 2 +- packages/clients/client-helpers/package.json | 2 +- packages/clients/tanstack-query/package.json | 2 +- packages/common-helpers/package.json | 2 +- packages/config/eslint-config/package.json | 2 +- packages/config/typescript-config/package.json | 2 +- packages/config/vitest-config/package.json | 2 +- packages/create-zenstack/package.json | 2 +- packages/ide/vscode/package.json | 2 +- packages/language/package.json | 2 +- packages/orm/package.json | 2 +- packages/plugins/policy/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- packages/zod/package.json | 2 +- samples/orm/package.json | 2 +- tests/e2e/package.json | 2 +- tests/regression/package.json | 2 +- tests/runtimes/bun/package.json | 2 +- tests/runtimes/edge-runtime/package.json | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index c7a6befbd..68d3f32b9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "zenstack-v3", "displayName": "ZenStack", "description": "ZenStack", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/auth-adapters/better-auth/package.json b/packages/auth-adapters/better-auth/package.json index 6e7d68b1b..2eff6a514 100644 --- a/packages/auth-adapters/better-auth/package.json +++ b/packages/auth-adapters/better-auth/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/better-auth", "displayName": "ZenStack Better Auth Adapter", "description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/cli/package.json b/packages/cli/package.json index 0ba908820..af9636e16 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/cli", "displayName": "ZenStack CLI", "description": "FullStack database toolkit with built-in access control and automatic API generation.", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/clients/client-helpers/package.json b/packages/clients/client-helpers/package.json index a1e8632f0..f2abd1823 100644 --- a/packages/clients/client-helpers/package.json +++ b/packages/clients/client-helpers/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/client-helpers", "displayName": "ZenStack Client Helpers", "description": "Helpers for implementing clients that consume ZenStack's CRUD service", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/clients/tanstack-query/package.json b/packages/clients/tanstack-query/package.json index de1591ecd..e17777508 100644 --- a/packages/clients/tanstack-query/package.json +++ b/packages/clients/tanstack-query/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/tanstack-query", "displayName": "ZenStack TanStack Query Integration", "description": "TanStack Query Client for consuming ZenStack v3's CRUD service", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index 0777bc6c3..96089e43e 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/common-helpers", "displayName": "ZenStack Common Helpers", "description": "ZenStack Common Helpers", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/config/eslint-config/package.json b/packages/config/eslint-config/package.json index 4471e2748..7e04d806b 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.5.5", + "version": "3.5.6", "type": "module", "private": true, "license": "MIT" diff --git a/packages/config/typescript-config/package.json b/packages/config/typescript-config/package.json index 8d65b3c80..3c63a0a57 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.5.5", + "version": "3.5.6", "private": true, "license": "MIT" } diff --git a/packages/config/vitest-config/package.json b/packages/config/vitest-config/package.json index 996db9b8a..31042b071 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.5.5", + "version": "3.5.6", "private": true, "license": "MIT", "exports": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index ff421ca9a..c91ddf496 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -2,7 +2,7 @@ "name": "create-zenstack", "displayName": "Create ZenStack", "description": "Create a new ZenStack project", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 48f7dfcc8..c40d900c4 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack-v3", "publisher": "zenstack", - "version": "3.5.5", + "version": "3.5.6", "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 7624c1458..c8617699b 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/language", "displayName": "ZenStack Language Tooling", "description": "ZenStack ZModel language specification", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/orm/package.json b/packages/orm/package.json index b3fbde116..e04da18d9 100644 --- a/packages/orm/package.json +++ b/packages/orm/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/orm", "displayName": "ZenStack ORM", "description": "ZenStack ORM", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/plugins/policy/package.json b/packages/plugins/policy/package.json index 34a7f45a5..f4c683dad 100644 --- a/packages/plugins/policy/package.json +++ b/packages/plugins/policy/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/plugin-policy", "displayName": "ZenStack Access Policy Plugin", "description": "ZenStack plugin that enforces access control policies defined in the schema", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/schema/package.json b/packages/schema/package.json index bd485e61b..70309d84e 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/schema", "displayName": "ZenStack Schema Object Model", "description": "TypeScript representation of ZModel schema", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c013c33cb..5a82edf81 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/sdk", "displayName": "ZenStack SDK", "description": "Utilities for building ZenStack plugins", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/server/package.json b/packages/server/package.json index 92d944370..1f7db39a1 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/server", "displayName": "ZenStack Automatic CRUD Server", "description": "ZenStack automatic CRUD API handlers and server adapters for popular frameworks", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index f803ec9f0..04fb5f999 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/testtools", "displayName": "ZenStack Test Tools", "description": "ZenStack Test Tools", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/zod/package.json b/packages/zod/package.json index 91539bea0..51761d27a 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/zod", "displayName": "ZenStack Zod Integration", "description": "Automatically deriving Zod schemas from ZModel schemas", - "version": "3.5.5", + "version": "3.5.6", "type": "module", "author": { "name": "ZenStack Team", diff --git a/samples/orm/package.json b/samples/orm/package.json index b2239629f..60d08c498 100644 --- a/samples/orm/package.json +++ b/samples/orm/package.json @@ -1,6 +1,6 @@ { "name": "sample-orm", - "version": "3.5.5", + "version": "3.5.6", "description": "", "main": "index.js", "private": true, diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 6220d9822..4137092a4 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.5.5", + "version": "3.5.6", "private": true, "type": "module", "scripts": { diff --git a/tests/regression/package.json b/tests/regression/package.json index f934e2721..36a5c442a 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -1,6 +1,6 @@ { "name": "regression", - "version": "3.5.5", + "version": "3.5.6", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/bun/package.json b/tests/runtimes/bun/package.json index 1f52dba4d..090c62598 100644 --- a/tests/runtimes/bun/package.json +++ b/tests/runtimes/bun/package.json @@ -1,6 +1,6 @@ { "name": "bun-e2e", - "version": "3.5.5", + "version": "3.5.6", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/edge-runtime/package.json b/tests/runtimes/edge-runtime/package.json index 772599b91..f1713f912 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.5.5", + "version": "3.5.6", "private": true, "type": "module", "scripts": { From d22709fb55e654fcc3ce2e9fe322438c03ca2fbb Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Mon, 6 Apr 2026 16:47:45 -0700 Subject: [PATCH 2/4] fix(orm): prepend DISTINCT ON fields to ORDER BY for PostgreSQL compatibility (#2562) Co-authored-by: Claude Sonnet 4.6 Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Yiming Cao --- .../src/client/crud/dialects/base-dialect.ts | 33 +++++++-- tests/regression/test/issue-2529.test.ts | 69 +++++++++++++++++++ 2 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 tests/regression/test/issue-2529.test.ts diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 1c7627547..b525ac486 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -138,21 +138,42 @@ export abstract class BaseCrudDialect { } result = this.buildSkipTake(result, skip, take); - // orderBy - result = this.buildOrderBy(result, model, modelAlias, args.orderBy, negateOrderBy, take); - // distinct + let distinctFields: string[] = []; if ('distinct' in args && (args as any).distinct) { - const distinct = ensureArray((args as any).distinct) as string[]; + distinctFields = ensureArray((args as any).distinct) as string[]; if (this.supportsDistinctOn) { - result = result.distinctOn(distinct.map((f) => this.eb.ref(`${modelAlias}.${f}`))); + result = result.distinctOn(distinctFields.map((f) => this.eb.ref(`${modelAlias}.${f}`))); } else { throw createNotSupportedError(`"distinct" is not supported by "${this.schema.provider.type}" provider`); } } + // orderBy + // Some dialects (e.g., postgres) requires DISTINCT ON expressions to match the leftmost ORDER BY expressions. + // Prepend distinct fields only when the user-supplied orderBy doesn't already satisfy this. + let effectiveOrderBy = args.orderBy; + if (distinctFields.length > 0 && this.supportsDistinctOn) { + const existingOrderBy = enumerate(args.orderBy).filter((o) => Object.keys(o as object).length > 0); + const alreadySatisfied = distinctFields.every( + (f, i) => i < existingOrderBy.length && Object.keys(existingOrderBy[i] as object)[0] === f, + ); + if (existingOrderBy.length > 0 && !alreadySatisfied) { + const prependedOrderBy = distinctFields.map((f) => ({ [f]: 'asc' })) as any[]; + effectiveOrderBy = [...prependedOrderBy, ...existingOrderBy]; + } + } + result = this.buildOrderBy(result, model, modelAlias, effectiveOrderBy, negateOrderBy, take); + if (args.cursor) { - result = this.buildCursorFilter(model, result, args.cursor, args.orderBy, negateOrderBy, modelAlias); + result = this.buildCursorFilter( + model, + result, + args.cursor, + effectiveOrderBy as OrArray> | undefined, + negateOrderBy, + modelAlias, + ); } return result; } diff --git a/tests/regression/test/issue-2529.test.ts b/tests/regression/test/issue-2529.test.ts new file mode 100644 index 000000000..f1da006e7 --- /dev/null +++ b/tests/regression/test/issue-2529.test.ts @@ -0,0 +1,69 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +// https://github.com/zenstackhq/zenstack/issues/2529 +describe('Regression for issue #2529', () => { + async function setup() { + const db = await createTestClient( + ` +model Post { + id Int @id @default(autoincrement()) + title String + createdAt DateTime @default(now()) +} + `, + { provider: 'postgresql' }, + ); + await db.post.create({ data: { title: 'A' } }); + await db.post.create({ data: { title: 'A' } }); + await db.post.create({ data: { title: 'B' } }); + return db; + } + + it('distinct only without orderBy', async () => { + const db = await setup(); + + const result = await db.post.findMany({ distinct: ['title'] }); + + expect(result).toHaveLength(2); + const titles = result.map((p: any) => p.title).sort(); + expect(titles).toEqual(['A', 'B']); + }); + + it('orderBy only without distinct', async () => { + const db = await setup(); + + const result = await db.post.findMany({ orderBy: { title: 'desc' } }); + + expect(result).toHaveLength(3); + expect(result.map((p: any) => p.title)).toEqual(['B', 'A', 'A']); + }); + + it('prepends the distinct field to orderBy when user-supplied orderBy does not start with it', async () => { + const db = await setup(); + + const result = await db.post.findMany({ + distinct: ['title'], + orderBy: { createdAt: 'desc' }, + }); + + expect(result).toHaveLength(2); + const titles = result.map((p: any) => p.title).sort(); + expect(titles).toEqual(['A', 'B']); + }); + + it('does not double-prepend when user-supplied orderBy already starts with the distinct field', async () => { + const db = await setup(); + + // User already satisfies pg's requirement: ORDER BY "title" DESC, "createdAt" DESC + // The distinct field must NOT be prepended again, which would change sort semantics. + const result = await db.post.findMany({ + distinct: ['title'], + orderBy: [{ title: 'desc' }, { createdAt: 'desc' }], + }); + + expect(result).toHaveLength(2); + // With ORDER BY title DESC, we expect 'B' before 'A' + expect(result.map((p: any) => p.title)).toEqual(['B', 'A']); + }); +}); From f1375259d104cc32495d5242818ce2cbdc6b1876 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Mon, 6 Apr 2026 16:48:29 -0700 Subject: [PATCH 3/4] fix(tanstack-query): propagate TPageParam generic through useInfiniteFindMany (#2561) Co-authored-by: Claude Sonnet 4.6 --- packages/clients/tanstack-query/src/react.ts | 32 +++--- .../tanstack-query/src/svelte/index.svelte.ts | 97 +++++++++++++------ packages/clients/tanstack-query/src/vue.ts | 21 ++-- .../test/react-typing.test-d.ts | 10 ++ .../tanstack-query/test/svelte-typing-test.ts | 9 ++ .../tanstack-query/test/vue-typing-test.ts | 9 ++ 6 files changed, 123 insertions(+), 55 deletions(-) diff --git a/packages/clients/tanstack-query/src/react.ts b/packages/clients/tanstack-query/src/react.ts index e84aa7e09..a62421de1 100644 --- a/packages/clients/tanstack-query/src/react.ts +++ b/packages/clients/tanstack-query/src/react.ts @@ -119,15 +119,15 @@ export type ModelSuspenseQueryResult = UseSuspenseQueryResult = Omit< - UseInfiniteQueryOptions>, +export type ModelInfiniteQueryOptions = Omit< + UseInfiniteQueryOptions, QueryKey, TPageParam>, 'queryKey' | 'initialPageParam' >; export type ModelInfiniteQueryResult = UseInfiniteQueryResult & { queryKey: QueryKey }; -export type ModelSuspenseInfiniteQueryOptions = Omit< - UseSuspenseInfiniteQueryOptions>, +export type ModelSuspenseInfiniteQueryOptions = Omit< + UseSuspenseInfiniteQueryOptions, QueryKey, TPageParam>, 'queryKey' | 'initialPageParam' >; @@ -263,15 +263,15 @@ export type ModelQueryHooks< options?: ModelSuspenseQueryOptions[]>, ): ModelSuspenseQueryResult[]>; - useInfiniteFindMany>( + useInfiniteFindMany, TPageParam = unknown>( args?: SelectSubset>, - options?: ModelInfiniteQueryOptions[]>, - ): ModelInfiniteQueryResult[]>>; + options?: ModelInfiniteQueryOptions[], TPageParam>, + ): ModelInfiniteQueryResult[], TPageParam>>; - useSuspenseInfiniteFindMany>( + useSuspenseInfiniteFindMany, TPageParam = unknown>( args?: SelectSubset>, - options?: ModelSuspenseInfiniteQueryOptions[]>, - ): ModelSuspenseInfiniteQueryResult[]>>; + options?: ModelSuspenseInfiniteQueryOptions[], TPageParam>, + ): ModelSuspenseInfiniteQueryResult[], TPageParam>>; useCreate>( options?: ModelMutationOptions, T>, @@ -592,14 +592,14 @@ export function useInternalSuspenseQuery( }; } -export function useInternalInfiniteQuery( +export function useInternalInfiniteQuery( _schema: SchemaDef, model: string, operation: string, args: unknown, options: | (Omit< - UseInfiniteQueryOptions>, + UseInfiniteQueryOptions, QueryKey, TPageParam>, 'queryKey' | 'initialPageParam' > & QueryContext) @@ -615,19 +615,19 @@ export function useInternalInfiniteQuery( queryFn: ({ pageParam, signal }) => { return fetcher(makeUrl(endpoint, model, operation, pageParam ?? args), { signal }, fetch); }, - initialPageParam: args, + initialPageParam: args as TPageParam, ...options, }), }; } -export function useInternalSuspenseInfiniteQuery( +export function useInternalSuspenseInfiniteQuery( _schema: SchemaDef, model: string, operation: string, args: unknown, options: Omit< - UseSuspenseInfiniteQueryOptions> & QueryContext, + UseSuspenseInfiniteQueryOptions, QueryKey, TPageParam> & QueryContext, 'queryKey' | 'initialPageParam' >, ) { @@ -640,7 +640,7 @@ export function useInternalSuspenseInfiniteQuery( queryFn: ({ pageParam, signal }) => { return fetcher(makeUrl(endpoint, model, operation, pageParam ?? args), { signal }, fetch); }, - initialPageParam: args, + initialPageParam: args as TPageParam, ...options, }), }; diff --git a/packages/clients/tanstack-query/src/svelte/index.svelte.ts b/packages/clients/tanstack-query/src/svelte/index.svelte.ts index 9ddfe40e2..639cf2969 100644 --- a/packages/clients/tanstack-query/src/svelte/index.svelte.ts +++ b/packages/clients/tanstack-query/src/svelte/index.svelte.ts @@ -125,8 +125,8 @@ export type ModelQueryOptions = Omit, 'qu export type ModelQueryResult = CreateQueryResult, DefaultError> & { queryKey: QueryKey }; -export type ModelInfiniteQueryOptions = Omit< - CreateInfiniteQueryOptions>, +export type ModelInfiniteQueryOptions = Omit< + CreateInfiniteQueryOptions, QueryKey, TPageParam>, 'queryKey' | 'initialPageParam' > & QueryContext; @@ -147,7 +147,10 @@ export type ModelMutationModelResult< Array extends boolean = false, Options extends QueryOptions = QueryOptions, ExtResult extends ExtResultBase = {}, -> = Omit, TArgs>, 'mutateAsync'> & { +> = Omit< + ModelMutationResult, TArgs>, + 'mutateAsync' +> & { mutateAsync( args: T, options?: ModelMutationOptions, T>, @@ -159,7 +162,12 @@ export type ClientHooks< Options extends QueryOptions = QueryOptions, ExtResult extends ExtResultBase = {}, > = { - [Model in GetSlicedModels as `${Uncapitalize}`]: ModelQueryHooks; + [Model in GetSlicedModels as `${Uncapitalize}`]: ModelQueryHooks< + Schema, + Model, + Options, + ExtResult + >; } & ProcedureHooks; type ProcedureHookGroup> = { @@ -238,10 +246,14 @@ export type ModelQueryHooks< options?: Accessor[]>>, ): ModelQueryResult[]>; - useInfiniteFindMany>( + useInfiniteFindMany, TPageParam = unknown>( args?: Accessor>>, - options?: Accessor[]>>, - ): ModelInfiniteQueryResult[]>>; + options?: Accessor< + ModelInfiniteQueryOptions[], TPageParam> + >, + ): ModelInfiniteQueryResult< + InfiniteData[], TPageParam> + >; useCreate>( options?: Accessor, T>>, @@ -310,23 +322,20 @@ export type ModelQueryHooks< * const client = useClientQueries(schema) * ``` */ -export function useClientQueries< - SchemaOrClient extends SchemaDef | ClientContract, ->( +export function useClientQueries>( schema: InferSchema, options?: Accessor, -): ClientHooks, InferOptions>, InferExtResult extends ExtResultBase> ? InferExtResult : {}> { - const result = Object.keys(schema.models).reduce( - (acc, model) => { - (acc as any)[lowerCaseFirst(model)] = useModelQueries( - schema as any, - model as any, - options, - ); - return acc; - }, - {} as any, - ); +): ClientHooks< + InferSchema, + InferOptions>, + InferExtResult extends ExtResultBase> + ? InferExtResult + : {} +> { + const result = Object.keys(schema.models).reduce((acc, model) => { + (acc as any)[lowerCaseFirst(model)] = useModelQueries(schema as any, model as any, options); + return acc; + }, {} as any); const procedures = (schema as any).procedures as Record | undefined; if (procedures) { @@ -376,7 +385,11 @@ export function useModelQueries< Model extends GetModels, Options extends QueryOptions, ExtResult extends ExtResultBase = {}, ->(schema: Schema, model: Model, rootOptions?: Accessor): ModelQueryHooks { +>( + schema: Schema, + model: Model, + rootOptions?: Accessor, +): ModelQueryHooks { const modelDef = Object.values(schema.models).find((m) => m.name.toLowerCase() === model.toLowerCase()); if (!modelDef) { throw new Error(`Model "${model}" not found in schema`); @@ -487,14 +500,20 @@ export function useInternalQuery( return createQueryResult(query, queryKey); } -export function useInternalInfiniteQuery( +export function useInternalInfiniteQuery( _schema: SchemaDef, model: string, operation: string, - args: Accessor, + args?: Accessor, options?: Accessor< Omit< - CreateInfiniteQueryOptions>, + CreateInfiniteQueryOptions< + TQueryFnData, + DefaultError, + InfiniteData, + QueryKey, + TPageParam + >, 'queryKey' | 'initialPageParam' > & QueryContext @@ -502,21 +521,37 @@ export function useInternalInfiniteQuery( ) { const { endpoint, fetch } = useFetchOptions(options); - const queryKey = $derived(getQueryKey(model, operation, args(), { infinite: true, optimisticUpdate: false })); + const queryKey = $derived(getQueryKey(model, operation, args?.(), { infinite: true, optimisticUpdate: false })); const finalOptions = () => { - const queryFn: QueryFunction = ({ pageParam, signal }) => - fetcher(makeUrl(endpoint, model, operation, pageParam ?? args()), { signal }, fetch); + const queryFn: QueryFunction = ({ pageParam, signal }) => + fetcher(makeUrl(endpoint, model, operation, pageParam ?? args?.()), { signal }, fetch); const optionsValue = options?.() ?? { getNextPageParam: () => undefined }; return { queryKey, queryFn, - initialPageParam: args(), + initialPageParam: args?.() as TPageParam, ...optionsValue, }; }; - const query = createInfiniteQuery>(finalOptions); + const query = createInfiniteQuery< + TQueryFnData, + DefaultError, + InfiniteData, + QueryKey, + TPageParam + >( + finalOptions as unknown as Accessor< + CreateInfiniteQueryOptions< + TQueryFnData, + DefaultError, + InfiniteData, + QueryKey, + TPageParam + > + >, + ); // svelte-ignore state_referenced_locally return createQueryResult(query, queryKey); } diff --git a/packages/clients/tanstack-query/src/vue.ts b/packages/clients/tanstack-query/src/vue.ts index 4b0e8a682..32ab6e656 100644 --- a/packages/clients/tanstack-query/src/vue.ts +++ b/packages/clients/tanstack-query/src/vue.ts @@ -118,8 +118,11 @@ export type ModelQueryOptions = MaybeRefOrGetter< export type ModelQueryResult = UseQueryReturnType, DefaultError> & { queryKey: Ref }; -export type ModelInfiniteQueryOptions = MaybeRefOrGetter< - Omit>>, 'queryKey' | 'initialPageParam'> & +export type ModelInfiniteQueryOptions = MaybeRefOrGetter< + Omit< + UnwrapRef, QueryKey, TPageParam>>, + 'queryKey' | 'initialPageParam' + > & QueryContext >; @@ -241,10 +244,10 @@ export type ModelQueryHooks< options?: MaybeRefOrGetter[]>>, ): ModelQueryResult[]>; - useInfiniteFindMany>( + useInfiniteFindMany, TPageParam = unknown>( args?: MaybeRefOrGetter>>, - options?: MaybeRefOrGetter[]>>, - ): ModelInfiniteQueryResult[]>>; + options?: MaybeRefOrGetter[], TPageParam>>, + ): ModelInfiniteQueryResult[], TPageParam>>; useCreate>( options?: MaybeRefOrGetter, T>>, @@ -510,14 +513,16 @@ export function useInternalQuery( return { queryKey, ...useQuery(finalOptions) }; } -export function useInternalInfiniteQuery( +export function useInternalInfiniteQuery( _schema: SchemaDef, model: string, operation: string, args: MaybeRefOrGetter, options: MaybeRefOrGetter< | (Omit< - UnwrapRef>>, + UnwrapRef< + UseInfiniteQueryOptions, QueryKey, TPageParam> + >, 'queryKey' | 'initialPageParam' > & QueryContext) @@ -543,7 +548,7 @@ export function useInternalInfiniteQuery( const reqUrl = makeUrl(endpoint, model, operation, argsValue); return fetcher(reqUrl, { signal }, fetch); }, - initialPageParam: toValue(argsValue), + initialPageParam: toValue(argsValue) as TPageParam, ...toValue(options), }; }); diff --git a/packages/clients/tanstack-query/test/react-typing.test-d.ts b/packages/clients/tanstack-query/test/react-typing.test-d.ts index 3dbcd8446..876336c5e 100644 --- a/packages/clients/tanstack-query/test/react-typing.test-d.ts +++ b/packages/clients/tanstack-query/test/react-typing.test-d.ts @@ -37,6 +37,16 @@ describe('React client typing test', () => { }, ).data?.pages[1]?.[0]?.email, ); + + // TPageParam should be inferred from getNextPageParam, not typed as unknown + const infiniteResult = client.user.useInfiniteFindMany( + {}, + { + getNextPageParam: (_lastPage, _allPages, lastPageParam: { cursor: string }) => lastPageParam, + }, + ); + check(infiniteResult.data?.pageParams[0]?.cursor); + // @ts-expect-error check(client.user.useInfiniteFindMany().data?.pages[0]?.[0]?.$optimistic); diff --git a/packages/clients/tanstack-query/test/svelte-typing-test.ts b/packages/clients/tanstack-query/test/svelte-typing-test.ts index eda2add95..492b086a1 100644 --- a/packages/clients/tanstack-query/test/svelte-typing-test.ts +++ b/packages/clients/tanstack-query/test/svelte-typing-test.ts @@ -43,6 +43,15 @@ check( // @ts-expect-error check(client.user.useInfiniteFindMany().data?.pages[0]?.[0]?.$optimistic); +// TPageParam should be inferred from getNextPageParam, not typed as unknown +const infiniteResult = client.user.useInfiniteFindMany( + () => ({}), + () => ({ + getNextPageParam: (_lastPage: unknown, _allPages: unknown, lastPageParam: { cursor: string }) => lastPageParam, + }), +); +check(infiniteResult.data?.pageParams[0]?.cursor); + check(client.user.useCount().data?.toFixed(2)); check(client.user.useCount(() => ({ select: { email: true } })).data?.email.toFixed(2)); diff --git a/packages/clients/tanstack-query/test/vue-typing-test.ts b/packages/clients/tanstack-query/test/vue-typing-test.ts index 60099fb2f..fdd4f8541 100644 --- a/packages/clients/tanstack-query/test/vue-typing-test.ts +++ b/packages/clients/tanstack-query/test/vue-typing-test.ts @@ -38,6 +38,15 @@ check( // @ts-expect-error check(client.user.useInfiniteFindMany().data.value?.pages[0]?.[0]?.$optimistic); +// TPageParam should be inferred from getNextPageParam, not typed as unknown +const infiniteResult = client.user.useInfiniteFindMany( + {}, + { + getNextPageParam: (_lastPage: unknown, _allPages: unknown, lastPageParam: { cursor: string }) => lastPageParam, + }, +); +check(infiniteResult.data.value?.pageParams[0]?.cursor); + check(client.user.useCount().data.value?.toFixed(2)); check(client.user.useCount({ select: { email: true } }).data.value?.email.toFixed(2)); From 39a0a28c5100f72dd53a9542f1f1ebf926d2a19e Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Mon, 6 Apr 2026 19:16:27 -0700 Subject: [PATCH 4/4] fix(orm): enforce at most one key per orderBy array element (#2563) --- packages/orm/src/client/zod/factory.ts | 11 +++++++---- tests/e2e/orm/client-api/find.test.ts | 22 +++++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/orm/src/client/zod/factory.ts b/packages/orm/src/client/zod/factory.ts index f6149cab7..0f8b22701 100644 --- a/packages/orm/src/client/zod/factory.ts +++ b/packages/orm/src/client/zod/factory.ts @@ -1126,6 +1126,10 @@ export class ZodSchemaFactory< ) { const fields: Record = {}; const sort = z.union([z.literal('asc'), z.literal('desc')]); + const refineAtMostOneKey = (s: ZodObject) => + s.refine((v: object) => Object.keys(v).length <= 1, { + message: 'Each orderBy element must have at most one key', + }); const nextOpts = this.nextOptions(options); for (const [field, fieldDef] of this.getModelFields(model)) { if (fieldDef.relation) { @@ -1139,9 +1143,8 @@ export class ZodSchemaFactory< nextOpts, ); if (fieldDef.array) { - relationOrderBy = relationOrderBy.extend({ - _count: sort, - }); + // safeExtend drops existing refinements, so re-apply after extending + relationOrderBy = refineAtMostOneKey(relationOrderBy.safeExtend({ _count: sort })); } return relationOrderBy.optional(); }); @@ -1172,7 +1175,7 @@ export class ZodSchemaFactory< } } - return z.strictObject(fields); + return refineAtMostOneKey(z.strictObject(fields)); } @cache() diff --git a/tests/e2e/orm/client-api/find.test.ts b/tests/e2e/orm/client-api/find.test.ts index 1ea151ab1..df7ca6665 100644 --- a/tests/e2e/orm/client-api/find.test.ts +++ b/tests/e2e/orm/client-api/find.test.ts @@ -126,13 +126,6 @@ describe('Client find tests ', () => { email: 'u2@test.com', }); - // multiple sorting conditions in one object - await expect( - client.user.findFirst({ - orderBy: { role: 'asc', email: 'desc' }, - }), - ).resolves.toMatchObject({ email: 'u2@test.com' }); - // multiple sorting conditions in array await expect( client.user.findFirst({ @@ -1189,6 +1182,21 @@ describe('Client find tests ', () => { expect(result3?._count.posts).toBe(1); }); + it('rejects orderBy array elements with multiple keys', async () => { + await createUser(client, 'u1@test.com'); + + // zero keys is valid + await expect(client.user.findMany({ orderBy: [{}] })).resolves.toBeDefined(); + + // single key is valid + await expect(client.user.findMany({ orderBy: [{ email: 'asc' }] })).resolves.toBeDefined(); + + // multiple keys in one element is rejected + await expect( + client.user.findMany({ orderBy: [{ email: 'asc', role: 'desc' }] } as any), + ).toBeRejectedByValidation(); + }); + it('supports $expr', async () => { await createUser(client, 'yiming@gmail.com'); await createUser(client, 'yiming@zenstack.dev');