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..7ef9d677 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -589,15 +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({ - select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), - include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), - }), - ]) - .optional(); + fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); } else { fields[field] = z.boolean().optional(); } @@ -634,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 = {}; @@ -652,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 9b588a03..70cc400c 100644 --- a/packages/runtime/test/client-api/find.test.ts +++ b/packages/runtime/test/client-api/find.test.ts @@ -637,6 +637,97 @@ 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 }, + include: { + 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.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({ + select: { author: { select: { email: true } } }, + }), + ).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 }, + include: { + profile: { where: { bio: 'My bio' } }, + }, + }), + ).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 }, + include: { + profile: { where: { bio: 'Other bio' } }, + }, + }), + ).resolves.toMatchObject({ profile: null }); + await expect( client.user.findUnique({ where: { id: user.id }, @@ -778,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({