Skip to content

Commit f2199db

Browse files
authored
fix: relation selection input validation issue (#175)
* fix: relation selection input validation issue * update * fix test
1 parent 1659789 commit f2199db

File tree

3 files changed

+124
-29
lines changed

3 files changed

+124
-29
lines changed

packages/runtime/src/client/crud-types.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -631,16 +631,15 @@ export type FindArgs<
631631
skip?: number;
632632
take?: number;
633633
orderBy?: OrArray<OrderBy<Schema, Model, true, false>>;
634-
}
634+
} & Distinct<Schema, Model> &
635+
Cursor<Schema, Model>
635636
: {}) &
636637
(AllowFilter extends true
637638
? {
638639
where?: WhereInput<Schema, Model>;
639640
}
640641
: {}) &
641-
SelectIncludeOmit<Schema, Model, Collection> &
642-
Distinct<Schema, Model> &
643-
Cursor<Schema, Model>;
642+
SelectIncludeOmit<Schema, Model, Collection>;
644643

645644
export type FindManyArgs<Schema extends SchemaDef, Model extends GetModels<Schema>> = FindArgs<Schema, Model, true>;
646645
export type FindFirstArgs<Schema extends SchemaDef, Model extends GetModels<Schema>> = FindArgs<Schema, Model, false>;

packages/runtime/src/client/crud/validator.ts

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -589,15 +589,7 @@ export class InputValidator<Schema extends SchemaDef> {
589589
for (const field of Object.keys(modelDef.fields)) {
590590
const fieldDef = requireField(this.schema, model, field);
591591
if (fieldDef.relation) {
592-
fields[field] = z
593-
.union([
594-
z.literal(true),
595-
z.strictObject({
596-
select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(),
597-
include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(),
598-
}),
599-
])
600-
.optional();
592+
fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional();
601593
} else {
602594
fields[field] = z.boolean().optional();
603595
}
@@ -634,6 +626,33 @@ export class InputValidator<Schema extends SchemaDef> {
634626
return z.strictObject(fields);
635627
}
636628

629+
private makeRelationSelectIncludeSchema(fieldDef: FieldDef) {
630+
return z.union([
631+
z.boolean(),
632+
z.strictObject({
633+
...(fieldDef.array || fieldDef.optional
634+
? {
635+
// to-many relations and optional to-one relations are filterable
636+
where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(),
637+
}
638+
: {}),
639+
select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(),
640+
include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(),
641+
omit: z.lazy(() => this.makeOmitSchema(fieldDef.type)).optional(),
642+
...(fieldDef.array
643+
? {
644+
// to-many relations can be ordered, skipped, taken, and cursor-located
645+
orderBy: z.lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)).optional(),
646+
skip: this.makeSkipSchema().optional(),
647+
take: this.makeTakeSchema().optional(),
648+
cursor: this.makeCursorSchema(fieldDef.type).optional(),
649+
distinct: this.makeDistinctSchema(fieldDef.type).optional(),
650+
}
651+
: {}),
652+
}),
653+
]);
654+
}
655+
637656
private makeOmitSchema(model: string) {
638657
const modelDef = requireModel(this.schema, model);
639658
const fields: Record<string, ZodType> = {};
@@ -652,21 +671,7 @@ export class InputValidator<Schema extends SchemaDef> {
652671
for (const field of Object.keys(modelDef.fields)) {
653672
const fieldDef = requireField(this.schema, model, field);
654673
if (fieldDef.relation) {
655-
fields[field] = z
656-
.union([
657-
z.literal(true),
658-
z.strictObject({
659-
select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(),
660-
include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(),
661-
omit: z.lazy(() => this.makeOmitSchema(fieldDef.type)).optional(),
662-
where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(),
663-
orderBy: z.lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)).optional(),
664-
skip: this.makeSkipSchema().optional(),
665-
take: this.makeTakeSchema().optional(),
666-
distinct: this.makeDistinctSchema(fieldDef.type).optional(),
667-
}),
668-
])
669-
.optional();
674+
fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional();
670675
}
671676
}
672677

packages/runtime/test/client-api/find.test.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,97 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider',
637637
expect(r?.posts[0]?.createdAt).toBeInstanceOf(Date);
638638
expect(r?.posts[0]?.published).toBeTypeOf('boolean');
639639

640+
await expect(
641+
client.user.findUnique({
642+
where: { id: user.id },
643+
select: {
644+
posts: { where: { published: true }, select: { title: true }, orderBy: { createdAt: 'desc' } },
645+
},
646+
}),
647+
).resolves.toMatchObject({
648+
posts: [expect.objectContaining({ title: 'Post1' })],
649+
});
650+
await expect(
651+
client.user.findUnique({
652+
where: { id: user.id },
653+
include: {
654+
posts: { where: { published: true }, select: { title: true }, orderBy: { createdAt: 'desc' } },
655+
},
656+
}),
657+
).resolves.toMatchObject({
658+
posts: [expect.objectContaining({ title: 'Post1' })],
659+
});
660+
661+
await expect(
662+
client.user.findUnique({
663+
where: { id: user.id },
664+
select: {
665+
posts: { orderBy: { title: 'asc' }, skip: 1, take: 1, distinct: ['title'] },
666+
},
667+
}),
668+
).resolves.toMatchObject({
669+
posts: [expect.objectContaining({ title: 'Post2' })],
670+
});
671+
await expect(
672+
client.user.findUnique({
673+
where: { id: user.id },
674+
include: {
675+
posts: { orderBy: { title: 'asc' }, skip: 1, take: 1, distinct: ['title'] },
676+
},
677+
}),
678+
).resolves.toMatchObject({
679+
posts: [expect.objectContaining({ title: 'Post2' })],
680+
});
681+
682+
await expect(
683+
client.post.findFirst({
684+
select: { author: { select: { email: true } } },
685+
}),
686+
).resolves.toMatchObject({
687+
author: { email: expect.any(String) },
688+
});
689+
await expect(
690+
client.post.findFirst({
691+
include: { author: { select: { email: true } } },
692+
}),
693+
).resolves.toMatchObject({
694+
author: { email: expect.any(String) },
695+
});
696+
697+
await expect(
698+
client.user.findUnique({
699+
where: { id: user.id },
700+
select: {
701+
profile: { where: { bio: 'My bio' } },
702+
},
703+
}),
704+
).resolves.toMatchObject({ profile: expect.any(Object) });
705+
await expect(
706+
client.user.findUnique({
707+
where: { id: user.id },
708+
include: {
709+
profile: { where: { bio: 'My bio' } },
710+
},
711+
}),
712+
).resolves.toMatchObject({ profile: expect.any(Object) });
713+
714+
await expect(
715+
client.user.findUnique({
716+
where: { id: user.id },
717+
select: {
718+
profile: { where: { bio: 'Other bio' } },
719+
},
720+
}),
721+
).resolves.toMatchObject({ profile: null });
722+
await expect(
723+
client.user.findUnique({
724+
where: { id: user.id },
725+
include: {
726+
profile: { where: { bio: 'Other bio' } },
727+
},
728+
}),
729+
).resolves.toMatchObject({ profile: null });
730+
640731
await expect(
641732
client.user.findUnique({
642733
where: { id: user.id },
@@ -778,7 +869,7 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider',
778869
// @ts-expect-error
779870
include: { author: { where: { email: user.email } } },
780871
}),
781-
).rejects.toThrow(`Field "author" doesn't support filtering`);
872+
).rejects.toThrow(`Invalid find args`);
782873

783874
// sorting
784875
let u = await client.user.findUniqueOrThrow({

0 commit comments

Comments
 (0)