mirror of
https://github.com/better-auth/better-auth.git
synced 2026-08-28 14:45:40 -05:00
fix(drizzle-adapter): validate fields in compound where clauses (#10859)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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" &&
|
||||
|
||||
Reference in New Issue
Block a user