mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-20 20:26:55 -05:00
fix(mongo-adapter): recognize ObjectId and UUID values from a different bson instance
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@better-auth/mongo-adapter": patch
|
||||
---
|
||||
|
||||
Recognize ObjectId and UUID values returned by a `mongodb` driver that loaded a different `bson` module instance, so ids are normalized to strings instead of failing `instanceof`.
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { BetterAuthOptions } from "@better-auth/core";
|
||||
import type { Db } from "mongodb";
|
||||
import { ObjectId, UUID } from "mongodb";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
@@ -535,3 +536,74 @@ describe("uuid support", () => {
|
||||
expect((result as Record<string, unknown>).id).toBe(uuid);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* When the driver and the adapter resolve different `bson` module instances
|
||||
* (CJS vs ESM entry point), driver values fail `instanceof` against the
|
||||
* adapter's classes and must be recognized structurally instead.
|
||||
* @see https://github.com/better-auth/better-auth/issues/11065
|
||||
*/
|
||||
describe("ids from a different bson instance", () => {
|
||||
const hex = "000000000000000000000001";
|
||||
const uuid = "550e8400-e29b-41d4-a716-446655440000";
|
||||
const foreignObjectId = { _bsontype: "ObjectId", toHexString: () => hex };
|
||||
const foreignUUID = {
|
||||
_bsontype: "Binary",
|
||||
sub_type: 4,
|
||||
toHexString: () => uuid,
|
||||
toString: () => uuid,
|
||||
};
|
||||
|
||||
function createAdapterReturning(
|
||||
document: Record<string, unknown>,
|
||||
generateId?: "uuid",
|
||||
) {
|
||||
const aggregate = vi.fn(() => ({
|
||||
toArray: vi.fn(async () => [document]),
|
||||
}));
|
||||
const db = { collection: vi.fn(() => ({ aggregate })) } as unknown as Db;
|
||||
const adapter = mongodbAdapter(db, { transaction: false })({
|
||||
advanced: { database: generateId ? { generateId } : {} },
|
||||
} as unknown as BetterAuthOptions);
|
||||
return { adapter, aggregate };
|
||||
}
|
||||
|
||||
it("normalizes a foreign ObjectId to its hex string", async () => {
|
||||
const { adapter } = createAdapterReturning({
|
||||
_id: foreignObjectId,
|
||||
email: "example@example.test",
|
||||
});
|
||||
const user = await adapter.findOne<{ id: unknown }>({
|
||||
model: "user",
|
||||
where: [],
|
||||
});
|
||||
expect(user?.id).toBe(hex);
|
||||
});
|
||||
|
||||
it("normalizes a foreign UUID to its string form", async () => {
|
||||
const { adapter } = createAdapterReturning(
|
||||
{ _id: foreignUUID, email: "example@example.test" },
|
||||
"uuid",
|
||||
);
|
||||
const user = await adapter.findOne<{ id: unknown }>({
|
||||
model: "user",
|
||||
where: [],
|
||||
});
|
||||
expect(user?.id).toBe(uuid);
|
||||
});
|
||||
|
||||
it("accepts a foreign ObjectId as a where clause value", async () => {
|
||||
const { adapter, aggregate } = createAdapterReturning({
|
||||
_id: foreignObjectId,
|
||||
email: "example@example.test",
|
||||
});
|
||||
await adapter.findOne({
|
||||
model: "user",
|
||||
where: [{ field: "id", value: foreignObjectId as unknown as string }],
|
||||
});
|
||||
expect(aggregate).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([{ $match: { _id: foreignObjectId } }]),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,32 @@ import {
|
||||
insensitiveStartsWith,
|
||||
} from "./query-builders";
|
||||
|
||||
/**
|
||||
* Structural checks for BSON ids. The driver may resolve a different `bson`
|
||||
* module instance than the adapter (CJS vs ESM entry point), in which case
|
||||
* its values fail `instanceof` against the adapter's classes.
|
||||
* @see https://github.com/better-auth/better-auth/issues/11065
|
||||
*/
|
||||
function isObjectIdLike(value: unknown): value is ObjectId {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const { _bsontype, toHexString } = value as Record<string, unknown>;
|
||||
return (
|
||||
(_bsontype === "ObjectId" || _bsontype === "ObjectID") &&
|
||||
typeof toHexString === "function"
|
||||
);
|
||||
}
|
||||
|
||||
function isUUIDLike(value: unknown): value is UUID {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const { _bsontype, sub_type, toHexString } = value as Record<string, unknown>;
|
||||
// A BSON UUID is a Binary with sub_type 4.
|
||||
return (
|
||||
_bsontype === "Binary" &&
|
||||
sub_type === 4 &&
|
||||
typeof toHexString === "function"
|
||||
);
|
||||
}
|
||||
|
||||
class MongoAdapterError extends Error {
|
||||
constructor(
|
||||
public code: "INVALID_ID" | "UNSUPPORTED_OPERATOR",
|
||||
@@ -157,8 +183,8 @@ export const mongodbAdapter = (
|
||||
}
|
||||
|
||||
function isIdInstance(value: unknown): value is ObjectId | UUID {
|
||||
if (useUUIDs) return value instanceof UUID;
|
||||
return value instanceof ObjectId;
|
||||
if (useUUIDs) return isUUIDLike(value);
|
||||
return isObjectIdLike(value);
|
||||
}
|
||||
|
||||
function serializeID({
|
||||
@@ -801,9 +827,9 @@ export const mongodbAdapter = (
|
||||
if (action !== "create" && action !== "update") {
|
||||
return data;
|
||||
}
|
||||
const IdClass =
|
||||
options.advanced?.database?.generateId === "uuid" ? UUID : ObjectId;
|
||||
if (data instanceof IdClass) {
|
||||
const useUUIDs = options.advanced?.database?.generateId === "uuid";
|
||||
const IdClass = useUUIDs ? UUID : ObjectId;
|
||||
if (useUUIDs ? isUUIDLike(data) : isObjectIdLike(data)) {
|
||||
return data;
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
@@ -841,18 +867,18 @@ export const mongodbAdapter = (
|
||||
},
|
||||
customTransformOutput({ data, field, fieldAttributes }) {
|
||||
if (field === "id" || fieldAttributes.references?.field === "id") {
|
||||
if (data instanceof UUID) {
|
||||
if (isUUIDLike(data)) {
|
||||
return data.toString();
|
||||
}
|
||||
if (data instanceof ObjectId) {
|
||||
if (isObjectIdLike(data)) {
|
||||
return data.toHexString();
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((v) => {
|
||||
if (v instanceof UUID) {
|
||||
if (isUUIDLike(v)) {
|
||||
return v.toString();
|
||||
}
|
||||
if (v instanceof ObjectId) {
|
||||
if (isObjectIdLike(v)) {
|
||||
return v.toHexString();
|
||||
}
|
||||
return v;
|
||||
|
||||
Reference in New Issue
Block a user