From b40e598e1c311a385ff119062e5886636cea7f02 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 30 Dec 2025 07:21:18 +0000 Subject: [PATCH 1/9] feat: exists operation --- packages/orm/src/client/client-impl.ts | 11 +++++++ packages/orm/src/client/contract.ts | 5 +++ packages/orm/src/client/crud-types.ts | 2 ++ .../orm/src/client/crud/operations/base.ts | 32 +++++++++++++++++-- .../orm/src/client/crud/operations/exists.ts | 12 +++++++ .../orm/src/client/crud/validator/index.ts | 20 ++++++++++++ packages/sdk/src/ts-schema-generator.ts | 1 + 7 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 packages/orm/src/client/crud/operations/exists.ts diff --git a/packages/orm/src/client/client-impl.ts b/packages/orm/src/client/client-impl.ts index 8e2c8382b..ef81cc71c 100644 --- a/packages/orm/src/client/client-impl.ts +++ b/packages/orm/src/client/client-impl.ts @@ -27,6 +27,7 @@ import { CountOperationHandler } from './crud/operations/count'; import { CreateOperationHandler } from './crud/operations/create'; import { DeleteOperationHandler } from './crud/operations/delete'; import { FindOperationHandler } from './crud/operations/find'; +import { ExistsOperationHandler } from './crud/operations/exists'; import { GroupByOperationHandler } from './crud/operations/group-by'; import { UpdateOperationHandler } from './crud/operations/update'; import { InputValidator } from './crud/validator'; @@ -598,5 +599,15 @@ function createModelCrudHandler( true, ); }, + + exists: (args: unknown) => { + return createPromise( + 'exists', + 'exists', + args, + new ExistsOperationHandler(client, model, inputValidator), + true, + ); + }, } as ModelOperations; } diff --git a/packages/orm/src/client/contract.ts b/packages/orm/src/client/contract.ts index 050381fd2..cf4c02f94 100644 --- a/packages/orm/src/client/contract.ts +++ b/packages/orm/src/client/contract.ts @@ -24,6 +24,7 @@ import type { DefaultModelResult, DeleteArgs, DeleteManyArgs, + ExistsArgs, FindFirstArgs, FindManyArgs, FindUniqueArgs, @@ -828,6 +829,10 @@ export type AllModelOperations< groupBy>( args: Subset>, ): ZenStackPromise>>; + + exists>( + args?: Subset>, + ): ZenStackPromise; }; export type OperationsIneligibleForDelegateModels = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert'; diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index 1fefdc7dd..471c43619 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -1079,6 +1079,8 @@ export type FindManyArgs> = FindArgs; +export type ExistsArgs> = FilterArgs; + export type FindUniqueArgs> = { where: WhereUniqueInput; } & SelectIncludeOmit; diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 8fb69846f..ad9c56534 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -68,9 +68,10 @@ export type CoreCrudOperation = | 'deleteMany' | 'count' | 'aggregate' - | 'groupBy'; + | 'groupBy' + | 'exists'; -export type AllCrudOperation = CoreCrudOperation | 'findUniqueOrThrow' | 'findFirstOrThrow'; +export type AllCrudOperation = CoreCrudOperation | 'findUniqueOrThrow' | 'findFirstOrThrow' // context for nested relation operations export type FromRelationContext = { @@ -145,6 +146,33 @@ export abstract class BaseOperationHandler { }); } + protected async existsNonUnique( + kysely: ToKysely, + model: GetModels, + filter: any, + ): Promise { + const query = kysely.selectNoFrom((eb) => ( + eb.exists( + this.dialect + .buildSelectModel(model, model) + // @ts-expect-error + .select(sql.lit(1)) + .where(() => this.dialect.buildFilter(model, model, filter)) + ).as('exists') + )).modifyEnd(this.makeContextComment({ model, operation: 'read' })); + + let result: {exists: number}[] = []; + const compiled = kysely.getExecutor().compileQuery(query.toOperationNode(), createQueryId()); + try { + const r = await kysely.getExecutor().executeQuery(compiled); + result = r.rows as {exists: number}[]; + } catch (err) { + throw createDBQueryError('Failed to execute query', err, compiled.sql, compiled.parameters); + } + + return result[0]?.exists === 1; + } + protected async read( kysely: AnyKysely, model: string, diff --git a/packages/orm/src/client/crud/operations/exists.ts b/packages/orm/src/client/crud/operations/exists.ts new file mode 100644 index 000000000..d4c6896fc --- /dev/null +++ b/packages/orm/src/client/crud/operations/exists.ts @@ -0,0 +1,12 @@ +import type { SchemaDef } from '../../../schema'; +import { BaseOperationHandler } from './base'; + +export class ExistsOperationHandler extends BaseOperationHandler { + async handle(_operation: 'exists', args: unknown): Promise { + // normalize args to strip `undefined` fields + const normalizedArgs = this.normalizeArgs(args); + const parsedArgs = this.inputValidator.validateExistsArgs(this.model, normalizedArgs); + + return await this.existsNonUnique(this.client.$qb, this.model, parsedArgs?.where); + } +} diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index dc1a6f836..29f6bd028 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -25,6 +25,7 @@ import { type CreateManyArgs, type DeleteArgs, type DeleteManyArgs, + type ExistsArgs, type FindArgs, type GroupByArgs, type UpdateArgs, @@ -81,6 +82,19 @@ export class InputValidator { >(model, 'find', options, (model, options) => this.makeFindSchema(model, options), args); } + validateExistsArgs( + model: GetModels, + args: unknown, + ): ExistsArgs> | undefined { + return this.validate>>( + model, + 'exists', + undefined, + (model) => this.makeExistsSchema(model), + args, + ); + } + validateCreateArgs(model: GetModels, args: unknown): CreateArgs> { return this.validate>>( model, @@ -297,6 +311,12 @@ export class InputValidator { return result; } + private makeExistsSchema(model: string) { + return z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + }).optional(); + } + private makeScalarSchema(type: string, attributes?: readonly AttributeApplication[]) { if (this.schema.typeDefs && type in this.schema.typeDefs) { return this.makeTypeDefSchema(type); diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index f68bb0bc6..c24436952 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -1539,6 +1539,7 @@ export class TsSchemaGenerator { 'FindManyArgs', 'FindUniqueArgs', 'FindFirstArgs', + 'ExistsArgs', 'CreateArgs', 'CreateManyArgs', 'CreateManyAndReturnArgs', From ae0ce4e5b0b3b4459d581cfb9f1da44a808a661a Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 30 Dec 2025 07:21:52 +0000 Subject: [PATCH 2/9] Add RPC handling. --- packages/server/src/api/rpc/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/api/rpc/index.ts b/packages/server/src/api/rpc/index.ts index e4e6ea64c..95f97e778 100644 --- a/packages/server/src/api/rpc/index.ts +++ b/packages/server/src/api/rpc/index.ts @@ -73,6 +73,7 @@ export class RPCApiHandler implements ApiH case 'aggregate': case 'groupBy': case 'count': + case 'exists': if (method !== 'GET') { return this.makeBadInputErrorResponse('invalid request method, only GET is supported'); } From 05434dcd15cbfdebcd30ffaeb554405e3d5c844c Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 30 Dec 2025 07:23:10 +0000 Subject: [PATCH 3/9] Add frontend handling. --- packages/clients/tanstack-query/src/react.ts | 10 ++++++++++ .../clients/tanstack-query/src/svelte/index.svelte.ts | 10 ++++++++++ packages/clients/tanstack-query/src/vue.ts | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/packages/clients/tanstack-query/src/react.ts b/packages/clients/tanstack-query/src/react.ts index 4449e29fb..66628491c 100644 --- a/packages/clients/tanstack-query/src/react.ts +++ b/packages/clients/tanstack-query/src/react.ts @@ -33,6 +33,7 @@ import type { CreateManyArgs, DeleteArgs, DeleteManyArgs, + ExistsArgs, FindFirstArgs, FindManyArgs, FindUniqueArgs, @@ -165,6 +166,11 @@ export type ModelQueryHooks< options?: ModelSuspenseQueryOptions | null>, ): ModelSuspenseQueryResult | null>; + useExists>( + args?: Subset>, + options?: ModelQueryOptions, + ): ModelQueryResult; + useFindMany>( args?: SelectSubset>, options?: ModelQueryOptions[]>, @@ -308,6 +314,10 @@ export function useModelQueries< return useInternalSuspenseQuery(schema, modelName, 'findFirst', args, { ...rootOptions, ...options }); }, + useExists: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'exists', args, { ...rootOptions, ...options }); + }, + useFindMany: (args: any, options?: any) => { return useInternalQuery(schema, modelName, 'findMany', args, { ...rootOptions, ...options }); }, diff --git a/packages/clients/tanstack-query/src/svelte/index.svelte.ts b/packages/clients/tanstack-query/src/svelte/index.svelte.ts index a94941c40..035dfe83b 100644 --- a/packages/clients/tanstack-query/src/svelte/index.svelte.ts +++ b/packages/clients/tanstack-query/src/svelte/index.svelte.ts @@ -34,6 +34,7 @@ import type { CreateManyArgs, DeleteArgs, DeleteManyArgs, + ExistsArgs, FindFirstArgs, FindManyArgs, FindUniqueArgs, @@ -144,6 +145,11 @@ export type ModelQueryHooks< options?: Accessor | null>>, ): ModelQueryResult | null>; + useExists>( + args?: Accessor>>, + options?: Accessor>, + ): ModelQueryResult; + useFindMany>( args?: Accessor>>, options?: Accessor[]>>, @@ -257,6 +263,10 @@ export function useModelQueries< return useInternalQuery(schema, modelName, 'findFirst', args, merge(rootOptions, options)); }, + useExists: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'exists', args, merge(rootOptions, options)); + }, + useFindMany: (args: any, options?: any) => { return useInternalQuery(schema, modelName, 'findMany', args, merge(rootOptions, options)); }, diff --git a/packages/clients/tanstack-query/src/vue.ts b/packages/clients/tanstack-query/src/vue.ts index bd4dcf74b..b0c54ac80 100644 --- a/packages/clients/tanstack-query/src/vue.ts +++ b/packages/clients/tanstack-query/src/vue.ts @@ -32,6 +32,7 @@ import type { CreateManyArgs, DeleteArgs, DeleteManyArgs, + ExistsArgs, FindFirstArgs, FindManyArgs, FindUniqueArgs, @@ -145,6 +146,11 @@ export type ModelQueryHooks< options?: MaybeRefOrGetter | null>>, ): ModelQueryResult | null>; + useExists>( + args?: MaybeRefOrGetter>>, + options?: MaybeRefOrGetter>, + ): ModelQueryResult; + useFindMany>( args?: MaybeRefOrGetter>>, options?: MaybeRefOrGetter[]>>, @@ -258,6 +264,10 @@ export function useModelQueries< return useInternalQuery(schema, modelName, 'findFirst', args, merge(rootOptions, options)); }, + useExists: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'exists', args, merge(rootOptions, options)); + }, + useFindMany: (args: any, options?: any) => { return useInternalQuery(schema, modelName, 'findMany', args, merge(rootOptions, options)); }, From d4d6d9dcc3f3931620dc22b394876836d6fff1ab Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 30 Dec 2025 07:23:18 +0000 Subject: [PATCH 4/9] Add tests. --- .../tanstack-query/test/react-typing-test.ts | 3 + .../test/schemas/basic/input.ts | 7 +- .../tanstack-query/test/svelte-typing-test.ts | 3 + .../tanstack-query/test/vue-typing-test.ts | 3 + packages/server/test/api/rpc.test.ts | 18 ++ tests/e2e/orm/client-api/exists.test.ts | 180 ++++++++++++++++++ 6 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/orm/client-api/exists.test.ts diff --git a/packages/clients/tanstack-query/test/react-typing-test.ts b/packages/clients/tanstack-query/test/react-typing-test.ts index 8f57ec670..b9f6337cc 100644 --- a/packages/clients/tanstack-query/test/react-typing-test.ts +++ b/packages/clients/tanstack-query/test/react-typing-test.ts @@ -18,6 +18,9 @@ check(client.user.useFindUnique({ where: { id: '1' }, include: { posts: true } } check(client.user.useFindFirst().data?.email); check(client.user.useFindFirst().data?.$optimistic); +check(client.user.useExists().data); +check(client.user.useExists({ where: { id: '1' } }).data); + check(client.user.useFindMany().data?.[0]?.email); check(client.user.useFindMany().data?.[0]?.$optimistic); diff --git a/packages/clients/tanstack-query/test/schemas/basic/input.ts b/packages/clients/tanstack-query/test/schemas/basic/input.ts index 766d0ca90..47638e49d 100644 --- a/packages/clients/tanstack-query/test/schemas/basic/input.ts +++ b/packages/clients/tanstack-query/test/schemas/basic/input.ts @@ -6,11 +6,12 @@ /* eslint-disable */ 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, QueryOptions as $QueryOptions } from "@zenstackhq/orm"; +import type { FindManyArgs as $FindManyArgs, FindUniqueArgs as $FindUniqueArgs, FindFirstArgs as $FindFirstArgs, ExistsArgs as $ExistsArgs, 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, QueryOptions as $QueryOptions } from "@zenstackhq/orm"; import type { SimplifiedPlainResult as $Result, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/orm"; export type UserFindManyArgs = $FindManyArgs<$Schema, "User">; export type UserFindUniqueArgs = $FindUniqueArgs<$Schema, "User">; export type UserFindFirstArgs = $FindFirstArgs<$Schema, "User">; +export type UserExistsArgs = $ExistsArgs<$Schema, "User">; export type UserCreateArgs = $CreateArgs<$Schema, "User">; export type UserCreateManyArgs = $CreateManyArgs<$Schema, "User">; export type UserCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "User">; @@ -31,6 +32,7 @@ export type UserGetPayload; export type PostFindUniqueArgs = $FindUniqueArgs<$Schema, "Post">; export type PostFindFirstArgs = $FindFirstArgs<$Schema, "Post">; +export type PostExistsArgs = $ExistsArgs<$Schema, "Post">; export type PostCreateArgs = $CreateArgs<$Schema, "Post">; export type PostCreateManyArgs = $CreateManyArgs<$Schema, "Post">; export type PostCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Post">; @@ -51,6 +53,7 @@ export type PostGetPayload; export type CategoryFindUniqueArgs = $FindUniqueArgs<$Schema, "Category">; export type CategoryFindFirstArgs = $FindFirstArgs<$Schema, "Category">; +export type CategoryExistsArgs = $ExistsArgs<$Schema, "Category">; export type CategoryCreateArgs = $CreateArgs<$Schema, "Category">; export type CategoryCreateManyArgs = $CreateManyArgs<$Schema, "Category">; export type CategoryCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Category">; @@ -71,6 +74,7 @@ export type CategoryGetPayload; export type FooFindUniqueArgs = $FindUniqueArgs<$Schema, "Foo">; export type FooFindFirstArgs = $FindFirstArgs<$Schema, "Foo">; +export type FooExistsArgs = $ExistsArgs<$Schema, "Foo">; export type FooCreateArgs = $CreateArgs<$Schema, "Foo">; export type FooCreateManyArgs = $CreateManyArgs<$Schema, "Foo">; export type FooCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Foo">; @@ -91,6 +95,7 @@ export type FooGetPayload, export type BarFindManyArgs = $FindManyArgs<$Schema, "Bar">; export type BarFindUniqueArgs = $FindUniqueArgs<$Schema, "Bar">; export type BarFindFirstArgs = $FindFirstArgs<$Schema, "Bar">; +export type BarExistsArgs = $ExistsArgs<$Schema, "Bar">; export type BarCreateArgs = $CreateArgs<$Schema, "Bar">; export type BarCreateManyArgs = $CreateManyArgs<$Schema, "Bar">; export type BarCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Bar">; diff --git a/packages/clients/tanstack-query/test/svelte-typing-test.ts b/packages/clients/tanstack-query/test/svelte-typing-test.ts index 9c8788ebf..b599431d8 100644 --- a/packages/clients/tanstack-query/test/svelte-typing-test.ts +++ b/packages/clients/tanstack-query/test/svelte-typing-test.ts @@ -18,6 +18,9 @@ check(client.user.useFindUnique(() => ({ where: { id: '1' }, include: { posts: t check(client.user.useFindFirst().data?.email); check(client.user.useFindFirst().data?.$optimistic); +check(client.user.useExists().data); +check(client.user.useExists(() => ({ where: { id: '1' } })).data); + check(client.user.useFindMany().data?.[0]?.email); check(client.user.useFindMany().data?.[0]?.$optimistic); diff --git a/packages/clients/tanstack-query/test/vue-typing-test.ts b/packages/clients/tanstack-query/test/vue-typing-test.ts index f134378cb..29478f875 100644 --- a/packages/clients/tanstack-query/test/vue-typing-test.ts +++ b/packages/clients/tanstack-query/test/vue-typing-test.ts @@ -18,6 +18,9 @@ check(client.user.useFindUnique({ where: { id: '1' }, include: { posts: true } } check(client.user.useFindFirst().data.value?.email); check(client.user.useFindFirst().data.value?.$optimistic); +check(client.user.useExists().data.value); +check(client.user.useExists({ where: { id: '1' } }).data.value); + check(client.user.useFindMany().data.value?.[0]?.email); check(client.user.useFindMany().data.value?.[0]?.$optimistic); diff --git a/packages/server/test/api/rpc.test.ts b/packages/server/test/api/rpc.test.ts index 19e44ca08..bf4a9edb5 100644 --- a/packages/server/test/api/rpc.test.ts +++ b/packages/server/test/api/rpc.test.ts @@ -27,6 +27,15 @@ describe('RPC API Handler Tests', () => { expect(r.status).toBe(200); expect(r.data).toHaveLength(0); + r = await handleRequest({ + method: 'get', + path: '/user/exists', + query: { q: JSON.stringify({ where: { id: 'user1' }})}, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toBe(false); + r = await handleRequest({ method: 'post', path: '/user/create', @@ -57,6 +66,15 @@ describe('RPC API Handler Tests', () => { }), ); + r = await handleRequest({ + method: 'get', + path: '/user/exists', + query: { q: JSON.stringify({ where: { id: 'user1' }})}, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toBe(true); + r = await handleRequest({ method: 'get', path: '/post/findMany', diff --git a/tests/e2e/orm/client-api/exists.test.ts b/tests/e2e/orm/client-api/exists.test.ts new file mode 100644 index 000000000..42e4e8c19 --- /dev/null +++ b/tests/e2e/orm/client-api/exists.test.ts @@ -0,0 +1,180 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import type { ClientContract } from '@zenstackhq/orm'; +import { schema } from '../schemas/basic'; +import { createTestClient } from '@zenstackhq/testtools'; + +describe('Client exists tests', () => { + let client: ClientContract; + + beforeEach(async () => { + client = await createTestClient(schema); + }); + + afterEach(async () => { + await client?.$disconnect(); + }); + + it('works with no args', async () => { + await expect(client.user.exists()).resolves.toBe(false); + + await client.user.create({ + data: { + email: 'test@email.com', + }, + }); + + await expect(client.user.exists()).resolves.toBe(true); + }); + + it('works with empty args', async () => { + await expect(client.user.exists({})).resolves.toBe(false); + + await client.user.create({ + data: { + email: 'test@email.com', + }, + }); + + await expect(client.user.exists({})).resolves.toBe(true); + }); + + it('works with empty where', async () => { + await expect(client.user.exists({ where: {} })).resolves.toBe(false); + + await client.user.create({ + data: { + email: 'test@email.com', + }, + }); + + await expect(client.user.exists({ where: {} })).resolves.toBe(true); + }); + + it('works with toplevel', async () => { + await client.user.create({ + data: { + email: 'test@email.com', + }, + }); + + await expect(client.user.exists({ + where: { + email: 'test@email.com', + }, + })).resolves.toBe(true); + + await expect(client.user.exists({ + where: { + email: 'wrong@email.com', + }, + })).resolves.toBe(false); + }); + + it('works with nested', async () => { + await client.user.create({ + data: { + email: 'test@email.com', + posts: { + create: { + title: 'Test title', + }, + }, + }, + }); + + await expect(client.user.exists({ + where: { + posts: { + some: { + title: 'Test title', + }, + }, + }, + })).resolves.toBe(true); + + await expect(client.user.exists({ + where: { + posts: { + some: { + title: 'Wrong test title', + }, + }, + }, + })).resolves.toBe(false); + + await expect(client.post.exists({ + where: { + title: 'Test title', + } + })).resolves.toBe(true); + + await expect(client.post.exists({ + where: { + title: 'Wrong test title', + } + })).resolves.toBe(false); + }); + + it('works with deeply nested', async () => { + await client.user.create({ + data: { + email: 'test@email.com', + posts: { + create: { + title: 'Test title', + comments: { + create: { + content: 'Test content', + }, + }, + }, + }, + }, + }); + + await expect(client.user.exists({ + where: { + posts: { + some: { + title: 'Test title', + comments: { + some: { + content: 'Test content', + }, + }, + }, + }, + }, + })).resolves.toBe(true); + + await expect(client.user.exists({ + where: { + posts: { + some: { + title: 'Test title', + comments: { + some: { + content: 'Wrong test content', + }, + }, + }, + }, + }, + })).resolves.toBe(false); + + await expect(client.user.exists({ + where: { + posts: { + some: { + title: 'Wrong test title', + comments: { + some: { + content: 'Test content', + }, + }, + }, + }, + }, + })).resolves.toBe(false); + }); +}); \ No newline at end of file From a6f3afa9a66a7c77ee935f4dd8d7334602590022 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 30 Dec 2025 08:35:27 +0000 Subject: [PATCH 5/9] Fix postgres error. --- packages/orm/src/client/crud/operations/base.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index ad9c56534..f89342d56 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -150,7 +150,7 @@ export abstract class BaseOperationHandler { kysely: ToKysely, model: GetModels, filter: any, - ): Promise { + ): Promise { const query = kysely.selectNoFrom((eb) => ( eb.exists( this.dialect @@ -161,16 +161,16 @@ export abstract class BaseOperationHandler { ).as('exists') )).modifyEnd(this.makeContextComment({ model, operation: 'read' })); - let result: {exists: number}[] = []; + let result: { exists: number | boolean }[] = []; const compiled = kysely.getExecutor().compileQuery(query.toOperationNode(), createQueryId()); try { const r = await kysely.getExecutor().executeQuery(compiled); - result = r.rows as {exists: number}[]; + result = r.rows as { exists: number | boolean}[]; } catch (err) { throw createDBQueryError('Failed to execute query', err, compiled.sql, compiled.parameters); } - return result[0]?.exists === 1; + return !!result[0]?.exists; } protected async read( From e861c1366a4e9f82f29980e04f949830bfeda7f9 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Wed, 7 Jan 2026 09:30:08 +0000 Subject: [PATCH 6/9] Add JSDoc. --- packages/orm/src/client/contract.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/orm/src/client/contract.ts b/packages/orm/src/client/contract.ts index cf4c02f94..3b9bb2e33 100644 --- a/packages/orm/src/client/contract.ts +++ b/packages/orm/src/client/contract.ts @@ -830,6 +830,23 @@ export type AllModelOperations< args: Subset>, ): ZenStackPromise>>; + /** + * Checks if an entity exists. + * @param args - exists args + * @returns whether a matching entity was found + * + * @example + * ```ts + * // check if a user exists + * await db.user.exists({ + * where: { id: 1 }, + * }); // result: `boolean` + * + * // check with a relation + * await db.user.exists({ + * where: { posts: { some: { published: true } } }, + * }); // result: `boolean` + */ exists>( args?: Subset>, ): ZenStackPromise; From 7288e7c90937b9abafd68074afb9864b05ce33e9 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Wed, 7 Jan 2026 09:30:35 +0000 Subject: [PATCH 7/9] Remove `@ts-expect-error` --- packages/orm/src/client/crud/operations/base.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 427b7cdf9..dbc844e06 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -156,8 +156,7 @@ export abstract class BaseOperationHandler { eb.exists( this.dialect .buildSelectModel(model, model) - // @ts-expect-error - .select(sql.lit(1)) + .select(sql.lit(1).as('$t')) .where(() => this.dialect.buildFilter(model, model, filter)) ).as('exists') )).modifyEnd(this.makeContextComment({ model, operation: 'read' })); From 06509cfbb83b187a837e767947fb36c108595e52 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Wed, 7 Jan 2026 09:31:38 +0000 Subject: [PATCH 8/9] Disable post-processing. --- packages/orm/src/client/client-impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/src/client/client-impl.ts b/packages/orm/src/client/client-impl.ts index ef81cc71c..7d7303148 100644 --- a/packages/orm/src/client/client-impl.ts +++ b/packages/orm/src/client/client-impl.ts @@ -606,7 +606,7 @@ function createModelCrudHandler( 'exists', args, new ExistsOperationHandler(client, model, inputValidator), - true, + false, ); }, } as ModelOperations; From 9ff7e5f5f7c9db0e962780de45d012a480b6171f Mon Sep 17 00:00:00 2001 From: sanny-io <3054653+sanny-io@users.noreply.github.com> Date: Wed, 7 Jan 2026 01:53:54 -0800 Subject: [PATCH 9/9] Put semicolon back. --- packages/orm/src/client/crud/operations/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index dbc844e06..fd32aa723 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -72,7 +72,7 @@ export type CoreCrudOperation = | 'groupBy' | 'exists'; -export type AllCrudOperation = CoreCrudOperation | 'findUniqueOrThrow' | 'findFirstOrThrow' +export type AllCrudOperation = CoreCrudOperation | 'findUniqueOrThrow' | 'findFirstOrThrow'; // context for nested relation operations export type FromRelationContext = {