Skip to content
Closed
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
49 changes: 27 additions & 22 deletions packages/orm/src/client/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,20 @@ import type {
FindUniqueArgs,
GroupByArgs,
GroupByResult,
OmitWhere,
ProcedureFunc,
SelectSubset,
SelectSubsetWithWhere,
SimplifiedPlainResult,
Subset,
SubsetWithWhere,
TypeDefResult,
UpdateArgs,
UpdateManyAndReturnArgs,
UpdateManyArgs,
UpsertArgs,
WhereInput,
WhereUniqueInput,
} from './crud-types';
import type { Diagnostics } from './diagnostics';
import type { ClientOptions, QueryOptions } from './options';
Expand Down Expand Up @@ -405,8 +410,8 @@ export type AllModelOperations<
* });
* ```
*/
updateManyAndReturn<T extends UpdateManyAndReturnArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: Subset<T, UpdateManyAndReturnArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
updateManyAndReturn<T extends OmitWhere<UpdateManyAndReturnArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where?: WhereInput<Schema, Model, Options> } & SubsetWithWhere<T, OmitWhere<UpdateManyAndReturnArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>[]>;
});

Expand Down Expand Up @@ -498,8 +503,8 @@ type CommonModelOperations<
* }); // result: `{ _count: { posts: number } }`
* ```
*/
findMany<T extends FindManyArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args?: SelectSubset<T, FindManyArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
findMany<T extends OmitWhere<FindManyArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args?: { where?: WhereInput<Schema, Model, Options> } & SelectSubset<T, FindManyArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>[]>;

/**
Expand All @@ -508,8 +513,8 @@ type CommonModelOperations<
* @returns a single entity or null if not found
* @see {@link findMany}
*/
findUnique<T extends FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: SelectSubset<T, FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
findUnique<T extends OmitWhere<FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where: WhereUniqueInput<Schema, Model, Options> } & SelectSubset<T, FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult> | null>;

/**
Expand All @@ -518,8 +523,8 @@ type CommonModelOperations<
* @returns a single entity
* @see {@link findMany}
*/
findUniqueOrThrow<T extends FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: SelectSubset<T, FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
findUniqueOrThrow<T extends OmitWhere<FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where: WhereUniqueInput<Schema, Model, Options> } & SelectSubset<T, FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>>;

/**
Expand All @@ -528,8 +533,8 @@ type CommonModelOperations<
* @returns a single entity or null if not found
* @see {@link findMany}
*/
findFirst<T extends FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args?: SelectSubset<T, FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
findFirst<T extends OmitWhere<FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args?: { where?: WhereInput<Schema, Model, Options> } & SelectSubset<T, FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult> | null>;

/**
Expand All @@ -538,8 +543,8 @@ type CommonModelOperations<
* @returns a single entity
* @see {@link findMany}
*/
findFirstOrThrow<T extends FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args?: SelectSubset<T, FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
findFirstOrThrow<T extends OmitWhere<FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args?: { where?: WhereInput<Schema, Model, Options> } & SelectSubset<T, FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>>;

/**
Expand Down Expand Up @@ -744,8 +749,8 @@ type CommonModelOperations<
* });
* ```
*/
update<T extends UpdateArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: SelectSubset<T, UpdateArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
update<T extends OmitWhere<UpdateArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where: WhereUniqueInput<Schema, Model, Options> } & SelectSubsetWithWhere<T, OmitWhere<UpdateArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>>;

/**
Expand All @@ -768,8 +773,8 @@ type CommonModelOperations<
* limit: 10
* });
*/
updateMany<T extends UpdateManyArgs<Schema, Model, Options, ExtQueryArgs>>(
args: Subset<T, UpdateManyArgs<Schema, Model, Options, ExtQueryArgs>>,
updateMany<T extends OmitWhere<UpdateManyArgs<Schema, Model, Options, ExtQueryArgs>>>(
args: { where?: WhereInput<Schema, Model, Options> } & SubsetWithWhere<T, OmitWhere<UpdateManyArgs<Schema, Model, Options, ExtQueryArgs>>>,
): ZenStackPromise<Schema, BatchResult>;

/**
Expand All @@ -792,8 +797,8 @@ type CommonModelOperations<
* });
* ```
*/
upsert<T extends UpsertArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: SelectSubset<T, UpsertArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
upsert<T extends OmitWhere<UpsertArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where: WhereUniqueInput<Schema, Model, Options> } & SelectSubsetWithWhere<T, OmitWhere<UpsertArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>>;

/**
Expand All @@ -815,8 +820,8 @@ type CommonModelOperations<
* }); // result: `{ id: string; email: string }`
* ```
*/
delete<T extends DeleteArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: SelectSubset<T, DeleteArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
delete<T extends OmitWhere<DeleteArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where: WhereUniqueInput<Schema, Model, Options> } & SelectSubset<T, DeleteArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>>;

