mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-25 10:13:54 -05:00
feat: add SurrealDB adapter (#355)
This commit is contained in:
@@ -1,6 +1,23 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
surrealdb:
|
||||
container_name: surrealdb
|
||||
entrypoint:
|
||||
- /surreal
|
||||
- start
|
||||
- --allow-all
|
||||
- --user
|
||||
- root
|
||||
- --pass
|
||||
- root
|
||||
- rocksdb:/surrealdb/surreal.db
|
||||
volumes:
|
||||
- ./_surrealdb:/surrealdb
|
||||
image: surrealdb/surrealdb:latest
|
||||
ports:
|
||||
- 8000:8000
|
||||
|
||||
mongodb:
|
||||
image: mongo:latest
|
||||
container_name: mongodb
|
||||
@@ -35,6 +52,7 @@ services:
|
||||
- mysql_data:/var/lib/mysql
|
||||
|
||||
volumes:
|
||||
_surrealdb:
|
||||
mongodb_data:
|
||||
postgres_data:
|
||||
mysql_data:
|
||||
@@ -142,6 +142,33 @@ export const auth = betterAuth({
|
||||
})
|
||||
```
|
||||
|
||||
### SurrealDB Adapter
|
||||
|
||||
The SurrealDB adapter expects a SurrealDB client instance with established connection. See <Link href="https://surrealdb.com/docs/sdk/javascript/start#initialize-the-sdk" target="_blank">SurrealDB docs</Link>.
|
||||
SurrealDB database must be configured to allow CRUD operation of the connected user.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth";
|
||||
import { Surreal } from "surrealdb";
|
||||
import { surrealdbAdapter } from "better-auth/adapters/surrealdb";
|
||||
|
||||
const client = new Surreal();
|
||||
const instance = await client.connect(
|
||||
"http://localhost:8000", {
|
||||
namespace: 'namespace',
|
||||
database: 'database',
|
||||
auth: {
|
||||
username: 'root', // do not use root in production
|
||||
password: 'surrealdb',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: surrealdbAdapter(instance)
|
||||
})
|
||||
```
|
||||
|
||||
### MongoDB Adapter
|
||||
|
||||
The MongoDB adapter expects a mongodb client instance and a database name. The adapter will create a new database with the provided name if it doesn't exist.
|
||||
|
||||
@@ -169,6 +169,18 @@ If your database isn’t supported by Kysely, you can use an adapter to connect.
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="surrealDB">
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth";
|
||||
import { surrealdbAdapter } from "better-auth/adapters/surrealdb";
|
||||
import { surrealClient } from "@/db"; // your SurrealDB client
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: surrealdbAdapter(client)
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<Callout>
|
||||
For optimal performance, use the built-in Kysely adapter if possible.
|
||||
|
||||
@@ -137,6 +137,11 @@
|
||||
"types": "./dist/adapters/mongodb.d.ts",
|
||||
"import": "./dist/adapters/mongodb.js",
|
||||
"require": "./dist/adapters/mongodb.cjs"
|
||||
},
|
||||
"./adapters/surrealdb": {
|
||||
"types": "./dist/adapters/surrealdb.d.ts",
|
||||
"import": "./dist/adapters/surrealdb.js",
|
||||
"require": "./dist/adapters/surrealdb.cjs"
|
||||
}
|
||||
},
|
||||
"typesVersions": {
|
||||
@@ -212,6 +217,9 @@
|
||||
],
|
||||
"adapters/mongodb": [
|
||||
"./dist/adapters/mongodb.d.ts"
|
||||
],
|
||||
"adapters/surrealdb": [
|
||||
"./dist/adapters/surrealdb.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -236,6 +244,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-native": "~0.74.6",
|
||||
"solid-js": "^1.8.18",
|
||||
"surrealdb": "^1.0.6",
|
||||
"tsup": "^8.2.4",
|
||||
"typescript": "5.6.1-rc",
|
||||
"vitest": "^1.6.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./prisma-adapter";
|
||||
export * from "./drizzle-adapter";
|
||||
export * from "./mongodb-adapter";
|
||||
export * from "./surrealdb-adapter";
|
||||
export * from "./kysely-adapter";
|
||||
export * from "./test";
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { beforeAll, describe } from "vitest";
|
||||
|
||||
import { Surreal } from "surrealdb";
|
||||
import { surrealdbAdapter } from ".";
|
||||
import { runAdapterTest } from "../test";
|
||||
|
||||
describe("adapter test", async () => {
|
||||
const surrealClient = async (
|
||||
url: string,
|
||||
surrealDB: string,
|
||||
surrealNS: string,
|
||||
auth?: { username: string; password: string },
|
||||
token?: string,
|
||||
) => {
|
||||
const client = new Surreal();
|
||||
await client.connect(url, {
|
||||
namespace: surrealNS,
|
||||
database: surrealDB,
|
||||
auth: auth || token,
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
const db = await surrealClient(
|
||||
"http://127.0.0.1:8000",
|
||||
"better_auth",
|
||||
"better_auth",
|
||||
{ username: "root", password: "root" },
|
||||
);
|
||||
async function cleanup() {
|
||||
await db.query(
|
||||
"DEFINE NAMESPACE IF NOT EXISTS better_auth; DEFINE DATABASE IF NOT EXISTS better_auth; DELETE user;",
|
||||
);
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
const adapter = surrealdbAdapter(db);
|
||||
await runAdapterTest({
|
||||
adapter,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
import { PreparedQuery, type Surreal } from "surrealdb";
|
||||
import type { Adapter, Where } from "../../types";
|
||||
|
||||
/**
|
||||
* Compose a SurrealDB where clause from a list of where objects.
|
||||
*
|
||||
* @param {Where[]} where - List of where objects.
|
||||
* @param {string} model - Model name.
|
||||
* @returns {string} Composed where clause as a string.
|
||||
*/
|
||||
function composeWhereClause(where: Where[], model: string): string {
|
||||
if (!where.length) return "";
|
||||
|
||||
return where
|
||||
.map(({ field, value, operator = "eq", connector = "AND" }, index) => {
|
||||
const val =
|
||||
typeof value === "string" ? `'${value.replace(/'/g, "\\'")}'` : value;
|
||||
const mod = `'${model.replace(/'/g, "\\'")}'`;
|
||||
|
||||
const condition = {
|
||||
eq: () =>
|
||||
field === "id"
|
||||
? `${field} = type::thing(${mod}, ${val})`
|
||||
: `${field} = ${val}`,
|
||||
in: () =>
|
||||
field === "id"
|
||||
? `${field} IN [${
|
||||
Array.isArray(val)
|
||||
? val.map((v) => `type::thing('${model}', '${v}')`).join(", ")
|
||||
: `type::thing('${model}', '${val}')`
|
||||
}]`
|
||||
: `${field} IN [${
|
||||
Array.isArray(value)
|
||||
? value.map((v) => `'${v}'`).join(", ")
|
||||
: val
|
||||
}]`,
|
||||
gt: () => `${field} > ${val}`,
|
||||
gte: () => `${field} >= ${val}`,
|
||||
lt: () => `${field} < ${val}`,
|
||||
lte: () => `${field} <= ${val}`,
|
||||
ne: () => `${field} != ${val}`,
|
||||
contains: () => `${field} CONTAINS ${val}`,
|
||||
starts_with: () => `string::starts_with(${field}, ${val})`,
|
||||
ends_with: () => `string::ends_with(${field}, ${val})`,
|
||||
}[operator.toLowerCase() as typeof operator]();
|
||||
|
||||
return index > 0 ? `${connector} ${condition}` : condition;
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans for the presence of an "id" field in the array of where clauses.
|
||||
*
|
||||
* @param {Where[]} where - Array of where objects.
|
||||
* @returns {string | number | boolean | string[] | number[] | undefined}
|
||||
* The value of the "id" field if present, otherwise undefined.
|
||||
*/
|
||||
function checkForIdInWhereClause(
|
||||
where: Where[],
|
||||
): string | number | boolean | string[] | number[] | undefined {
|
||||
if (where.some(({ field }) => field === "id")) {
|
||||
return where.find(({ field }) => field === "id")?.value;
|
||||
}
|
||||
}
|
||||
|
||||
export const surrealdbAdapter = (
|
||||
surrealdb: Surreal,
|
||||
opts?: {
|
||||
/**
|
||||
* Custom generateId function.
|
||||
*
|
||||
* If not provided, nanoid will be used.
|
||||
* If set to false, the database's auto generated id will be used.
|
||||
*
|
||||
* @default nanoid
|
||||
*/
|
||||
generateId?: ((size?: number) => string) | false;
|
||||
},
|
||||
) => {
|
||||
const db = surrealdb;
|
||||
|
||||
return {
|
||||
id: "surrealdb",
|
||||
async create(data) {
|
||||
const { model, data: val } = data;
|
||||
|
||||
if (opts?.generateId !== undefined) {
|
||||
val.id = opts.generateId ? opts.generateId() : undefined;
|
||||
}
|
||||
|
||||
const query = new PreparedQuery(
|
||||
`CREATE type::table($model) CONTENT $val `,
|
||||
{
|
||||
model: model,
|
||||
val: val,
|
||||
},
|
||||
);
|
||||
|
||||
const response = await db.query<[any[]]>(query);
|
||||
const result = response[0][0];
|
||||
return result;
|
||||
},
|
||||
async findOne(data) {
|
||||
const { model, where, select = [] } = data;
|
||||
|
||||
const wheres = composeWhereClause(where, model);
|
||||
|
||||
const query =
|
||||
select.length > 0
|
||||
? new PreparedQuery(
|
||||
`SELECT type::fields($selects) FROM IF $thing {type::thing($model, $thing)} ELSE {type::table($model)} WHERE $wheres;`,
|
||||
{
|
||||
id: select.includes("id")
|
||||
? 'meta::id("id") as id, '
|
||||
: undefined,
|
||||
thing: checkForIdInWhereClause(where) || undefined,
|
||||
selects: select,
|
||||
model: model,
|
||||
wheres: wheres,
|
||||
},
|
||||
)
|
||||
: new PreparedQuery(
|
||||
`SELECT * FROM IF $thing {type::thing($model, $thing)} ELSE {type::table($model)} WHERE $wheres;`,
|
||||
{
|
||||
id: select.includes("id")
|
||||
? 'meta::id("id") as id, '
|
||||
: undefined,
|
||||
thing: checkForIdInWhereClause(where) || undefined,
|
||||
model: model,
|
||||
wheres: wheres,
|
||||
},
|
||||
);
|
||||
|
||||
const response = await db.query<[any[]]>(query);
|
||||
const result = response[0][0];
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
async findMany(data) {
|
||||
const { model, where, limit, offset, sortBy } = data;
|
||||
const clauses = [];
|
||||
|
||||
if (where) {
|
||||
const wheres = composeWhereClause(where, model);
|
||||
clauses.push(`WHERE ${wheres}`);
|
||||
}
|
||||
if (sortBy !== undefined) {
|
||||
clauses.push(`ORDER BY ${sortBy.field} ${sortBy.direction}`);
|
||||
}
|
||||
if (limit !== undefined) {
|
||||
clauses.push(`LIMIT type::number('${limit}')`);
|
||||
}
|
||||
if (offset !== undefined) {
|
||||
clauses.push(`START type::number('${offset}')`);
|
||||
}
|
||||
|
||||
const query = new PreparedQuery(
|
||||
`SELECT * FROM type::table($model) ${
|
||||
clauses.length > 0 ? clauses.join(" ") : ""
|
||||
}`,
|
||||
{
|
||||
model: model,
|
||||
},
|
||||
);
|
||||
|
||||
const response = await db.query<[any[]]>(query);
|
||||
const result = response[0];
|
||||
|
||||
return result;
|
||||
},
|
||||
async update(data) {
|
||||
const { model, where, update } = data;
|
||||
const wheres = composeWhereClause(where, model);
|
||||
if (!wheres)
|
||||
throw new Error("Empty conditions - possible unintended operation");
|
||||
|
||||
if (update.id) {
|
||||
update.id = undefined;
|
||||
}
|
||||
|
||||
const query = new PreparedQuery(
|
||||
`UPDATE type::table($model) MERGE { $update } WHERE $wheres`,
|
||||
{
|
||||
model: model,
|
||||
update: update,
|
||||
wheres: wheres,
|
||||
},
|
||||
);
|
||||
|
||||
const response = await db.query<[any[]]>(query);
|
||||
const result = response[0][0];
|
||||
|
||||
return result;
|
||||
},
|
||||
async delete(data) {
|
||||
const { model, where } = data;
|
||||
const wheres = composeWhereClause(where, model);
|
||||
if (!wheres)
|
||||
throw new Error("Empty conditions - possible unintended operation");
|
||||
|
||||
const query = new PreparedQuery(
|
||||
`DELETE type::table($model) WHERE ${wheres}`,
|
||||
{
|
||||
model: model,
|
||||
wheres: wheres,
|
||||
},
|
||||
);
|
||||
|
||||
await db.query(query);
|
||||
},
|
||||
async deleteMany(data) {
|
||||
const { model, where } = data;
|
||||
const wheres = composeWhereClause(where, model);
|
||||
if (!wheres)
|
||||
throw new Error("Empty conditions - possible unintended operation");
|
||||
|
||||
const query = new PreparedQuery(
|
||||
`DELETE type::table($model) WHERE ${wheres}`,
|
||||
{
|
||||
model: model,
|
||||
wheres: wheres,
|
||||
},
|
||||
);
|
||||
|
||||
await db.query(query);
|
||||
},
|
||||
} satisfies Adapter;
|
||||
};
|
||||
@@ -131,7 +131,7 @@ export async function runAdapterTest(opts: AdapterTestOptions) {
|
||||
expect(res.length).toBe(1);
|
||||
});
|
||||
|
||||
test("should fin many with operators", async () => {
|
||||
test("should find many with operators", async () => {
|
||||
const res = await adapter.findMany({
|
||||
model: "user",
|
||||
where: [
|
||||
|
||||
@@ -12,6 +12,7 @@ export default defineConfig((env) => {
|
||||
"adapters/prisma": "./src/adapters/prisma-adapter/index.ts",
|
||||
"adapters/drizzle": "./src/adapters/drizzle-adapter/index.ts",
|
||||
"adapters/mongodb": "./src/adapters/mongodb-adapter/index.ts",
|
||||
"adapters/surrealdb": "./src/adapters/surrealdb-adapter/index.ts",
|
||||
"adapters/kysely": "./src/adapters/kysely-adapter/index.ts",
|
||||
db: "./src/db/index.ts",
|
||||
oauth2: "./src/oauth2/index.ts",
|
||||
|
||||
Generated
+3549
-7705
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user