Skip to content
Open
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
53 changes: 52 additions & 1 deletion packages/orm/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1315,8 +1315,59 @@ export type Subset<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : never;
};

type PreserveNullish<T, Strict> = Strict | Extract<T, null | undefined>;

type NoExtraKeys<T, Shape> = Shape extends unknown
? T extends Shape
? T extends object
? T & { [K in Exclude<keyof T, keyof Shape>]: never }
: T
: never
: never;

type StrictExistingArg<T, Shape> = PreserveNullish<T, NoExtraKeys<NonNullable<T>, NonNullable<Shape>>>;

type StrictExistingArgKeys = 'where' | 'orderBy' | 'cursor';

type StrictQueryArgs<T, Shape> = T extends object
? {
[K in keyof T]: K extends keyof Shape
? K extends 'select' | 'include' | 'omit'
? StrictArgProperty<T[K], Shape, K>
: K extends StrictExistingArgKeys
? StrictExistingArg<T[K], Shape[K]>
: T[K]
: never;
}
: T;

type StrictSelectionValue<T, Shape> =
T extends object
? Extract<NonNullable<Shape>, object> extends infer ObjectShape extends object
? [ObjectShape] extends [never]
? T
: PreserveNullish<T, StrictQueryArgs<NonNullable<T>, ObjectShape> & ObjectShape>
: T
: T;

type StrictSelection<T, Shape> = T extends object
? {
[K in keyof T]: K extends keyof Shape ? StrictSelectionValue<T[K], Shape[K]> : never;
}
: T;

type StrictArgProperty<T, U, Key extends PropertyKey> = Key extends keyof U
? PreserveNullish<T, StrictSelection<NonNullable<T>, NonNullable<U[Key]>> & NonNullable<U[Key]>>
: T;

export type SelectSubset<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : never;
[key in keyof T]: key extends keyof U
? key extends 'select' | 'include' | 'omit'
? StrictArgProperty<T[key], U, key>
: key extends StrictExistingArgKeys
? StrictExistingArg<T[key], U[key]>
: T[key]
: never;
} & (T extends { select: any; include: any }
? 'Please either choose `select` or `include`.'
: T extends { select: any; omit: any }
Expand Down
5 changes: 5 additions & 0 deletions tests/e2e/orm/client-api/slicing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,15 @@ describe('Query slicing tests', () => {
// Profile is excluded, so selecting it should cause type error
await expect(
db.user.findMany({
// @ts-expect-error Profile is excluded by slicing
select: { id: true, profile: true },
}),
).toBeRejectedByValidation(['"profile"', '"select"']);

// Comment is excluded, so selecting it should cause type error
await expect(
db.post.findMany({
// @ts-expect-error Comment is excluded by slicing
select: { id: true, comments: true },
}),
).toBeRejectedByValidation(['"comments"', '"select"']);
Expand Down Expand Up @@ -295,13 +297,15 @@ describe('Query slicing tests', () => {
// Profile is not included, so selecting it should cause type error
await expect(
db.user.findMany({
// @ts-expect-error Profile is not included by slicing
select: { id: true, profile: true },
}),
).toBeRejectedByValidation(['"profile"', '"select"']);

// Comment is not included, so selecting it should cause type error
await expect(
db.post.findMany({
// @ts-expect-error Comment is not included by slicing
select: { id: true, comments: true },
}),
).toBeRejectedByValidation(['"comments"', '"select"']);
Expand Down Expand Up @@ -375,6 +379,7 @@ describe('Query slicing tests', () => {
select: {
id: true,
posts: {
// @ts-expect-error Comment is excluded by slicing
select: { id: true, comments: true },
},
},
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/orm/client-api/unsupported.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ describe('Unsupported field exclusion - Zod runtime validation', () => {
it('rejects Unsupported fields in select', async () => {
// valid call
await db.item.findMany({ select: { id: true, name: true } });
// @ts-expect-error data (Unsupported) should not be in select
await expect(db.item.findMany({ select: { data: true } })).toBeRejectedByValidation();
});

Expand All @@ -218,6 +219,7 @@ describe('Unsupported field exclusion - Zod runtime validation', () => {
it('rejects Unsupported fields in orderBy', async () => {
// valid call
await db.item.findMany({ orderBy: { name: 'asc' } });
// @ts-expect-error data (Unsupported) should not be in orderBy
await expect(db.item.findMany({ orderBy: { data: 'asc' } })).toBeRejectedByValidation();
});

Expand Down
74 changes: 74 additions & 0 deletions tests/e2e/orm/schemas/typing/typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,53 @@ async function find() {
});
console.log(user2.email);
console.log(user2.profile?.age);
// @ts-expect-error name was not selected
console.log(user2.name);

await client.user.findMany({
select: {
email: true,
// @ts-expect-error invalid select field
missingField: true,
},
});

await client.user.findMany({
select: {
posts: {
select: {
title: true,
// @ts-expect-error invalid nested select field
missingField: true,
},
},
},
});

await client.user.findMany({
include: {
posts: {
include: {
meta: {
select: {
reviewed: true,
// @ts-expect-error invalid deeply nested select field
missingField: true,
},
},
},
},
},
});

await client.user.findMany({
select: {
posts: {
// @ts-expect-error invalid nested omit field
omit: { missingField: true },
},
},
});

await client.user.findUnique({
// @ts-expect-error expect unique filter
Expand All @@ -91,16 +138,43 @@ async function find() {
where: { status: { hasEvery: [Status.ACTIVE] } },
});

await client.user.findMany({
where: {
email: 'alex@zenstack.dev',
// @ts-expect-error invalid where field
missingField: 'value',
},
});

await client.user.findMany({
skip: 1,
take: 1,
orderBy: {
email: 'asc',
name: 'desc',
// @ts-expect-error invalid orderBy field
missingField: 'asc',
},
cursor: { id: 1 },
});

await client.user.findMany({
select: {
posts: {
where: {
title: 'Hello',
// @ts-expect-error invalid nested where field
missingField: 'value',
},
orderBy: {
title: 'asc',
// @ts-expect-error invalid nested orderBy field
missingField: 'asc',
},
},
},
});

const user3 = await client.user.findFirstOrThrow({
select: {
_count: true,
Expand Down
Loading