fix(drizzle-adapter): validate fields in compound where clauses (#10859)

This commit is contained in:
Taesu
2026-08-18 21:28:06 +00:00
committed by GitHub
parent 73a61ec1b7
commit ea77118d4e
3 changed files with 108 additions and 44 deletions
@@ -0,0 +1,5 @@
---
"@better-auth/drizzle-adapter": patch
---
Reject missing Drizzle schema fields before building compound `where` clauses, preventing malformed SQL when an application schema is out of date.
@@ -1,4 +1,11 @@
import { is, Param, SQL } from "drizzle-orm";
import { is, Param, SQL, sql } from "drizzle-orm";
import {
boolean,
integer,
pgTable,
text,
timestamp,
} from "drizzle-orm/pg-core";
import { describe, expect, it, vi } from "vitest";
import { drizzleAdapter } from "./drizzle-adapter";
@@ -206,17 +213,70 @@ describe("drizzle-adapter", () => {
});
});
describe("where field validation", () => {
it("rejects a missing field in multiple AND conditions before querying", async () => {
const account = pgTable("account", {
accountId: text("account_id").notNull(),
});
const select = vi.fn();
const adapter = drizzleAdapter(
{ _: { fullSchema: { account } }, select },
{ provider: "pg", schema: { account } },
)({ secret: "test-secret-that-is-at-least-32-chars-long!!" });
await expect(
adapter.findOne({
model: "account",
where: [
{ field: "issuer", value: "https://issuer.example" },
{ field: "accountId", value: "subject" },
],
}),
).rejects.toThrow(
'The field "issuer" does not exist in the schema for the model "account"',
);
expect(select).not.toHaveBeenCalled();
});
it("rejects inherited field names before querying", async () => {
const account = pgTable("account", {
accountId: text("account_id").notNull(),
});
const select = vi.fn();
const adapter = drizzleAdapter(
{ _: { fullSchema: { account } }, select },
{ provider: "pg", schema: { account } },
)({
secret: "test-secret-that-is-at-least-32-chars-long!!",
account: { fields: { issuer: "constructor" } },
});
await expect(
adapter.findOne({
model: "account",
where: [
{ field: "issuer", value: "https://issuer.example" },
{ field: "accountId", value: "subject" },
],
}),
).rejects.toThrow(
'The field "constructor" does not exist in the schema for the model "account"',
);
expect(select).not.toHaveBeenCalled();
});
});
describe("updateMany affected-row count", () => {
const defaultSecret = "test-secret-that-is-at-least-32-chars-long!!";
const userTable = {
id: { name: "id" },
name: { name: "name" },
email: { name: "email" },
emailVerified: { name: "emailVerified" },
image: { name: "image" },
createdAt: { name: "createdAt" },
updatedAt: { name: "updatedAt" },
};
const userTable = pgTable("user", {
id: text("id"),
name: text("name"),
email: text("email"),
emailVerified: boolean("emailVerified"),
image: text("image"),
createdAt: timestamp("createdAt"),
updatedAt: timestamp("updatedAt"),
});
/**
* Builds a mock db whose `update().set().where()` chain resolves to the
@@ -310,14 +370,14 @@ describe("drizzle-adapter", () => {
describe("consumeOne affected-row count", () => {
const defaultSecret = "test-secret-that-is-at-least-32-chars-long!!";
const verificationTable = {
id: { name: "id" },
identifier: { name: "identifier" },
value: { name: "value" },
expiresAt: { name: "expiresAt" },
createdAt: { name: "createdAt" },
updatedAt: { name: "updatedAt" },
};
const verificationTable = pgTable("verification", {
id: text("id"),
identifier: text("identifier"),
value: text("value"),
expiresAt: timestamp("expiresAt"),
createdAt: timestamp("createdAt"),
updatedAt: timestamp("updatedAt"),
});
const verificationRow = {
id: "verification-1",
identifier: "reset-password:token",
@@ -381,18 +441,16 @@ describe("drizzle-adapter", () => {
describe("incrementOne", () => {
const defaultSecret = "test-secret-that-is-at-least-32-chars-long!!";
// `attempts` is a plain numeric column the increment targets; the rest
// mirror the default user table so the factory's schema validation passes.
const userTable = {
id: { name: "id" },
name: { name: "name" },
email: { name: "email" },
emailVerified: { name: "emailVerified" },
image: { name: "image" },
attempts: { name: "attempts" },
createdAt: { name: "createdAt" },
updatedAt: { name: "updatedAt" },
};
const userTable = pgTable("user", {
id: text("id"),
name: text("name"),
email: text("email"),
emailVerified: boolean("emailVerified"),
image: text("image"),
attempts: integer("attempts"),
createdAt: timestamp("createdAt"),
updatedAt: timestamp("updatedAt"),
});
/**
* Builds a mock db that mirrors the adapter's single-row update: a
@@ -417,7 +475,7 @@ describe("drizzle-adapter", () => {
calls.set = payload;
return { where: updateWhere };
});
const targetIds = { __subquery: true };
const targetIds = sql`select id from user`;
const selectLimit = vi.fn().mockReturnValue(targetIds);
const selectWhere = vi.fn((...args: unknown[]) => {
calls.selectGuard = args;
+14 -13
View File
@@ -13,12 +13,14 @@ import type { SQL } from "drizzle-orm";
import {
and,
asc,
Column,
count,
desc,
eq,
gt,
gte,
inArray,
is,
isNotNull,
isNull,
like,
@@ -343,18 +345,22 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => {
};
function convertWhereClause(where: Where[], model: string) {
const schemaModel = getSchema(model);
const resolveFieldName = (where: Where) => {
const field = getFieldName({ model, field: where.field });
if (!is(schemaModel[field], Column)) {
throw new BetterAuthError(
`The field "${where.field}" does not exist in the schema for the model "${model}". Please update your schema.`,
);
}
return field;
};
if (!where) return [];
if (where.length === 1) {
const w = where[0];
if (!w) {
return [];
}
const field = getFieldName({ model, field: w.field });
if (!schemaModel[field]) {
throw new BetterAuthError(
`The field "${w.field}" does not exist in the schema for the model "${model}". Please update your schema.`,
);
}
const field = resolveFieldName(w);
const mode = w.mode ?? "sensitive";
const isInsensitive =
mode === "insensitive" &&
@@ -468,7 +474,7 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => {
const andClause = and(
...andGroup.map((w) => {
const field = getFieldName({ model, field: w.field });
const field = resolveFieldName(w);
const mode = w.mode ?? "sensitive";
const isInsensitive =
mode === "insensitive" &&
@@ -571,12 +577,7 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => {
);
const orClause = or(
...orGroup.map((w) => {
const field = getFieldName({ model, field: w.field });
if (!schemaModel[field]) {
throw new BetterAuthError(
`The field "${w.field}" does not exist in the schema for the model "${model}". Please update your schema.`,
);
}
const field = resolveFieldName(w);
const mode = w.mode ?? "sensitive";
const isInsensitive =
mode === "insensitive" &&