diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index 1d6542a99..8166dc9b8 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -1315,8 +1315,59 @@ export type Subset = { [key in keyof T]: key extends keyof U ? T[key] : never; }; +type PreserveNullish = Strict | Extract; + +type NoExtraKeys = Shape extends unknown + ? T extends Shape + ? T extends object + ? T & { [K in Exclude]: never } + : T + : never + : never; + +type StrictExistingArg = PreserveNullish, NonNullable>>; + +type StrictExistingArgKeys = 'where' | 'orderBy' | 'cursor'; + +type StrictQueryArgs = T extends object + ? { + [K in keyof T]: K extends keyof Shape + ? K extends 'select' | 'include' | 'omit' + ? StrictArgProperty + : K extends StrictExistingArgKeys + ? StrictExistingArg + : T[K] + : never; + } + : T; + +type StrictSelectionValue = + T extends object + ? Extract, object> extends infer ObjectShape extends object + ? [ObjectShape] extends [never] + ? T + : PreserveNullish, ObjectShape> & ObjectShape> + : T + : T; + +type StrictSelection = T extends object + ? { + [K in keyof T]: K extends keyof Shape ? StrictSelectionValue : never; + } + : T; + +type StrictArgProperty = Key extends keyof U + ? PreserveNullish, NonNullable> & NonNullable> + : T; + export type SelectSubset = { - [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 + : key extends StrictExistingArgKeys + ? StrictExistingArg + : T[key] + : never; } & (T extends { select: any; include: any } ? 'Please either choose `select` or `include`.' : T extends { select: any; omit: any } diff --git a/tests/e2e/orm/client-api/slicing.test.ts b/tests/e2e/orm/client-api/slicing.test.ts index 4432b58ca..b07de0c93 100644 --- a/tests/e2e/orm/client-api/slicing.test.ts +++ b/tests/e2e/orm/client-api/slicing.test.ts @@ -216,6 +216,7 @@ 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"']); @@ -223,6 +224,7 @@ describe('Query slicing tests', () => { // 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"']); @@ -295,6 +297,7 @@ 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"']); @@ -302,6 +305,7 @@ describe('Query slicing tests', () => { // 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"']); @@ -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 }, }, }, diff --git a/tests/e2e/orm/client-api/unsupported.test.ts b/tests/e2e/orm/client-api/unsupported.test.ts index ad1daba45..933e79c6b 100644 --- a/tests/e2e/orm/client-api/unsupported.test.ts +++ b/tests/e2e/orm/client-api/unsupported.test.ts @@ -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(); }); @@ -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(); }); diff --git a/tests/e2e/orm/schemas/typing/typecheck.ts b/tests/e2e/orm/schemas/typing/typecheck.ts index 9f8b2aa86..13a8dc2b5 100644 --- a/tests/e2e/orm/schemas/typing/typecheck.ts +++ b/tests/e2e/orm/schemas/typing/typecheck.ts @@ -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 @@ -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,