From 7b447315730af7dc939864be4166dbf087e77ad5 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:00:41 +0800 Subject: [PATCH 1/3] fix: relation selection input validation issue --- packages/runtime/src/client/crud-types.ts | 7 ++- packages/runtime/src/client/crud/validator.ts | 19 ++++++++ packages/runtime/test/client-api/find.test.ts | 48 +++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/packages/runtime/src/client/crud-types.ts b/packages/runtime/src/client/crud-types.ts index b41cf728..728082d7 100644 --- a/packages/runtime/src/client/crud-types.ts +++ b/packages/runtime/src/client/crud-types.ts @@ -631,16 +631,15 @@ export type FindArgs< skip?: number; take?: number; orderBy?: OrArray>; - } + } & Distinct & + Cursor : {}) & (AllowFilter extends true ? { where?: WhereInput; } : {}) & - SelectIncludeOmit & - Distinct & - Cursor; + SelectIncludeOmit; export type FindManyArgs> = FindArgs; export type FindFirstArgs> = FindArgs; diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index 5b660aee..aa7f8b8d 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -593,8 +593,27 @@ export class InputValidator { .union([ z.literal(true), z.strictObject({ + ...(fieldDef.array || fieldDef.optional + ? { + // to-many relations and optional to-one relations are filterable + where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), + } + : {}), select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), + omit: z.lazy(() => this.makeOmitSchema(fieldDef.type)).optional(), + ...(fieldDef.array + ? { + // to-many relations can be ordered, skipped, taken, and cursor-located + orderBy: z + .lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)) + .optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + cursor: this.makeCursorSchema(fieldDef.type).optional(), + distinct: this.makeDistinctSchema(fieldDef.type).optional(), + } + : {}), }), ]) .optional(); diff --git a/packages/runtime/test/client-api/find.test.ts b/packages/runtime/test/client-api/find.test.ts index 9b588a03..eb883104 100644 --- a/packages/runtime/test/client-api/find.test.ts +++ b/packages/runtime/test/client-api/find.test.ts @@ -637,6 +637,54 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', expect(r?.posts[0]?.createdAt).toBeInstanceOf(Date); expect(r?.posts[0]?.published).toBeTypeOf('boolean'); + await expect( + client.user.findUnique({ + where: { id: user.id }, + select: { + posts: { where: { published: true }, select: { title: true }, orderBy: { createdAt: 'desc' } }, + }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ title: 'Post1' })], + }); + + await expect( + client.user.findUnique({ + where: { id: user.id }, + select: { + posts: { orderBy: { title: 'asc' }, skip: 1, take: 1, distinct: ['title'] }, + }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ title: 'Post2' })], + }); + + await expect( + client.post.findFirst({ + select: { author: { select: { email: true } } }, + }), + ).resolves.toMatchObject({ + author: { email: expect.any(String) }, + }); + + await expect( + client.user.findUnique({ + where: { id: user.id }, + include: { + profile: { where: { bio: 'My bio' } }, + }, + }), + ).resolves.toMatchObject({ profile: expect.any(Object) }); + + await expect( + client.user.findUnique({ + where: { id: user.id }, + include: { + profile: { where: { bio: 'Other bio' } }, + }, + }), + ).resolves.toMatchObject({ profile: null }); + await expect( client.user.findUnique({ where: { id: user.id }, From e4970614fc1fc685e980a8f8fa61923e1fbd0151 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:09:29 +0800 Subject: [PATCH 2/3] update --- packages/runtime/src/client/crud/validator.ts | 72 ++++++++----------- packages/runtime/test/client-api/find.test.ts | 43 +++++++++++ 2 files changed, 72 insertions(+), 43 deletions(-) diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index aa7f8b8d..7ef9d677 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -589,34 +589,7 @@ export class InputValidator { for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { - fields[field] = z - .union([ - z.literal(true), - z.strictObject({ - ...(fieldDef.array || fieldDef.optional - ? { - // to-many relations and optional to-one relations are filterable - where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), - } - : {}), - select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), - include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), - omit: z.lazy(() => this.makeOmitSchema(fieldDef.type)).optional(), - ...(fieldDef.array - ? { - // to-many relations can be ordered, skipped, taken, and cursor-located - orderBy: z - .lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)) - .optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - cursor: this.makeCursorSchema(fieldDef.type).optional(), - distinct: this.makeDistinctSchema(fieldDef.type).optional(), - } - : {}), - }), - ]) - .optional(); + fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); } else { fields[field] = z.boolean().optional(); } @@ -653,6 +626,33 @@ export class InputValidator { return z.strictObject(fields); } + private makeRelationSelectIncludeSchema(fieldDef: FieldDef) { + return z.union([ + z.boolean(), + z.strictObject({ + ...(fieldDef.array || fieldDef.optional + ? { + // to-many relations and optional to-one relations are filterable + where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), + } + : {}), + select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), + include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), + omit: z.lazy(() => this.makeOmitSchema(fieldDef.type)).optional(), + ...(fieldDef.array + ? { + // to-many relations can be ordered, skipped, taken, and cursor-located + orderBy: z.lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)).optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + cursor: this.makeCursorSchema(fieldDef.type).optional(), + distinct: this.makeDistinctSchema(fieldDef.type).optional(), + } + : {}), + }), + ]); + } + private makeOmitSchema(model: string) { const modelDef = requireModel(this.schema, model); const fields: Record = {}; @@ -671,21 +671,7 @@ export class InputValidator { for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { - fields[field] = z - .union([ - z.literal(true), - z.strictObject({ - select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), - include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), - omit: z.lazy(() => this.makeOmitSchema(fieldDef.type)).optional(), - where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), - orderBy: z.lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)).optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - distinct: this.makeDistinctSchema(fieldDef.type).optional(), - }), - ]) - .optional(); + fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); } } diff --git a/packages/runtime/test/client-api/find.test.ts b/packages/runtime/test/client-api/find.test.ts index eb883104..42ffce90 100644 --- a/packages/runtime/test/client-api/find.test.ts +++ b/packages/runtime/test/client-api/find.test.ts @@ -647,6 +647,16 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', ).resolves.toMatchObject({ posts: [expect.objectContaining({ title: 'Post1' })], }); + await expect( + client.user.findUnique({ + where: { id: user.id }, + include: { + posts: { where: { published: true }, select: { title: true }, orderBy: { createdAt: 'desc' } }, + }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ title: 'Post1' })], + }); await expect( client.user.findUnique({ @@ -658,6 +668,16 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', ).resolves.toMatchObject({ posts: [expect.objectContaining({ title: 'Post2' })], }); + await expect( + client.user.findUnique({ + where: { id: user.id }, + include: { + posts: { orderBy: { title: 'asc' }, skip: 1, take: 1, distinct: ['title'] }, + }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ title: 'Post2' })], + }); await expect( client.post.findFirst({ @@ -666,7 +686,22 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', ).resolves.toMatchObject({ author: { email: expect.any(String) }, }); + await expect( + client.post.findFirst({ + include: { author: { select: { email: true } } }, + }), + ).resolves.toMatchObject({ + author: { email: expect.any(String) }, + }); + await expect( + client.user.findUnique({ + where: { id: user.id }, + select: { + profile: { where: { bio: 'My bio' } }, + }, + }), + ).resolves.toMatchObject({ profile: expect.any(Object) }); await expect( client.user.findUnique({ where: { id: user.id }, @@ -676,6 +711,14 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', }), ).resolves.toMatchObject({ profile: expect.any(Object) }); + await expect( + client.user.findUnique({ + where: { id: user.id }, + select: { + profile: { where: { bio: 'Other bio' } }, + }, + }), + ).resolves.toMatchObject({ profile: null }); await expect( client.user.findUnique({ where: { id: user.id }, From 3eba80a038beb1dadc2a17b96b2d4c0c8b1922f5 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:19:20 +0800 Subject: [PATCH 3/3] fix test --- packages/runtime/test/client-api/find.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/test/client-api/find.test.ts b/packages/runtime/test/client-api/find.test.ts index 42ffce90..70cc400c 100644 --- a/packages/runtime/test/client-api/find.test.ts +++ b/packages/runtime/test/client-api/find.test.ts @@ -869,7 +869,7 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', // @ts-expect-error include: { author: { where: { email: user.email } } }, }), - ).rejects.toThrow(`Field "author" doesn't support filtering`); + ).rejects.toThrow(`Invalid find args`); // sorting let u = await client.user.findUniqueOrThrow({