From b4db4cd48c2adecb2c68491b94bbeea7f7ea7acc Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:43:05 -0700 Subject: [PATCH] merge from svetch/main --- .../src/client/crud/operations/find.ts | 23 +++++++++++-------- packages/runtime/src/client/crud/validator.ts | 21 +++++++++-------- .../test/client-api/computed-fields.test.ts | 2 +- packages/runtime/test/client-api/find.test.ts | 6 ++++- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/packages/runtime/src/client/crud/operations/find.ts b/packages/runtime/src/client/crud/operations/find.ts index e0d2440f..49938c8c 100644 --- a/packages/runtime/src/client/crud/operations/find.ts +++ b/packages/runtime/src/client/crud/operations/find.ts @@ -7,23 +7,26 @@ export class FindOperationHandler extends BaseOperatio // normalize args to strip `undefined` fields const normalizedArgs = this.normalizeArgs(args); + const findOne = operation === 'findFirst' || operation === 'findUnique'; + // parse args - const parsedArgs = (validateArgs - ? this.inputValidator.validateFindArgs(this.model, operation === 'findUnique', normalizedArgs) - : normalizedArgs) as FindArgs, true>; + let parsedArgs = validateArgs + ? this.inputValidator.validateFindArgs(this.model, normalizedArgs, { + unique: operation === 'findUnique', + findOne, + }) + : (normalizedArgs as FindArgs, true> | undefined); - if (operation === 'findFirst') { + if (findOne) { + // ensure "limit 1" + parsedArgs = parsedArgs ?? {}; parsedArgs.take = 1; } // run query - const result = await this.read( - this.client.$qb, - this.model, - parsedArgs, - ); + const result = await this.read(this.client.$qb, this.model, parsedArgs); - const finalResult = operation === 'findMany' ? result : (result[0] ?? null); + const finalResult = findOne ? (result[0] ?? null) : result; return finalResult; } } diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index f2eb732a..b4097dea 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -40,14 +40,11 @@ export class InputValidator { constructor(private readonly schema: Schema) {} - validateFindArgs(model: GetModels, unique: boolean, args: unknown) { - return this.validate, true>, Parameters[1]>( - model, - 'find', - { unique }, - (model, options) => this.makeFindSchema(model, options), - args, - ); + validateFindArgs(model: GetModels, args: unknown, options: { unique: boolean; findOne: boolean }) { + return this.validate< + FindArgs, true> | undefined, + Parameters[1] + >(model, 'find', options, (model, options) => this.makeFindSchema(model, options), args); } validateCreateArgs(model: GetModels, args: unknown) { @@ -196,7 +193,7 @@ export class InputValidator { // #region Find - private makeFindSchema(model: string, options: { unique: boolean }) { + private makeFindSchema(model: string, options: { unique: boolean; findOne: boolean }) { const fields: Record = {}; const where = this.makeWhereSchema(model, options.unique); if (options.unique) { @@ -211,7 +208,11 @@ export class InputValidator { if (!options.unique) { fields['skip'] = this.makeSkipSchema().optional(); - fields['take'] = this.makeTakeSchema().optional(); + if (options.findOne) { + fields['take'] = z.literal(1).optional(); + } else { + fields['take'] = this.makeTakeSchema().optional(); + } fields['orderBy'] = this.orArray(this.makeOrderBySchema(model, true, false), true).optional(); fields['cursor'] = this.makeCursorSchema(model).optional(); fields['distinct'] = this.makeDistinctSchema(model).optional(); diff --git a/packages/runtime/test/client-api/computed-fields.test.ts b/packages/runtime/test/client-api/computed-fields.test.ts index 8ae18a91..054997a3 100644 --- a/packages/runtime/test/client-api/computed-fields.test.ts +++ b/packages/runtime/test/client-api/computed-fields.test.ts @@ -75,7 +75,7 @@ model User { await expect( db.user.findFirst({ orderBy: { upperName: 'desc' }, - take: -1, + take: 1, }), ).resolves.toMatchObject({ upperName: 'ALEX', diff --git a/packages/runtime/test/client-api/find.test.ts b/packages/runtime/test/client-api/find.test.ts index adea8d3b..36d20eba 100644 --- a/packages/runtime/test/client-api/find.test.ts +++ b/packages/runtime/test/client-api/find.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import type { ClientContract } from '../../src/client'; -import { NotFoundError } from '../../src/client/errors'; +import { InputValidationError, NotFoundError } from '../../src/client/errors'; import { schema } from '../schemas/basic'; import { createClientSpecs } from './client-specs'; import { createPosts, createUser } from './utils'; @@ -54,6 +54,10 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', await expect(client.user.findMany({ take: 2 })).resolves.toHaveLength(2); await expect(client.user.findMany({ take: 4 })).resolves.toHaveLength(3); + // findFirst's take must be 1 + await expect(client.user.findFirst({ take: 2 })).rejects.toThrow(InputValidationError); + await expect(client.user.findFirst({ take: 1 })).toResolveTruthy(); + // skip await expect(client.user.findMany({ skip: 1 })).resolves.toHaveLength(2); await expect(client.user.findMany({ skip: 2 })).resolves.toHaveLength(1);