diff --git a/packages/better-auth/src/db/db.test.ts b/packages/better-auth/src/db/db.test.ts index b2c7fe24b8..bb1f881bea 100644 --- a/packages/better-auth/src/db/db.test.ts +++ b/packages/better-auth/src/db/db.test.ts @@ -97,6 +97,70 @@ describe("db", async () => { expect(session?.user.email).toBe("test@email.com"); }); + it("should coerce string where values to match field types", async () => { + // HTTP query params arrive as strings. + // The adapter should coerce values to match the field's schema type. + const { auth, db } = await getTestInstance( + { + user: { + additionalFields: { + age: { type: "number", required: false }, + }, + }, + }, + { + // Uses MongoDB because SQLite/MySQL/PostgreSQL silently cast types, + // which would make this test pass even without the coercion code. + testWith: "mongodb", + }, + ); + + // boolean: "false" → false, "true" → true + const users = await db.findMany<{ emailVerified: boolean }>({ + model: "user", + where: [{ field: "emailVerified", operator: "eq", value: "false" }], + }); + expect(users.length).toBeGreaterThanOrEqual(1); + expect(users.every((u) => u.emailVerified === false)).toBe(true); + + // number: "25" → 25 + await db.update({ + model: "user", + where: [{ field: "emailVerified", operator: "eq", value: false }], + update: { age: 25 }, + }); + const byAge = await db.findMany<{ age: number | null }>({ + model: "user", + where: [{ field: "age", operator: "eq", value: "25" }], + }); + expect(byAge.length).toBeGreaterThanOrEqual(1); + expect(byAge.every((u) => u.age === 25)).toBe(true); + + // number array: ["25", "30"] → [25, 30] + await auth.api.signUpEmail({ + body: { + email: "age30@test.com", + password: "password", + name: "user-30", + age: 30, + }, + }); + await auth.api.signUpEmail({ + body: { + email: "age40@test.com", + password: "password", + name: "user-40", + age: 40, + }, + }); + const byAgeIn = await db.findMany<{ age: number | null }>({ + model: "user", + where: [{ field: "age", operator: "in", value: ["25", "30"] }], + }); + expect(byAgeIn).toHaveLength(2); + expect(byAgeIn.map((u) => u.age).sort()).toEqual([25, 30]); + }); + it("delete hooks", async () => { const hookUserDeleteBefore = vi.fn(); const hookUserDeleteAfter = vi.fn(); diff --git a/packages/core/src/db/adapter/factory.ts b/packages/core/src/db/adapter/factory.ts index aa360fec88..c27ad454d7 100644 --- a/packages/core/src/db/adapter/factory.ts +++ b/packages/core/src/db/adapter/factory.ts @@ -540,12 +540,32 @@ export const createAdapterFactory = newValue = value.toISOString(); } + if (fieldAttr.type === "boolean" && typeof newValue === "string") { + newValue = newValue === "true"; + } + + if (fieldAttr.type === "number") { + if (typeof newValue === "string" && newValue.trim() !== "") { + const parsed = Number(newValue); + if (!Number.isNaN(parsed)) { + newValue = parsed; + } + } else if (Array.isArray(newValue)) { + const parsed = newValue.map((v) => + typeof v === "string" && v.trim() !== "" ? Number(v) : NaN, + ); + if (parsed.every((n) => !Number.isNaN(n))) { + newValue = parsed; + } + } + } + if ( fieldAttr.type === "boolean" && - typeof value === "boolean" && + typeof newValue === "boolean" && !config.supportsBooleans ) { - newValue = value ? 1 : 0; + newValue = newValue ? 1 : 0; } if (