/**
Expand All @@ -838,8 +843,8 @@ type CommonModelOperations<
* });
* ```
*/
deleteMany<T extends DeleteManyArgs<Schema, Model, Options, ExtQueryArgs>>(
args?: Subset<T, DeleteManyArgs<Schema, Model, Options, ExtQueryArgs>>,
deleteMany<T extends OmitWhere<DeleteManyArgs<Schema, Model, Options, ExtQueryArgs>>>(
args?: { where?: WhereInput<Schema, Model, Options> } & Subset<T, DeleteManyArgs<Schema, Model, Options, ExtQueryArgs>>,
): ZenStackPromise<Schema, BatchResult>;

/**
Expand Down
36 changes: 36 additions & 0 deletions packages/orm/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,42 @@ export type SelectSubset<T, U> = {
? 'Please either choose `select` or `omit`.'
: {});

/**
* Strips the `where` field from an args type so the remaining fields can be used as
* the generic type parameter `T` in CRUD methods, allowing `where` to be typed directly
* and benefit from TypeScript's excess property checking.
* @internal
*/
export type OmitWhere<T> = Omit<T, 'where'>;

/**
* Like {@link Subset} but maps the `where` key to `unknown` (instead of `never`) when
* `where` is not present in `U`. This is used in CRUD method signatures where `where`
* is separately typed as `{ where: WhereXxxInput }`: because TypeScript infers T from
* the full argument object (including the `where` field), a naive `Subset<T, OmitWhere<U>>`
* would produce `where: never` in the mapped result, collapsing the `where` type in the
* intersection to `never`. Mapping to `unknown` instead gives
* `{ where: W } & { where: unknown }` = `{ where: W }`, preserving both the correct type
* and TypeScript's excess-property checking on `where`.
* @internal
*/
export type SubsetWithWhere<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : key extends 'where' ? unknown : never;
};

/**
* Like {@link SelectSubset} but maps the `where` key to `unknown` (instead of `never`) when
* `where` is not present in `U`. See {@link SubsetWithWhere} for the rationale.
* @internal
*/
export type SelectSubsetWithWhere<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : key extends 'where' ? unknown : never;
} & (T extends { select: any; include: any }
? 'Please either choose `select` or `include`.'
: T extends { select: any; omit: any }
? 'Please either choose `select` or `omit`.'
: {});

type ToManyRelationFilter<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Expand Down
37 changes: 37 additions & 0 deletions tests/e2e/orm/schemas/delegate/typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,48 @@ async function queryBuilder() {
client.$qb.selectFrom('Video').select(['viewCount']).execute();
}

async function whereEPC() {
// unknown fields in `where` clause should produce a TypeScript error
await client.asset.findMany({
where: {
viewCount: 1,
// @ts-expect-error notExistsColumn is not a valid field
notExistsColumn: 1,
},
});

await client.asset.findFirst({
where: {
viewCount: 1,
// @ts-expect-error notExistsColumn is not a valid field
notExistsColumn: 1,
},
});

// valid fields should not produce errors
await client.asset.findMany({
where: {
viewCount: { gt: 0 },
},
});

// unknown fields in `where` clause for update should also produce TypeScript errors
await client.asset.update({
where: {
id: 1,
// @ts-expect-error notExistsColumn is not a valid field
notExistsColumn: 1,
},
data: { viewCount: 2 },
});
}

async function main() {
await create();
await update();
await find();
await queryBuilder();
await whereEPC();
}

main();
53 changes: 53 additions & 0 deletions tests/e2e/orm/schemas/typing/typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,30 @@ async function find() {
console.log(u.posts[0]?.author?.role);
// @ts-expect-error
console.log(u.posts[0]?.author?.email);

// unknown fields in `where` clause should produce TypeScript errors
await client.user.findMany({
where: {
email: 'test@test.com',
// @ts-expect-error notExistsColumn is not a valid field
notExistsColumn: 1,
},
});

await client.user.findFirst({
where: {
email: 'test@test.com',
// @ts-expect-error notExistsColumn is not a valid field
notExistsColumn: 1,
},
});

// valid where fields should not produce errors
await client.user.findMany({
where: {
email: { contains: '@test.com' },
},
});
}

async function create() {
Expand Down Expand Up @@ -554,6 +578,35 @@ async function update() {
email: 'alex@zenstack.dev',
},
});

// unknown fields in `where` clause should produce TypeScript errors for update/upsert/updateMany
await client.user.update({
where: {
id: 1,
// @ts-expect-error notExistsColumn is not a valid field
notExistsColumn: 1,
},
data: { name: 'Alex' },
});

await client.user.upsert({
where: {
id: 1,
// @ts-expect-error notExistsColumn is not a valid field
notExistsColumn: 1,
},
create: { name: 'Alex', email: 'alex@zenstack.dev' },
update: { name: 'Alex New' },
});

await client.user.updateMany({
where: {
email: 'test@test.com',
// @ts-expect-error notExistsColumn is not a valid field
notExistsColumn: 1,
},
data: { name: 'Alex' },
});
}

async function del() {
Expand Down
Loading