From 17e9e64cfb6b28ce700f2c30c30ef295dae4c826 Mon Sep 17 00:00:00 2001 From: Ahmad Fathallah Date: Sun, 5 Jul 2026 23:10:46 +0300 Subject: [PATCH 01/11] feat: add createTypedAdapter for model-inferred doc types Convex function references use a fixed return type, so adapter findOne/create results stay `any` even when model is a literal. createTypedAdapter wraps the adapter refs and infers Doc from the model argument at the call site. Co-authored-by: Cursor --- src/client/create-typed-adapter.ts | 193 ++++++++++++++++++ src/client/create-typed-adapter.types.test.ts | 63 ++++++ src/client/index.ts | 10 + 3 files changed, 266 insertions(+) create mode 100644 src/client/create-typed-adapter.ts create mode 100644 src/client/create-typed-adapter.types.test.ts diff --git a/src/client/create-typed-adapter.ts b/src/client/create-typed-adapter.ts new file mode 100644 index 00000000..011a8511 --- /dev/null +++ b/src/client/create-typed-adapter.ts @@ -0,0 +1,193 @@ +import type { + DataModelFromSchemaDefinition, + DocumentByName, + FunctionReference, + PaginationOptions, + PaginationResult, + SchemaDefinition, + TableNamesInDataModel, +} from "convex/server"; +import type { Infer } from "convex/values"; +import type { adapterWhereValidator } from "./adapter-utils.js"; + +export type AdapterWhere = Infer; + +/** Function references for the adapter API created by {@link createApi}. */ +export type AdapterFunctions = { + findOne: FunctionReference<"query", "public" | "internal", any, any>; + findMany: FunctionReference<"query", "public" | "internal", any, any>; + create: FunctionReference<"mutation", "public" | "internal", any, any>; + updateOne: FunctionReference<"mutation", "public" | "internal", any, any>; + updateMany: FunctionReference<"mutation", "public" | "internal", any, any>; + deleteOne: FunctionReference<"mutation", "public" | "internal", any, any>; + deleteMany: FunctionReference<"mutation", "public" | "internal", any, any>; +}; + +type SchemaTables> = + TableNamesInDataModel>; + +type AdapterDoc< + S extends SchemaDefinition, + T extends SchemaTables, +> = DocumentByName, T>; + +type AdapterInsert< + S extends SchemaDefinition, + T extends SchemaTables, +> = Omit, "_id" | "_creationTime">; + +type AdapterUpdate< + S extends SchemaDefinition, + T extends SchemaTables, +> = Partial>; + +type QueryRunner = { + runQuery: >( + query: Q, + args: Q["_args"] + ) => Promise; +}; + +type MutationRunner = { + runMutation: >( + mutation: M, + args: M["_args"] + ) => Promise; +}; + +type SortBy = { direction: "asc" | "desc"; field: string }; + +type BatchResult = { + count: number; + ids: Array; + isDone: boolean; + continueCursor: string; + splitCursor?: string | null; + pageStatus?: "SplitRecommended" | "SplitRequired" | null; +}; + +/** + * Typed wrappers around the Better Auth adapter API. + * + * Convex function references have a fixed return type, so `model` on the args + * does not narrow `findOne` / `create` results. These helpers infer the doc + * type from the `model` literal at the call site. + * + * @example + * ```ts + * import schema from "./betterAuth/schema"; + * import { createTypedAdapter } from "@convex-dev/better-auth"; + * import { components } from "./_generated/api"; + * + * export const adapter = createTypedAdapter(schema, components.betterAuth.adapter); + * + * const user = await adapter.findOne(ctx, { + * model: "user", + * where: [{ field: "email", value: email }], + * }); + * // user: Doc<"user"> | null + * ``` + */ +export function createTypedAdapter>( + _schema: S, + fns: AdapterFunctions +) { + return { + findOne>( + ctx: QueryRunner, + args: { + model: T; + where?: AdapterWhere[]; + select?: string[]; + join?: unknown; + } + ): Promise | null> { + return ctx.runQuery(fns.findOne, args) as Promise | null>; + }, + + findMany>( + ctx: QueryRunner, + args: { + model: T; + where?: AdapterWhere[]; + select?: string[]; + limit?: number; + sortBy?: SortBy; + offset?: number; + join?: unknown; + paginationOpts: PaginationOptions; + } + ): Promise>> { + return ctx.runQuery(fns.findMany, args) as Promise< + PaginationResult> + >; + }, + + create>( + ctx: MutationRunner, + args: { + input: { model: T; data: AdapterInsert }; + select?: string[]; + onCreateHandle?: string; + } + ): Promise> { + return ctx.runMutation(fns.create, args) as Promise>; + }, + + updateOne>( + ctx: MutationRunner, + args: { + input: { + model: T; + update: AdapterUpdate; + where?: AdapterWhere[]; + }; + onUpdateHandle?: string; + } + ): Promise> { + return ctx.runMutation(fns.updateOne, args) as Promise>; + }, + + updateMany>( + ctx: MutationRunner, + args: { + input: { + model: T; + update: AdapterUpdate; + where?: AdapterWhere[]; + }; + paginationOpts: PaginationOptions; + onUpdateHandle?: string; + } + ): Promise { + return ctx.runMutation(fns.updateMany, args) as Promise; + }, + + deleteOne>( + ctx: MutationRunner, + args: { + input: { model: T; where?: AdapterWhere[] }; + onDeleteHandle?: string; + } + ): Promise | undefined> { + return ctx.runMutation(fns.deleteOne, args) as Promise< + AdapterDoc | undefined + >; + }, + + deleteMany>( + ctx: MutationRunner, + args: { + input: { model: T; where?: AdapterWhere[] }; + paginationOpts: PaginationOptions; + onDeleteHandle?: string; + } + ): Promise { + return ctx.runMutation(fns.deleteMany, args) as Promise; + }, + }; +} + +export type TypedAdapter> = ReturnType< + typeof createTypedAdapter +>; diff --git a/src/client/create-typed-adapter.types.test.ts b/src/client/create-typed-adapter.types.test.ts new file mode 100644 index 00000000..ab2fdee9 --- /dev/null +++ b/src/client/create-typed-adapter.types.test.ts @@ -0,0 +1,63 @@ +import { expectTypeOf, it } from "vitest"; +import { createTypedAdapter } from "./create-typed-adapter.js"; +import type { Doc } from "../component/_generated/dataModel.js"; +import schema from "../component/schema.js"; + +const fns = { + findOne: "adapter:findOne", + findMany: "adapter:findMany", + create: "adapter:create", + updateOne: "adapter:updateOne", + updateMany: "adapter:updateMany", + deleteOne: "adapter:deleteOne", + deleteMany: "adapter:deleteMany", +} as Parameters[1]; + +const adapter = createTypedAdapter(schema, fns); + +const queryCtx = { + runQuery: async () => + ({ page: [] as never, isDone: true as const, continueCursor: "" }) as never, +}; + +const mutationCtx = { + runMutation: async () => ({}) as never, +}; + +it("infers doc type from model on findOne", async () => { + const user = await adapter.findOne(queryCtx, { + model: "user", + where: [{ field: "email", value: "a@b.c" }], + }); + expectTypeOf(user).toEqualTypeOf | null>(); + + const member = await adapter.findOne(queryCtx, { + model: "member", + where: [{ field: "userId", value: "u1" }], + }); + expectTypeOf(member).toEqualTypeOf | null>(); +}); + +it("infers doc type from model on create", async () => { + const created = await adapter.create(mutationCtx, { + input: { + model: "session", + data: { + expiresAt: 0, + token: "t", + createdAt: 0, + updatedAt: 0, + userId: "u" as Doc<"user">["_id"], + }, + }, + }); + expectTypeOf(created).toEqualTypeOf>(); +}); + +it("infers page doc type from model on findMany", async () => { + const result = await adapter.findMany(queryCtx, { + model: "invitation", + paginationOpts: { cursor: null, numItems: 10 }, + }); + expectTypeOf(result.page).toEqualTypeOf>>(); +}); diff --git a/src/client/index.ts b/src/client/index.ts index 032fa410..516e1964 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -4,7 +4,13 @@ import semverLt from "semver/functions/lt.js"; import { createClient } from "./create-client.js"; import type { AuthFunctions, Triggers } from "./create-client.js"; import { createApi } from "./create-api.js"; +import { createTypedAdapter } from "./create-typed-adapter.js"; import type { CreateAuth, EventFunction, GenericCtx } from "../utils/index.js"; +import type { + AdapterFunctions, + AdapterWhere, + TypedAdapter, +} from "./create-typed-adapter.js"; if (semverLt(convexVersion, "1.25.0")) { throw new Error("Convex version must be at least 1.25.0"); @@ -14,9 +20,13 @@ export { convexAdapter, createClient, createApi, + createTypedAdapter, + type AdapterFunctions, + type AdapterWhere, type CreateAuth, type EventFunction, type GenericCtx, type Triggers, type AuthFunctions, + type TypedAdapter, }; From 3b097294ae5b1f58564c1c4c59bfa63cbf6956f4 Mon Sep 17 00:00:00 2001 From: FatahChan Date: Sun, 5 Jul 2026 23:15:35 +0300 Subject: [PATCH 02/11] fix: use makeFunctionReference in typed adapter type tests Stub adapter fns with real FunctionReference values so tsc accepts the test file, and align model literals with the current schema. Co-authored-by: Cursor --- src/client/create-typed-adapter.types.test.ts | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/client/create-typed-adapter.types.test.ts b/src/client/create-typed-adapter.types.test.ts index ab2fdee9..73f20b5a 100644 --- a/src/client/create-typed-adapter.types.test.ts +++ b/src/client/create-typed-adapter.types.test.ts @@ -1,17 +1,19 @@ +import { makeFunctionReference } from "convex/server"; import { expectTypeOf, it } from "vitest"; -import { createTypedAdapter } from "./create-typed-adapter.js"; +import { createTypedAdapter } from './create-typed-adapter.js'; +import type { AdapterFunctions } from './create-typed-adapter.js'; import type { Doc } from "../component/_generated/dataModel.js"; import schema from "../component/schema.js"; const fns = { - findOne: "adapter:findOne", - findMany: "adapter:findMany", - create: "adapter:create", - updateOne: "adapter:updateOne", - updateMany: "adapter:updateMany", - deleteOne: "adapter:deleteOne", - deleteMany: "adapter:deleteMany", -} as Parameters[1]; + findOne: makeFunctionReference<"query">("adapter:findOne"), + findMany: makeFunctionReference<"query">("adapter:findMany"), + create: makeFunctionReference<"mutation">("adapter:create"), + updateOne: makeFunctionReference<"mutation">("adapter:updateOne"), + updateMany: makeFunctionReference<"mutation">("adapter:updateMany"), + deleteOne: makeFunctionReference<"mutation">("adapter:deleteOne"), + deleteMany: makeFunctionReference<"mutation">("adapter:deleteMany"), +} satisfies AdapterFunctions; const adapter = createTypedAdapter(schema, fns); @@ -31,11 +33,11 @@ it("infers doc type from model on findOne", async () => { }); expectTypeOf(user).toEqualTypeOf | null>(); - const member = await adapter.findOne(queryCtx, { - model: "member", + const session = await adapter.findOne(queryCtx, { + model: "session", where: [{ field: "userId", value: "u1" }], }); - expectTypeOf(member).toEqualTypeOf | null>(); + expectTypeOf(session).toEqualTypeOf | null>(); }); it("infers doc type from model on create", async () => { @@ -56,8 +58,8 @@ it("infers doc type from model on create", async () => { it("infers page doc type from model on findMany", async () => { const result = await adapter.findMany(queryCtx, { - model: "invitation", + model: "account", paginationOpts: { cursor: null, numItems: 10 }, }); - expectTypeOf(result.page).toEqualTypeOf>>(); + expectTypeOf(result.page).toEqualTypeOf>>(); }); From 5b87f72b3af150a3fa82f27162d30d95babba21f Mon Sep 17 00:00:00 2001 From: FatahChan Date: Sun, 5 Jul 2026 23:17:57 +0300 Subject: [PATCH 03/11] refactor: update type annotations in createTypedAdapter to use Array syntax Changed type annotations for 'where' and 'select' parameters in createTypedAdapter to use Array syntax for consistency and clarity. --- src/client/create-typed-adapter.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client/create-typed-adapter.ts b/src/client/create-typed-adapter.ts index 011a8511..a7caa3d5 100644 --- a/src/client/create-typed-adapter.ts +++ b/src/client/create-typed-adapter.ts @@ -97,8 +97,8 @@ export function createTypedAdapter>( ctx: QueryRunner, args: { model: T; - where?: AdapterWhere[]; - select?: string[]; + where?: Array; + select?: Array; join?: unknown; } ): Promise | null> { @@ -109,8 +109,8 @@ export function createTypedAdapter>( ctx: QueryRunner, args: { model: T; - where?: AdapterWhere[]; - select?: string[]; + where?: Array; + select?: Array; limit?: number; sortBy?: SortBy; offset?: number; @@ -127,7 +127,7 @@ export function createTypedAdapter>( ctx: MutationRunner, args: { input: { model: T; data: AdapterInsert }; - select?: string[]; + select?: Array; onCreateHandle?: string; } ): Promise> { @@ -140,7 +140,7 @@ export function createTypedAdapter>( input: { model: T; update: AdapterUpdate; - where?: AdapterWhere[]; + where?: Array; }; onUpdateHandle?: string; } @@ -154,7 +154,7 @@ export function createTypedAdapter>( input: { model: T; update: AdapterUpdate; - where?: AdapterWhere[]; + where?: Array; }; paginationOpts: PaginationOptions; onUpdateHandle?: string; @@ -166,7 +166,7 @@ export function createTypedAdapter>( deleteOne>( ctx: MutationRunner, args: { - input: { model: T; where?: AdapterWhere[] }; + input: { model: T; where?: Array }; onDeleteHandle?: string; } ): Promise | undefined> { @@ -178,7 +178,7 @@ export function createTypedAdapter>( deleteMany>( ctx: MutationRunner, args: { - input: { model: T; where?: AdapterWhere[] }; + input: { model: T; where?: Array }; paginationOpts: PaginationOptions; onDeleteHandle?: string; } From 563eef232a735626bcd412a8caef39272c3bc21e Mon Sep 17 00:00:00 2001 From: FatahChan Date: Sun, 5 Jul 2026 23:23:57 +0300 Subject: [PATCH 04/11] fix: export typed adapter public types and enable type tests Export SortBy and BatchResult for consumers, include *.types.test.ts in Vitest typecheck, and allow generated Convex files in test tsconfig. Co-authored-by: Cursor --- src/client/create-typed-adapter.ts | 4 ++-- src/client/index.ts | 4 ++++ tsconfig.test.json | 2 +- vitest.config.ts | 4 ++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/client/create-typed-adapter.ts b/src/client/create-typed-adapter.ts index a7caa3d5..b736120c 100644 --- a/src/client/create-typed-adapter.ts +++ b/src/client/create-typed-adapter.ts @@ -55,9 +55,9 @@ type MutationRunner = { ) => Promise; }; -type SortBy = { direction: "asc" | "desc"; field: string }; +export type SortBy = { direction: "asc" | "desc"; field: string }; -type BatchResult = { +export type BatchResult = { count: number; ids: Array; isDone: boolean; diff --git a/src/client/index.ts b/src/client/index.ts index 516e1964..27b3b7ff 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -9,6 +9,8 @@ import type { CreateAuth, EventFunction, GenericCtx } from "../utils/index.js"; import type { AdapterFunctions, AdapterWhere, + BatchResult, + SortBy, TypedAdapter, } from "./create-typed-adapter.js"; @@ -23,6 +25,8 @@ export { createTypedAdapter, type AdapterFunctions, type AdapterWhere, + type BatchResult, + type SortBy, type CreateAuth, type EventFunction, type GenericCtx, diff --git a/tsconfig.test.json b/tsconfig.test.json index aff52303..a64ce3b9 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -7,5 +7,5 @@ "example/src/**/*.tsx", "example/convex/**/*.ts" ], - "exclude": ["node_modules", "dist", "**/_generated"] + "exclude": ["node_modules", "dist"] } diff --git a/vitest.config.ts b/vitest.config.ts index 0c4712f4..acd974a4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,6 +5,10 @@ export default defineConfig({ environment: "edge-runtime", typecheck: { tsconfig: "./tsconfig.test.json", + include: [ + "**/*.{test,spec}-d.?(c|m)[jt]s?(x)", + "**/*.types.test.ts", + ], }, exclude: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/e2e/**", "**/examples/**"], }, From bba2648d02bd18849ca68d48edd44b0abd3bc829 Mon Sep 17 00:00:00 2001 From: FatahChan Date: Sun, 5 Jul 2026 23:32:14 +0300 Subject: [PATCH 05/11] feat: expose typedAdapter wrapper on createClient and createApi Wire createTypedAdapter into the component client and local-install API so adapter calls infer doc types from model, and document the pattern. Co-authored-by: Cursor --- docs/content/docs/api/component-client.mdx | 26 +++++++ docs/content/docs/api/type-utilities.mdx | 76 ++++++++++++++++++++ docs/content/docs/features/local-install.mdx | 37 +++++++++- src/client/create-api.ts | 7 +- src/client/create-client.ts | 36 +++++++--- src/component/adapter.ts | 6 +- 6 files changed, 175 insertions(+), 13 deletions(-) diff --git a/docs/content/docs/api/component-client.mdx b/docs/content/docs/api/component-client.mdx index 941923ad..3f8f125a 100644 --- a/docs/content/docs/api/component-client.mdx +++ b/docs/content/docs/api/component-client.mdx @@ -25,6 +25,32 @@ export const createAuth = (ctx: GenericCtx) => { }); ``` +## `typedAdapter` + +Typed wrappers around `components.betterAuth.adapter` for use in your Convex +functions. Prefer this over calling +`ctx.runQuery(components.betterAuth.adapter.findOne, …)` directly — the +`model` argument narrows the return type. + +With [local install](/features/local-install), pass your schema in `createClient` +config. See [Type Utilities](/api/type-utilities#typedadapter-on-the-component-client). + +```ts +import { query } from "./_generated/server"; +import { v } from "convex/values"; +import { authComponent } from "./auth"; + +export const getUserByEmail = query({ + args: { email: v.string() }, + handler: async (ctx, args) => { + return await authComponent.typedAdapter.findOne(ctx, { + model: "user", + where: [{ field: "email", value: args.email }], + }); + }, +}); +``` + ## `getAuth()` Better Auth [API endpoints](https://www.better-auth.com/docs/concepts/api) can diff --git a/docs/content/docs/api/type-utilities.mdx b/docs/content/docs/api/type-utilities.mdx index a540e65a..e349e0c3 100644 --- a/docs/content/docs/api/type-utilities.mdx +++ b/docs/content/docs/api/type-utilities.mdx @@ -3,6 +3,82 @@ title: Type Utilities description: Type utilities for Better Auth --- +## `typedAdapter` on the component client + +The recommended wrapper for typed adapter calls. `createClient` exposes +`typedAdapter` so you don't call +`ctx.runQuery(components.betterAuth.adapter.findOne, …)` directly — use +`authComponent.typedAdapter.findOne(ctx, …)` instead and the return type is +inferred from `model`. + +With [local install](/features/local-install), pass your schema in +`createClient` config so types match your tables (including plugin tables): + +```ts title="convex/auth.ts" +import { createClient } from "@convex-dev/better-auth"; +import authSchema from "./betterAuth/schema"; +import { components } from "./_generated/api"; +import { type DataModel } from "./_generated/dataModel"; + +export const authComponent = createClient( + components.betterAuth, + { local: { schema: authSchema } } +); +``` + +```ts title="convex/users.ts" +import { query } from "./_generated/server"; +import { v } from "convex/values"; +import { authComponent } from "./auth"; + +export const getUserByEmail = query({ + args: { email: v.string() }, + handler: async (ctx, args) => { + return await authComponent.typedAdapter.findOne(ctx, { + model: "user", + where: [{ field: "email", value: args.email }], + }); + // Doc<"user"> | null + }, +}); +``` + +For local install, `createApi` also returns a `typed` property alongside the +Convex function exports: + +```ts title="convex/betterAuth/adapter.ts" +const api = createApi(schema, createAuthOptions); +export const { create, findOne, findMany, updateOne, updateMany, deleteOne, deleteMany } = api; +export const typed = api.typed; +``` + +## `createTypedAdapter()` + +Lower-level helper when you need to build a typed wrapper yourself (same +methods as `typedAdapter`). `createClient` and `createApi` use this internally. + +```ts +import { createTypedAdapter } from "@convex-dev/better-auth"; +import schema from "./betterAuth/schema"; +import { components } from "./_generated/api"; + +export const adapter = createTypedAdapter(schema, components.betterAuth.adapter); +``` + +The adapter exposes the same operations as +[`createApi`](/features/local-install#export-adapter-functions): `findOne`, +`findMany`, `create`, `updateOne`, `updateMany`, `deleteOne`, and `deleteMany`. + +### Exported types + +| Type | Description | +| --- | --- | +| `TypedAdapter` | Return type of `createTypedAdapter` for schema `S` | +| `AdapterFunctions` | Function references passed as the second argument | +| `AdapterWhere` | Where-clause shape used by read and write methods | +| `SortBy` | `{ direction: "asc" \| "desc"; field: string }` for `findMany` | +| `BatchResult` | Paginated batch result from `updateMany` and `deleteMany` | + ## `requireRunMutationCtx()` A type guard to ensure a given context object has a `runMutation` property. diff --git a/docs/content/docs/features/local-install.mdx b/docs/content/docs/features/local-install.mdx index db3ecc5f..4e219cd1 100644 --- a/docs/content/docs/features/local-install.mdx +++ b/docs/content/docs/features/local-install.mdx @@ -113,6 +113,8 @@ installed component. import schema from "./schema"; import { createAuthOptions } from "../auth"; + const api = createApi(schema, createAuthOptions); + export const { create, findOne, @@ -121,7 +123,10 @@ installed component. updateMany, deleteOne, deleteMany, - } = createApi(schema, createAuthOptions); + } = api; + + // Typed wrapper for app-side adapter calls (see Usage below) + export const typed = api.typed; ``` @@ -266,6 +271,36 @@ internet, even if they are public**. type won't be inferred unless a return validator is provided. +#### Querying with the typed adapter + +Don't call `ctx.runQuery(components.betterAuth.adapter.findOne, …)` directly +from app code — return types won't narrow by `model`. Use +`authComponent.typedAdapter` instead (requires your local schema in +`createClient` config): + +```ts title="convex/users.ts" +import { query } from "./_generated/server"; +import { v } from "convex/values"; +import { authComponent } from "./auth"; + +export const getUserByEmail = query({ + args: { email: v.string() }, + handler: async (ctx, args) => { + return await authComponent.typedAdapter.findOne(ctx, { + model: "user", + where: [{ field: "email", value: args.email }], + }); + // Doc<"user"> | null — inferred from model + }, +}); +``` + +See [`typedAdapter`](/api/type-utilities#typedadapter-on-the-component-client) for +setup and [`createTypedAdapter`](/api/type-utilities#createtypedadapter) for the +lower-level API. + +#### Custom component functions + ```ts title="convex/betterAuth/someFile.ts" import { query, mutation } from "./_generated/server"; import { doc } from "convex-helpers/validators"; diff --git a/src/client/create-api.ts b/src/client/create-api.ts index 97679db6..19206838 100644 --- a/src/client/create-api.ts +++ b/src/client/create-api.ts @@ -16,6 +16,7 @@ import { paginate, selectFields, } from "./adapter-utils.js"; +import { createTypedAdapter, type AdapterFunctions } from "./create-typed-adapter.js"; import { getAuthTables } from "better-auth/db"; import type { TableNames } from "../component/_generated/dataModel.js"; import type { BetterAuthOptions } from "better-auth/minimal"; @@ -63,7 +64,7 @@ export const createApi = >( createAuthOptions: (ctx: any) => BetterAuthOptions ) => { const betterAuthSchema = getAuthTables(createAuthOptions({} as any)); - return { + const api = { create: mutationGeneric({ args: { input: v.union( @@ -366,4 +367,8 @@ export const createApi = >( }, }), }; + return { + ...api, + typed: createTypedAdapter(schema, api as unknown as AdapterFunctions), + }; }; diff --git a/src/client/create-client.ts b/src/client/create-client.ts index 9b4c3f77..fd269500 100644 --- a/src/client/create-client.ts +++ b/src/client/create-client.ts @@ -4,7 +4,6 @@ import { queryGeneric, } from "convex/server"; import type { - DataModelFromSchemaDefinition, FunctionReference, GenericDataModel, GenericMutationCtx, @@ -15,8 +14,13 @@ import type { import { ConvexError, v } from "convex/values"; import type { Infer } from "convex/values"; import { convexAdapter } from "./adapter.js"; +import { + createTypedAdapter, + type AdapterFunctions, + type TypedAdapter, +} from "./create-typed-adapter.js"; import { corsRouter } from "convex-helpers/server/cors"; -import type defaultSchema from "../component/schema.js"; +import defaultSchema from "../component/schema.js"; import type { CreateAuth, GenericCtx } from "./index.js"; import type { TrustedOriginsOption } from "../utils/index.js"; @@ -139,14 +143,18 @@ export const createClient = < | { triggers?: undefined } ) ) => { - type BetterAuthDataModel = DataModelFromSchemaDefinition; + const adapterSchema = config?.local?.schema ?? defaultSchema; + const typedAdapter: TypedAdapter = createTypedAdapter( + adapterSchema, + component.adapter as AdapterFunctions + ); const safeGetAuthUser = async (ctx: GenericCtx) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { return; } - const session = (await ctx.runQuery(component.adapter.findOne, { + const session = await typedAdapter.findOne(ctx, { model: "session", where: [ { @@ -159,13 +167,13 @@ export const createClient = < value: new Date().getTime(), }, ], - })) as BetterAuthDataModel["session"]["document"] | null; + }); if (!session) { return; } - const doc = (await ctx.runQuery(component.adapter.findOne, { + const doc = await typedAdapter.findOne(ctx, { model: "user", where: [ { @@ -173,7 +181,7 @@ export const createClient = < value: identity.subject, }, ], - })) as BetterAuthDataModel["user"]["document"] | null; + }); if (!doc) { return; } @@ -194,7 +202,7 @@ export const createClient = < return new Headers(); } // Don't validate the session here, let Better Auth handle that - const session = await ctx.runQuery(component.adapter.findOne, { + const session = await typedAdapter.findOne(ctx, { model: "session", where: [ { @@ -223,6 +231,14 @@ export const createClient = < debugLogs: config?.verbose, }), + /** + * Typed wrappers around `components.betterAuth.adapter` queries and + * mutations. Use this instead of calling + * `ctx.runQuery(components.betterAuth.adapter.findOne, …)` directly when + * you need document types inferred from the `model` argument. + */ + typedAdapter, + /** * Returns the Better Auth auth object and headers for using Better Auth API * methods directly in a Convex mutation or query. Convex functions don't @@ -275,10 +291,10 @@ export const createClient = < * @returns The user or null if the user is not found */ getAnyUserById: async (ctx: GenericCtx, id: string) => { - return (await ctx.runQuery(component.adapter.findOne, { + return await typedAdapter.findOne(ctx, { model: "user", where: [{ field: "_id", value: id }], - })) as BetterAuthDataModel["user"]["document"] | null; + }); }, /** diff --git a/src/component/adapter.ts b/src/component/adapter.ts index 41ee5cbc..2f6fcd9f 100644 --- a/src/component/adapter.ts +++ b/src/component/adapter.ts @@ -2,6 +2,8 @@ import { createApi } from "../client/index.js"; import { options } from "../auth-options.js"; import schema from "./schema.js"; +const api = createApi(schema, () => options); + export const { create, findOne, @@ -10,4 +12,6 @@ export const { updateMany, deleteOne, deleteMany, -} = createApi(schema, () => options); +} = api; + +export const typed = api.typed; From 8f1cb3eca2f9c8b9d095d22dd3096c0824bca91e Mon Sep 17 00:00:00 2001 From: FatahChan Date: Sun, 5 Jul 2026 23:41:41 +0300 Subject: [PATCH 06/11] docs: drop unused createApi.typed and document typedAdapter usage Remove the unused typed export from createApi and add a full local-install example for authComponent.typedAdapter as the single typed entry point. Co-authored-by: Cursor --- docs/content/docs/api/type-utilities.mdx | 11 +-- docs/content/docs/features/local-install.mdx | 74 ++++++++++++-------- src/client/create-api.ts | 7 +- src/component/adapter.ts | 6 +- 4 files changed, 49 insertions(+), 49 deletions(-) diff --git a/docs/content/docs/api/type-utilities.mdx b/docs/content/docs/api/type-utilities.mdx index e349e0c3..15823bb9 100644 --- a/docs/content/docs/api/type-utilities.mdx +++ b/docs/content/docs/api/type-utilities.mdx @@ -43,19 +43,10 @@ export const getUserByEmail = query({ }); ``` -For local install, `createApi` also returns a `typed` property alongside the -Convex function exports: - -```ts title="convex/betterAuth/adapter.ts" -const api = createApi(schema, createAuthOptions); -export const { create, findOne, findMany, updateOne, updateMany, deleteOne, deleteMany } = api; -export const typed = api.typed; -``` - ## `createTypedAdapter()` Lower-level helper when you need to build a typed wrapper yourself (same -methods as `typedAdapter`). `createClient` and `createApi` use this internally. +methods as `typedAdapter`). `createClient` uses this internally. ```ts import { createTypedAdapter } from "@convex-dev/better-auth"; diff --git a/docs/content/docs/features/local-install.mdx b/docs/content/docs/features/local-install.mdx index 4e219cd1..7019d0a0 100644 --- a/docs/content/docs/features/local-install.mdx +++ b/docs/content/docs/features/local-install.mdx @@ -113,8 +113,6 @@ installed component. import schema from "./schema"; import { createAuthOptions } from "../auth"; - const api = createApi(schema, createAuthOptions); - export const { create, findOne, @@ -123,10 +121,7 @@ installed component. updateMany, deleteOne, deleteMany, - } = api; - - // Typed wrapper for app-side adapter calls (see Usage below) - export const typed = api.typed; + } = createApi(schema, createAuthOptions); ``` @@ -254,29 +249,35 @@ custom indexes aren't overwritten when the schema is regenerated. -### Accessing component data +### Querying auth tables from app code -Convex functions within your Better Auth component directory can access the -component's tables directly, and can then be run from outside the component via -`ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction`. - -Note that if an internal function is defined in a component, it will not be -accessible from outside the component, so functions that need to run outside the -component cannot be internal. While normally public functions are exposed to the -internet, **Convex functions exported by a component are never exposed to the -internet, even if they are public**. +When you read or write Better Auth tables from your app (outside the component), +use `authComponent.typedAdapter`. It wraps the adapter the same way as +`ctx.runQuery(components.betterAuth.adapter.findOne, …)` but infers the document +type from the `model` you pass. - - If a function in a component is called from outside the component, the return - type won't be inferred unless a return validator is provided. - +**1. Pass your local schema to `createClient`** (from the install step above): -#### Querying with the typed adapter +```ts title="convex/auth.ts" +import { createClient } from "@convex-dev/better-auth"; +import authSchema from "./betterAuth/schema"; +import { components } from "./_generated/api"; +import { type DataModel } from "./_generated/dataModel"; + +export const authComponent = createClient( + components.betterAuth, + { local: { schema: authSchema } } +); + +export const createAuth = (ctx: GenericCtx) => { + return betterAuth({ + database: authComponent.adapter(ctx), + // ... rest of auth config + }); +}; +``` -Don't call `ctx.runQuery(components.betterAuth.adapter.findOne, …)` directly -from app code — return types won't narrow by `model`. Use -`authComponent.typedAdapter` instead (requires your local schema in -`createClient` config): +**2. Call `typedAdapter` from your queries and mutations:** ```ts title="convex/users.ts" import { query } from "./_generated/server"; @@ -295,9 +296,26 @@ export const getUserByEmail = query({ }); ``` -See [`typedAdapter`](/api/type-utilities#typedadapter-on-the-component-client) for -setup and [`createTypedAdapter`](/api/type-utilities#createtypedadapter) for the -lower-level API. +`typedAdapter` exposes the same methods as the adapter API: `findOne`, `findMany`, +`create`, `updateOne`, `updateMany`, `deleteOne`, and `deleteMany`. See +[Type Utilities](/api/type-utilities#typedadapter-on-the-component-client). + +### Accessing component data + +Convex functions within your Better Auth component directory can access the +component's tables directly, and can then be run from outside the component via +`ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction`. + +Note that if an internal function is defined in a component, it will not be +accessible from outside the component, so functions that need to run outside the +component cannot be internal. While normally public functions are exposed to the +internet, **Convex functions exported by a component are never exposed to the +internet, even if they are public**. + + + If a function in a component is called from outside the component, the return + type won't be inferred unless a return validator is provided. + #### Custom component functions diff --git a/src/client/create-api.ts b/src/client/create-api.ts index 19206838..97679db6 100644 --- a/src/client/create-api.ts +++ b/src/client/create-api.ts @@ -16,7 +16,6 @@ import { paginate, selectFields, } from "./adapter-utils.js"; -import { createTypedAdapter, type AdapterFunctions } from "./create-typed-adapter.js"; import { getAuthTables } from "better-auth/db"; import type { TableNames } from "../component/_generated/dataModel.js"; import type { BetterAuthOptions } from "better-auth/minimal"; @@ -64,7 +63,7 @@ export const createApi = >( createAuthOptions: (ctx: any) => BetterAuthOptions ) => { const betterAuthSchema = getAuthTables(createAuthOptions({} as any)); - const api = { + return { create: mutationGeneric({ args: { input: v.union( @@ -367,8 +366,4 @@ export const createApi = >( }, }), }; - return { - ...api, - typed: createTypedAdapter(schema, api as unknown as AdapterFunctions), - }; }; diff --git a/src/component/adapter.ts b/src/component/adapter.ts index 2f6fcd9f..41ee5cbc 100644 --- a/src/component/adapter.ts +++ b/src/component/adapter.ts @@ -2,8 +2,6 @@ import { createApi } from "../client/index.js"; import { options } from "../auth-options.js"; import schema from "./schema.js"; -const api = createApi(schema, () => options); - export const { create, findOne, @@ -12,6 +10,4 @@ export const { updateMany, deleteOne, deleteMany, -} = api; - -export const typed = api.typed; +} = createApi(schema, () => options); From 68624b78b141ebba1daf8b9e2e7a1e15e5f0cee5 Mon Sep 17 00:00:00 2001 From: FatahChan Date: Sun, 5 Jul 2026 23:49:30 +0300 Subject: [PATCH 07/11] fix: restore _generated exclude in test tsconfig Keep generated files out of the test project root while still allowing type tests to import component _generated types via composite: false. Co-authored-by: Cursor --- tsconfig.test.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tsconfig.test.json b/tsconfig.test.json index a64ce3b9..d9f68fdb 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,5 +1,8 @@ { "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false + }, "include": [ "src/**/*.ts", "src/**/*.tsx", @@ -7,5 +10,5 @@ "example/src/**/*.tsx", "example/convex/**/*.ts" ], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "**/_generated"] } From cad1465d1595005afd6a68a78e13e18478f0fcbd Mon Sep 17 00:00:00 2001 From: FatahChan Date: Mon, 6 Jul 2026 02:10:34 +0300 Subject: [PATCH 08/11] feat: add typed-adapter entry in package.json for type definitions and default exports --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index cdbd412d..79764dca 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,10 @@ "./utils": { "types": "./dist/utils/index.d.ts", "default": "./dist/utils/index.js" + }, + "./typed-adapter": { + "types": "./dist/client/create-typed-adapter.d.ts", + "default": "./dist/client/create-typed-adapter.js" } }, "typesVersions": { From 8dda29b35430547c7a3f1b9cf93cf4cffc3d69be Mon Sep 17 00:00:00 2001 From: FatahChan Date: Mon, 6 Jul 2026 10:38:17 +0300 Subject: [PATCH 09/11] refactor: streamline imports in create-client.ts for clarity and consistency --- src/client/create-client.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/client/create-client.ts b/src/client/create-client.ts index fd269500..a1a19cbb 100644 --- a/src/client/create-client.ts +++ b/src/client/create-client.ts @@ -14,11 +14,8 @@ import type { import { ConvexError, v } from "convex/values"; import type { Infer } from "convex/values"; import { convexAdapter } from "./adapter.js"; -import { - createTypedAdapter, - type AdapterFunctions, - type TypedAdapter, -} from "./create-typed-adapter.js"; +import { createTypedAdapter } from "./create-typed-adapter.js"; +import type { AdapterFunctions, TypedAdapter } from "./create-typed-adapter.js"; import { corsRouter } from "convex-helpers/server/cors"; import defaultSchema from "../component/schema.js"; import type { CreateAuth, GenericCtx } from "./index.js"; From 6c471a85812de9cf4512876a57c8c7fd7df839c8 Mon Sep 17 00:00:00 2001 From: FatahChan Date: Mon, 6 Jul 2026 10:55:35 +0300 Subject: [PATCH 10/11] feat: make AdapterWhere model-aware in typed adapter Narrow where-clause field and value types from the model argument, with a fallback to the validator shape when the schema type is still generic. Co-authored-by: Cursor --- docs/content/docs/api/type-utilities.mdx | 2 +- src/client/create-client.ts | 4 +- src/client/create-typed-adapter.ts | 45 ++++++++++++++++--- src/client/create-typed-adapter.types.test.ts | 19 ++++++++ 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/docs/content/docs/api/type-utilities.mdx b/docs/content/docs/api/type-utilities.mdx index 15823bb9..e829f974 100644 --- a/docs/content/docs/api/type-utilities.mdx +++ b/docs/content/docs/api/type-utilities.mdx @@ -66,7 +66,7 @@ The adapter exposes the same operations as | --- | --- | | `TypedAdapter` | Return type of `createTypedAdapter` for schema `S` | | `AdapterFunctions` | Function references passed as the second argument | -| `AdapterWhere` | Where-clause shape used by read and write methods | +| `AdapterWhere` | Where-clause for table `T` in schema `S`; `field` and `value` narrow with the model | | `SortBy` | `{ direction: "asc" \| "desc"; field: string }` for `findMany` | | `BatchResult` | Paginated batch result from `updateMany` and `deleteMany` | diff --git a/src/client/create-client.ts b/src/client/create-client.ts index a1a19cbb..c591a63f 100644 --- a/src/client/create-client.ts +++ b/src/client/create-client.ts @@ -141,10 +141,10 @@ export const createClient = < ) ) => { const adapterSchema = config?.local?.schema ?? defaultSchema; - const typedAdapter: TypedAdapter = createTypedAdapter( + const typedAdapter = createTypedAdapter( adapterSchema, component.adapter as AdapterFunctions - ); + ) as TypedAdapter; const safeGetAuthUser = async (ctx: GenericCtx) => { const identity = await ctx.auth.getUserIdentity(); diff --git a/src/client/create-typed-adapter.ts b/src/client/create-typed-adapter.ts index b736120c..9424dd89 100644 --- a/src/client/create-typed-adapter.ts +++ b/src/client/create-typed-adapter.ts @@ -10,7 +10,38 @@ import type { import type { Infer } from "convex/values"; import type { adapterWhereValidator } from "./adapter-utils.js"; -export type AdapterWhere = Infer; +type AdapterWhereBase = Infer; + +type AdapterWhereOperator = NonNullable; + +type AdapterWhereValue = + | Exclude + | (Exclude extends string ? string | Array : never) + | (Exclude extends number ? Array : never) + | null; + +type ModelAdapterWhere< + S extends SchemaDefinition, + T extends SchemaTables, +> = { + [F in keyof AdapterDoc]: { + field: F; + operator?: AdapterWhereOperator; + value: AdapterWhereValue[F]>; + connector?: AdapterWhereBase["connector"]; + mode?: AdapterWhereBase["mode"]; + }; +}[keyof AdapterDoc]; + +/** Where-clause for a single table; `field` and `value` narrow with `T`. */ +export type AdapterWhere< + S extends SchemaDefinition, + T extends SchemaTables, +> = string extends SchemaTables + ? AdapterWhereBase + : string extends keyof AdapterDoc + ? AdapterWhereBase + : ModelAdapterWhere; /** Function references for the adapter API created by {@link createApi}. */ export type AdapterFunctions = { @@ -97,7 +128,7 @@ export function createTypedAdapter>( ctx: QueryRunner, args: { model: T; - where?: Array; + where?: Array>; select?: Array; join?: unknown; } @@ -109,7 +140,7 @@ export function createTypedAdapter>( ctx: QueryRunner, args: { model: T; - where?: Array; + where?: Array>; select?: Array; limit?: number; sortBy?: SortBy; @@ -140,7 +171,7 @@ export function createTypedAdapter>( input: { model: T; update: AdapterUpdate; - where?: Array; + where?: Array>; }; onUpdateHandle?: string; } @@ -154,7 +185,7 @@ export function createTypedAdapter>( input: { model: T; update: AdapterUpdate; - where?: Array; + where?: Array>; }; paginationOpts: PaginationOptions; onUpdateHandle?: string; @@ -166,7 +197,7 @@ export function createTypedAdapter>( deleteOne>( ctx: MutationRunner, args: { - input: { model: T; where?: Array }; + input: { model: T; where?: Array> }; onDeleteHandle?: string; } ): Promise | undefined> { @@ -178,7 +209,7 @@ export function createTypedAdapter>( deleteMany>( ctx: MutationRunner, args: { - input: { model: T; where?: Array }; + input: { model: T; where?: Array> }; paginationOpts: PaginationOptions; onDeleteHandle?: string; } diff --git a/src/client/create-typed-adapter.types.test.ts b/src/client/create-typed-adapter.types.test.ts index 73f20b5a..52022c2c 100644 --- a/src/client/create-typed-adapter.types.test.ts +++ b/src/client/create-typed-adapter.types.test.ts @@ -63,3 +63,22 @@ it("infers page doc type from model on findMany", async () => { }); expectTypeOf(result.page).toEqualTypeOf>>(); }); + +it("narrows where field and value from model", () => { + adapter.findOne(queryCtx, { + model: "user", + where: [{ field: "email", value: "a@b.c" }], + }); + + adapter.findOne(queryCtx, { + model: "user", + // @ts-expect-error token is not a user field + where: [{ field: "token", value: "x" }], + }); + + adapter.findOne(queryCtx, { + model: "session", + // @ts-expect-error expiresAt expects a number + where: [{ field: "expiresAt", value: "not-a-number" }], + }); +}); From 7f9d801613d7ac9b0cf11ae0e87e2ef1a359eb40 Mon Sep 17 00:00:00 2001 From: FatahChan Date: Mon, 6 Jul 2026 11:05:38 +0300 Subject: [PATCH 11/11] feat: type-aware select and SortBy on typed adapter Narrow findOne/findMany returns from select, validate sortBy.field per model, and document where/select/sortBy narrowing in guides. Co-authored-by: Cursor --- docs/content/docs/api/component-client.mdx | 5 +- docs/content/docs/api/type-utilities.mdx | 10 +- docs/content/docs/features/local-install.mdx | 5 +- src/client/create-typed-adapter.ts | 94 ++++++++++++++----- src/client/create-typed-adapter.types.test.ts | 36 +++++++ 5 files changed, 118 insertions(+), 32 deletions(-) diff --git a/docs/content/docs/api/component-client.mdx b/docs/content/docs/api/component-client.mdx index 3f8f125a..286dd2e0 100644 --- a/docs/content/docs/api/component-client.mdx +++ b/docs/content/docs/api/component-client.mdx @@ -29,8 +29,9 @@ export const createAuth = (ctx: GenericCtx) => { Typed wrappers around `components.betterAuth.adapter` for use in your Convex functions. Prefer this over calling -`ctx.runQuery(components.betterAuth.adapter.findOne, …)` directly — the -`model` argument narrows the return type. +`ctx.runQuery(components.betterAuth.adapter.findOne, …)` directly — `model` +narrows return types, and `where`, `select`, and `sortBy` are checked against +that table's fields. With [local install](/features/local-install), pass your schema in `createClient` config. See [Type Utilities](/api/type-utilities#typedadapter-on-the-component-client). diff --git a/docs/content/docs/api/type-utilities.mdx b/docs/content/docs/api/type-utilities.mdx index e829f974..f570682c 100644 --- a/docs/content/docs/api/type-utilities.mdx +++ b/docs/content/docs/api/type-utilities.mdx @@ -8,8 +8,9 @@ description: Type utilities for Better Auth The recommended wrapper for typed adapter calls. `createClient` exposes `typedAdapter` so you don't call `ctx.runQuery(components.betterAuth.adapter.findOne, …)` directly — use -`authComponent.typedAdapter.findOne(ctx, …)` instead and the return type is -inferred from `model`. +`authComponent.typedAdapter.findOne(ctx, …)` instead. The `model` argument +narrows return types, and `where`, `select`, and `sortBy` (on `findMany`) are +checked against that table's fields. With [local install](/features/local-install), pass your schema in `createClient` config so types match your tables (including plugin tables): @@ -37,8 +38,9 @@ export const getUserByEmail = query({ return await authComponent.typedAdapter.findOne(ctx, { model: "user", where: [{ field: "email", value: args.email }], + select: ["email", "name"], }); - // Doc<"user"> | null + // Pick, "email" | "name"> | null }, }); ``` @@ -67,7 +69,7 @@ The adapter exposes the same operations as | `TypedAdapter` | Return type of `createTypedAdapter` for schema `S` | | `AdapterFunctions` | Function references passed as the second argument | | `AdapterWhere` | Where-clause for table `T` in schema `S`; `field` and `value` narrow with the model | -| `SortBy` | `{ direction: "asc" \| "desc"; field: string }` for `findMany` | +| `SortBy` | `{ direction: "asc" \| "desc"; field: … }` for `findMany`; `field` narrows with the model | | `BatchResult` | Paginated batch result from `updateMany` and `deleteMany` | ## `requireRunMutationCtx()` diff --git a/docs/content/docs/features/local-install.mdx b/docs/content/docs/features/local-install.mdx index 7019d0a0..09d43cf8 100644 --- a/docs/content/docs/features/local-install.mdx +++ b/docs/content/docs/features/local-install.mdx @@ -253,8 +253,9 @@ custom indexes aren't overwritten when the schema is regenerated. When you read or write Better Auth tables from your app (outside the component), use `authComponent.typedAdapter`. It wraps the adapter the same way as -`ctx.runQuery(components.betterAuth.adapter.findOne, …)` but infers the document -type from the `model` you pass. +`ctx.runQuery(components.betterAuth.adapter.findOne, …)` but infers document +types from `model`, and type-checks `where`, `select`, and `sortBy` against that +table. **1. Pass your local schema to `createClient`** (from the install step above): diff --git a/src/client/create-typed-adapter.ts b/src/client/create-typed-adapter.ts index 9424dd89..1a751636 100644 --- a/src/client/create-typed-adapter.ts +++ b/src/client/create-typed-adapter.ts @@ -86,7 +86,58 @@ type MutationRunner = { ) => Promise; }; -export type SortBy = { direction: "asc" | "desc"; field: string }; +type AdapterDocKey< + S extends SchemaDefinition, + T extends SchemaTables, +> = string extends keyof AdapterDoc ? string : keyof AdapterDoc; + +type AdapterSelectResult< + S extends SchemaDefinition, + T extends SchemaTables, + Sel extends ReadonlyArray> | undefined, +> = Sel extends ReadonlyArray> + ? string extends AdapterDocKey + ? Partial> + : Pick, Sel[number] & keyof AdapterDoc> + : AdapterDoc; + +type FindOneArgs< + S extends SchemaDefinition, + T extends SchemaTables, + Sel extends ReadonlyArray> | undefined, +> = { + model: T; + where?: Array>; + select?: Sel; + join?: unknown; +}; + +type FindManyArgs< + S extends SchemaDefinition, + T extends SchemaTables, + Sel extends ReadonlyArray> | undefined, +> = { + model: T; + where?: Array>; + select?: Sel; + limit?: number; + sortBy?: SortBy; + offset?: number; + join?: unknown; + paginationOpts: PaginationOptions; +}; + +type SortByBase = { direction: "asc" | "desc"; field: string }; + +/** Sort option for `findMany`; `field` narrows with table `T`. */ +export type SortBy< + S extends SchemaDefinition, + T extends SchemaTables, +> = string extends SchemaTables + ? SortByBase + : string extends keyof AdapterDoc + ? SortByBase + : { direction: "asc" | "desc"; field: keyof AdapterDoc }; export type BatchResult = { count: number; @@ -115,8 +166,9 @@ export type BatchResult = { * const user = await adapter.findOne(ctx, { * model: "user", * where: [{ field: "email", value: email }], + * select: ["email", "name"], * }); - * // user: Doc<"user"> | null + * // user: Pick, "email" | "name"> | null * ``` */ export function createTypedAdapter>( @@ -124,33 +176,27 @@ export function createTypedAdapter>( fns: AdapterFunctions ) { return { - findOne>( + findOne< + T extends SchemaTables, + const Sel extends ReadonlyArray> | undefined = undefined, + >( ctx: QueryRunner, - args: { - model: T; - where?: Array>; - select?: Array; - join?: unknown; - } - ): Promise | null> { - return ctx.runQuery(fns.findOne, args) as Promise | null>; + args: FindOneArgs + ): Promise | null> { + return ctx.runQuery(fns.findOne, args) as Promise< + AdapterSelectResult | null + >; }, - findMany>( + findMany< + T extends SchemaTables, + const Sel extends ReadonlyArray> | undefined = undefined, + >( ctx: QueryRunner, - args: { - model: T; - where?: Array>; - select?: Array; - limit?: number; - sortBy?: SortBy; - offset?: number; - join?: unknown; - paginationOpts: PaginationOptions; - } - ): Promise>> { + args: FindManyArgs + ): Promise>> { return ctx.runQuery(fns.findMany, args) as Promise< - PaginationResult> + PaginationResult> >; }, diff --git a/src/client/create-typed-adapter.types.test.ts b/src/client/create-typed-adapter.types.test.ts index 52022c2c..f3c20c0a 100644 --- a/src/client/create-typed-adapter.types.test.ts +++ b/src/client/create-typed-adapter.types.test.ts @@ -82,3 +82,39 @@ it("narrows where field and value from model", () => { where: [{ field: "expiresAt", value: "not-a-number" }], }); }); + +it("narrows findOne and findMany return type from select", async () => { + const user = await adapter.findOne(queryCtx, { + model: "user", + select: ["email", "name"], + }); + expectTypeOf(user).toEqualTypeOf, "email" | "name"> | null>(); + + const users = await adapter.findMany(queryCtx, { + model: "user", + select: ["email"], + paginationOpts: { cursor: null, numItems: 10 }, + }); + expectTypeOf(users.page).toEqualTypeOf, "email">>>(); + + adapter.findOne(queryCtx, { + model: "user", + // @ts-expect-error token is not a user field + select: ["token"], + }); +}); + +it("narrows sortBy field from model on findMany", () => { + adapter.findMany(queryCtx, { + model: "user", + sortBy: { field: "email", direction: "asc" }, + paginationOpts: { cursor: null, numItems: 10 }, + }); + + adapter.findMany(queryCtx, { + model: "user", + // @ts-expect-error token is not a user field + sortBy: { field: "token", direction: "desc" }, + paginationOpts: { cursor: null, numItems: 10 }, + }); +});