Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/clients/tanstack-query/src/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {
CreateManyArgs,
DeleteArgs,
DeleteManyArgs,
ExistsArgs,
FindFirstArgs,
FindManyArgs,
FindUniqueArgs,
Expand Down Expand Up @@ -165,6 +166,11 @@ export type ModelQueryHooks<
options?: ModelSuspenseQueryOptions<SimplifiedPlainResult<Schema, Model, T, Options> | null>,
): ModelSuspenseQueryResult<SimplifiedPlainResult<Schema, Model, T, Options> | null>;

useExists<T extends ExistsArgs<Schema, Model>>(
args?: Subset<T, ExistsArgs<Schema, Model>>,
options?: ModelQueryOptions<boolean>,
): ModelQueryResult<boolean>;

useFindMany<T extends FindManyArgs<Schema, Model>>(
args?: SelectSubset<T, FindManyArgs<Schema, Model>>,
options?: ModelQueryOptions<SimplifiedPlainResult<Schema, Model, T, Options>[]>,
Expand Down Expand Up @@ -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 });
},
Expand Down
10 changes: 10 additions & 0 deletions packages/clients/tanstack-query/src/svelte/index.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type {
CreateManyArgs,
DeleteArgs,
DeleteManyArgs,
ExistsArgs,
FindFirstArgs,
FindManyArgs,
FindUniqueArgs,
Expand Down Expand Up @@ -144,6 +145,11 @@ export type ModelQueryHooks<
options?: Accessor<ModelQueryOptions<SimplifiedPlainResult<Schema, Model, T, Options> | null>>,
): ModelQueryResult<SimplifiedPlainResult<Schema, Model, T, Options> | null>;

useExists<T extends ExistsArgs<Schema, Model>>(
args?: Accessor<Subset<T, ExistsArgs<Schema, Model>>>,
options?: Accessor<ModelQueryOptions<boolean>>,
): ModelQueryResult<boolean>;

useFindMany<T extends FindManyArgs<Schema, Model>>(
args?: Accessor<SelectSubset<T, FindManyArgs<Schema, Model>>>,
options?: Accessor<ModelQueryOptions<SimplifiedPlainResult<Schema, Model, T, Options>[]>>,
Expand Down Expand Up @@ -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));
},
Expand Down
10 changes: 10 additions & 0 deletions packages/clients/tanstack-query/src/vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
CreateManyArgs,
DeleteArgs,
DeleteManyArgs,
ExistsArgs,
FindFirstArgs,
FindManyArgs,
FindUniqueArgs,
Expand Down Expand Up @@ -145,6 +146,11 @@ export type ModelQueryHooks<
options?: MaybeRefOrGetter<ModelQueryOptions<SimplifiedPlainResult<Schema, Model, T, Options> | null>>,
): ModelQueryResult<SimplifiedPlainResult<Schema, Model, T, Options> | null>;

useExists<T extends ExistsArgs<Schema, Model>>(
args?: MaybeRefOrGetter<Subset<T, ExistsArgs<Schema, Model>>>,
options?: MaybeRefOrGetter<ModelQueryOptions<boolean>>,
): ModelQueryResult<boolean>;

useFindMany<T extends FindManyArgs<Schema, Model>>(
args?: MaybeRefOrGetter<SelectSubset<T, FindManyArgs<Schema, Model>>>,
options?: MaybeRefOrGetter<ModelQueryOptions<SimplifiedPlainResult<Schema, Model, T, Options>[]>>,
Expand Down Expand Up @@ -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));
},
Expand Down
3 changes: 3 additions & 0 deletions packages/clients/tanstack-query/test/react-typing-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
7 changes: 6 additions & 1 deletion packages/clients/tanstack-query/test/schemas/basic/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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">;
Expand All @@ -31,6 +32,7 @@ export type UserGetPayload<Args extends $SelectIncludeOmit<$Schema, "User", true
export type PostFindManyArgs = $FindManyArgs<$Schema, "Post">;
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">;
Expand All @@ -51,6 +53,7 @@ export type PostGetPayload<Args extends $SelectIncludeOmit<$Schema, "Post", true
export type CategoryFindManyArgs = $FindManyArgs<$Schema, "Category">;
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">;
Expand All @@ -71,6 +74,7 @@ export type CategoryGetPayload<Args extends $SelectIncludeOmit<$Schema, "Categor
export type FooFindManyArgs = $FindManyArgs<$Schema, "Foo">;
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">;
Expand All @@ -91,6 +95,7 @@ export type FooGetPayload<Args extends $SelectIncludeOmit<$Schema, "Foo", true>,
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">;
Expand Down
3 changes: 3 additions & 0 deletions packages/clients/tanstack-query/test/svelte-typing-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
3 changes: 3 additions & 0 deletions packages/clients/tanstack-query/test/vue-typing-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
11 changes: 11 additions & 0 deletions packages/orm/src/client/client-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -598,5 +599,15 @@ function createModelCrudHandler(
true,
);
},

exists: (args: unknown) => {
return createPromise(
'exists',
'exists',
args,
new ExistsOperationHandler<any>(client, model, inputValidator),
false,
);
},
} as ModelOperations<any, any>;
}
22 changes: 22 additions & 0 deletions packages/orm/src/client/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
DefaultModelResult,
DeleteArgs,
DeleteManyArgs,
ExistsArgs,
FindFirstArgs,
FindManyArgs,
FindUniqueArgs,
Expand Down Expand Up @@ -828,6 +829,27 @@ export type AllModelOperations<
groupBy<T extends GroupByArgs<Schema, Model>>(
args: Subset<T, GroupByArgs<Schema, Model>>,
): ZenStackPromise<Schema, Simplify<GroupByResult<Schema, Model, T>>>;

/**
* 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<T extends ExistsArgs<Schema, Model>>(
Comment thread
sanny-io marked this conversation as resolved.
args?: Subset<T, ExistsArgs<Schema, Model>>,
): ZenStackPromise<Schema, boolean>;
};

export type OperationsIneligibleForDelegateModels = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert';
Expand Down
2 changes: 2 additions & 0 deletions packages/orm/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,8 @@ export type FindManyArgs<Schema extends SchemaDef, Model extends GetModels<Schem

export type FindFirstArgs<Schema extends SchemaDef, Model extends GetModels<Schema>> = FindArgs<Schema, Model, true>;

export type ExistsArgs<Schema extends SchemaDef, Model extends GetModels<Schema>> = FilterArgs<Schema, Model>;

export type FindUniqueArgs<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
where: WhereUniqueInput<Schema, Model>;
} & SelectIncludeOmit<Schema, Model, true>;
Expand Down
29 changes: 28 additions & 1 deletion packages/orm/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export type CoreCrudOperation =
| 'deleteMany'
| 'count'
| 'aggregate'
| 'groupBy';
| 'groupBy'
| 'exists';

export type AllCrudOperation = CoreCrudOperation | 'findUniqueOrThrow' | 'findFirstOrThrow';

Expand Down Expand Up @@ -146,6 +147,32 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
});
}

protected async existsNonUnique(
Comment thread
sanny-io marked this conversation as resolved.
kysely: ToKysely<Schema>,
model: GetModels<Schema>,
filter: any,
): Promise<boolean> {
const query = kysely.selectNoFrom((eb) => (
eb.exists(
this.dialect
.buildSelectModel(model, model)
Comment thread
sanny-io marked this conversation as resolved.
.select(sql.lit(1).as('$t'))
.where(() => this.dialect.buildFilter(model, model, filter))
).as('exists')
)).modifyEnd(this.makeContextComment({ model, operation: 'read' }));

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 | boolean}[];
} catch (err) {
throw createDBQueryError('Failed to execute query', err, compiled.sql, compiled.parameters);
}

return !!result[0]?.exists;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

protected async read(
kysely: AnyKysely,
model: string,
Expand Down
12 changes: 12 additions & 0 deletions packages/orm/src/client/crud/operations/exists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { SchemaDef } from '../../../schema';
import { BaseOperationHandler } from './base';

export class ExistsOperationHandler<Schema extends SchemaDef> extends BaseOperationHandler<Schema> {
async handle(_operation: 'exists', args: unknown): Promise<unknown> {
// normalize args to strip `undefined` fields
Comment thread
sanny-io marked this conversation as resolved.
const normalizedArgs = this.normalizeArgs(args);
const parsedArgs = this.inputValidator.validateExistsArgs(this.model, normalizedArgs);

return await this.existsNonUnique(this.client.$qb, this.model, parsedArgs?.where);
}
}
20 changes: 20 additions & 0 deletions packages/orm/src/client/crud/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
type CreateManyArgs,
type DeleteArgs,
type DeleteManyArgs,
type ExistsArgs,
type FindArgs,
type GroupByArgs,
type UpdateArgs,
Expand Down Expand Up @@ -81,6 +82,19 @@ export class InputValidator<Schema extends SchemaDef> {
>(model, 'find', options, (model, options) => this.makeFindSchema(model, options), args);
}

validateExistsArgs(
model: GetModels<Schema>,
args: unknown,
): ExistsArgs<Schema, GetModels<Schema>> | undefined {
return this.validate<ExistsArgs<Schema, GetModels<Schema>>>(
model,
'exists',
undefined,
(model) => this.makeExistsSchema(model),
args,
);
}

validateCreateArgs(model: GetModels<Schema>, args: unknown): CreateArgs<Schema, GetModels<Schema>> {
return this.validate<CreateArgs<Schema, GetModels<Schema>>>(
model,
Expand Down Expand Up @@ -297,6 +311,12 @@ export class InputValidator<Schema extends SchemaDef> {
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);
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/ts-schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,7 @@ export class TsSchemaGenerator {
'FindManyArgs',
'FindUniqueArgs',
'FindFirstArgs',
'ExistsArgs',
'CreateArgs',
'CreateManyArgs',
'CreateManyAndReturnArgs',
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/api/rpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class RPCApiHandler<Schema extends SchemaDef = SchemaDef> implements ApiH
case 'aggregate':
case 'groupBy':
case 'count':
case 'exists':
if (method !== 'GET') {
return this.makeBadInputErrorResponse('invalid request method, only GET is supported');
}
Expand Down
18 changes: 18 additions & 0 deletions packages/server/test/api/rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
Loading
Loading