surreal-better-auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yaml +59 -0
- package/.github/workflows/release.yaml +79 -0
- package/CHANGELOG.md +1 -0
- package/LICENSE +17 -0
- package/db/surreal.ts +46 -0
- package/package.json +37 -0
- package/src/index.ts +228 -0
- package/tests/runAdapterTest.ts +443 -0
- package/tests/surreal-adapter.test.ts +94 -0
- package/tests/testInstance.ts +222 -0
- package/tsconfig.json +11 -0
- package/utlis/getBaseURL.ts +32 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: Run Tests with SurrealDB
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
# Checkout code
|
|
15
|
+
- name: Checkout repository
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
# Setup SurrealDB
|
|
18
|
+
- name: Start SurrealDB
|
|
19
|
+
uses: surrealdb/setup-surreal@v2
|
|
20
|
+
with:
|
|
21
|
+
surrealdb_version: latest
|
|
22
|
+
surrealdb_port: 8000
|
|
23
|
+
surrealdb_username: root
|
|
24
|
+
surrealdb_password: root
|
|
25
|
+
surrealdb_auth: false
|
|
26
|
+
surrealdb_strict: false
|
|
27
|
+
surrealdb_log: info
|
|
28
|
+
surrealdb_additional_args: --allow-all
|
|
29
|
+
surrealdb_retry_count: 30
|
|
30
|
+
# Checkout code
|
|
31
|
+
- name: Checkout repository
|
|
32
|
+
uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
# Setup Bun
|
|
35
|
+
- name: Setup Bun
|
|
36
|
+
uses: oven-sh/setup-bun@v1
|
|
37
|
+
|
|
38
|
+
# Install dependencies
|
|
39
|
+
- name: Install dependencies
|
|
40
|
+
run: bun install
|
|
41
|
+
|
|
42
|
+
# Wait for SurrealDB
|
|
43
|
+
- name: Wait for SurrealDB
|
|
44
|
+
run: |
|
|
45
|
+
echo "Waiting for SurrealDB to become healthy..."
|
|
46
|
+
until curl --fail http://localhost:8000/status; do
|
|
47
|
+
echo "Waiting for SurrealDB..."
|
|
48
|
+
sleep 1
|
|
49
|
+
done
|
|
50
|
+
echo "SurrealDB is up and running!"
|
|
51
|
+
|
|
52
|
+
# Run tests
|
|
53
|
+
- name: Run tests
|
|
54
|
+
run: bun test
|
|
55
|
+
|
|
56
|
+
# Stop SurrealDB and cleanup
|
|
57
|
+
- name: Stop SurrealDB
|
|
58
|
+
if: always()
|
|
59
|
+
run: echo "Cleaning up... SurrealDB service will stop automatically."
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_run:
|
|
5
|
+
workflows:
|
|
6
|
+
- ci
|
|
7
|
+
types:
|
|
8
|
+
- completed
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release:
|
|
12
|
+
name: Create Release
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
# Checkout code
|
|
19
|
+
- name: Checkout repository
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
# Setup Bun
|
|
23
|
+
- name: Setup Bun
|
|
24
|
+
uses: oven-sh/setup-bun@v1
|
|
25
|
+
|
|
26
|
+
# Install dependencies
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: bun install
|
|
29
|
+
|
|
30
|
+
# Check version change
|
|
31
|
+
- name: Get current and previous version
|
|
32
|
+
id: version
|
|
33
|
+
run: |
|
|
34
|
+
PREV_VERSION=$(git tag --list --sort=-v:refname 'v*' | head -n 1 | sed 's/v//')
|
|
35
|
+
CURR_VERSION=$(node -p "require('./package.json').version")
|
|
36
|
+
echo "Previous Version: $PREV_VERSION"
|
|
37
|
+
echo "Current Version: $CURR_VERSION"
|
|
38
|
+
if [ "$PREV_VERSION" = "$CURR_VERSION" ]; then
|
|
39
|
+
echo "Version unchanged. Exiting..."
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
echo "version=$CURR_VERSION" >> $GITHUB_OUTPUT
|
|
43
|
+
|
|
44
|
+
# Generate Changelog
|
|
45
|
+
- name: Generate Changelog
|
|
46
|
+
id: changelog
|
|
47
|
+
uses: orhun/git-cliff-action@v2
|
|
48
|
+
with:
|
|
49
|
+
args: --output CHANGELOG.md
|
|
50
|
+
env:
|
|
51
|
+
GIT_CLIFF_CONFIG: |
|
|
52
|
+
[changelog]
|
|
53
|
+
header = "## Changelog"
|
|
54
|
+
footer = ""
|
|
55
|
+
trim = true
|
|
56
|
+
|
|
57
|
+
[commit-types]
|
|
58
|
+
feat = "✨ Features"
|
|
59
|
+
fix = "🐛 Bug Fixes"
|
|
60
|
+
docs = "📝 Documentation"
|
|
61
|
+
chore = "📦 Chores"
|
|
62
|
+
refactor = "♻️ Refactors"
|
|
63
|
+
|
|
64
|
+
# Create GitHub Release
|
|
65
|
+
- name: Create GitHub Release
|
|
66
|
+
uses: softprops/action-gh-release@v1
|
|
67
|
+
with:
|
|
68
|
+
tag_name: v${{ steps.version.outputs.version }}
|
|
69
|
+
name: Release v${{ steps.version.outputs.version }}
|
|
70
|
+
body_path: CHANGELOG.md
|
|
71
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
72
|
+
|
|
73
|
+
# Publish to npm
|
|
74
|
+
- name: Publish to npm
|
|
75
|
+
run: |
|
|
76
|
+
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
|
77
|
+
bun publish
|
|
78
|
+
env:
|
|
79
|
+
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) 2024 - present, Oskar Gmerek
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
5
|
+
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
|
6
|
+
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
7
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
|
|
8
|
+
is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
11
|
+
substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
14
|
+
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
15
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
16
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
17
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/db/surreal.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Surreal from "surrealdb";
|
|
2
|
+
|
|
3
|
+
// Define the database configuration interface
|
|
4
|
+
interface DatabaseConfig {
|
|
5
|
+
url: string;
|
|
6
|
+
namespace: string;
|
|
7
|
+
database: string;
|
|
8
|
+
auth: {
|
|
9
|
+
username: string;
|
|
10
|
+
password: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Define the default database configuration
|
|
15
|
+
const DEFAULT_CONFIG: DatabaseConfig = {
|
|
16
|
+
url: "http://127.0.0.1:8000/rpc",
|
|
17
|
+
namespace: "better_auth",
|
|
18
|
+
database: "better_auth",
|
|
19
|
+
auth: {
|
|
20
|
+
username: "root",
|
|
21
|
+
password: "root",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Define the function to get the database instance
|
|
26
|
+
export async function getDatabase(
|
|
27
|
+
config: DatabaseConfig = DEFAULT_CONFIG,
|
|
28
|
+
): Promise<Surreal> {
|
|
29
|
+
const db = new Surreal();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await db.connect(config.url, {
|
|
33
|
+
namespace: config.namespace,
|
|
34
|
+
database: config.database,
|
|
35
|
+
auth: { username: config.auth.username, password: config.auth.password },
|
|
36
|
+
});
|
|
37
|
+
return db;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(
|
|
40
|
+
"Failed to connect to SurrealDB:",
|
|
41
|
+
err instanceof Error ? err.message : String(err),
|
|
42
|
+
);
|
|
43
|
+
await db.close();
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "surreal-better-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Oskar Gmerek",
|
|
6
|
+
"url": "https://oskargmerek.com",
|
|
7
|
+
"email": "oskar.gmerek@neetrio.com",
|
|
8
|
+
"github": "https://github.com/oskar-gmerek"
|
|
9
|
+
},
|
|
10
|
+
"description": "Better Auth adapter for SurrealDB",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"adapter",
|
|
13
|
+
"auth",
|
|
14
|
+
"authentication",
|
|
15
|
+
"database",
|
|
16
|
+
"better-auth",
|
|
17
|
+
"better-auth-adapter",
|
|
18
|
+
"surrealdb",
|
|
19
|
+
"surrealdb-adapter",
|
|
20
|
+
"surrealdb-auth",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/oskar-gmerek/surreal-better-auth.git"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"main": "src/index.ts",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "^1.1.14"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"better-auth": "^1.0.22",
|
|
34
|
+
"surrealdb": "^1.1.0",
|
|
35
|
+
"typescript": "^5.7.2"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { Adapter, BetterAuthOptions, Where } from "better-auth/types";
|
|
2
|
+
import { PreparedQuery, type Surreal } from "surrealdb";
|
|
3
|
+
|
|
4
|
+
function composeWhereClause(where: Where[], model: string): string {
|
|
5
|
+
if (!where.length) return "";
|
|
6
|
+
|
|
7
|
+
return where
|
|
8
|
+
.map(({ field, value, operator = "eq", connector = "AND" }, index) => {
|
|
9
|
+
const val =
|
|
10
|
+
typeof value === "string" ? `'${value.replace(/'/g, "\\'")}'` : value;
|
|
11
|
+
const mod = `'${model.replace(/'/g, "\\'")}'`;
|
|
12
|
+
|
|
13
|
+
const condition = {
|
|
14
|
+
eq: () =>
|
|
15
|
+
field === "id"
|
|
16
|
+
? `${field} = type::thing(${mod}, ${val})`
|
|
17
|
+
: `${field} = ${val}`,
|
|
18
|
+
in: () =>
|
|
19
|
+
field === "id"
|
|
20
|
+
? `${field} IN [${
|
|
21
|
+
Array.isArray(val)
|
|
22
|
+
? val.map((v) => `type::thing('${model}', ${v})`).join(", ")
|
|
23
|
+
: `type::thing('${model}', ${val})`
|
|
24
|
+
}]`
|
|
25
|
+
: `${field} IN [${
|
|
26
|
+
Array.isArray(value) ? value.map((v) => `${v}`).join(", ") : val
|
|
27
|
+
}]`,
|
|
28
|
+
gt: () => `${field} > ${val}`,
|
|
29
|
+
gte: () => `${field} >= ${val}`,
|
|
30
|
+
lt: () => `${field} < ${val}`,
|
|
31
|
+
lte: () => `${field} <= ${val}`,
|
|
32
|
+
ne: () => `${field} != ${val}`,
|
|
33
|
+
contains: () => `${field} CONTAINS ${val}`,
|
|
34
|
+
starts_with: () => `string::starts_with(${field}, ${val})`,
|
|
35
|
+
ends_with: () => `string::ends_with(${field}, ${val})`,
|
|
36
|
+
}[operator.toLowerCase() as typeof operator]();
|
|
37
|
+
|
|
38
|
+
return index > 0 ? `${connector} ${condition}` : condition;
|
|
39
|
+
})
|
|
40
|
+
.join(" ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function checkForIdInWhereClause(
|
|
44
|
+
where: Where[],
|
|
45
|
+
): string | number | boolean | string[] | number[] | undefined {
|
|
46
|
+
if (where.some(({ field }) => field === "id")) {
|
|
47
|
+
return where.find(({ field }) => field === "id")?.value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const surrealAdapter = (db: Surreal) => (options: BetterAuthOptions) => {
|
|
52
|
+
if (!db) {
|
|
53
|
+
throw new Error("SurrealDB adapter requires a SurrealDB client");
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
id: "surrealdb",
|
|
57
|
+
async create(data) {
|
|
58
|
+
const { model, data: val } = data;
|
|
59
|
+
|
|
60
|
+
const query = new PreparedQuery(
|
|
61
|
+
`CREATE type::table($model) CONTENT $val `,
|
|
62
|
+
{
|
|
63
|
+
model: model,
|
|
64
|
+
val: val,
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const response = await db.query<[any[]]>(query);
|
|
69
|
+
const result = response[0][0];
|
|
70
|
+
return result;
|
|
71
|
+
},
|
|
72
|
+
async findOne(data) {
|
|
73
|
+
const { model, where, select = [] } = data;
|
|
74
|
+
|
|
75
|
+
const wheres = composeWhereClause(where, model);
|
|
76
|
+
|
|
77
|
+
const query =
|
|
78
|
+
select.length > 0
|
|
79
|
+
? new PreparedQuery(
|
|
80
|
+
`SELECT type::fields($selects) FROM IF $thing {type::thing($model, $thing)} ELSE {type::table($model)} WHERE $wheres;`,
|
|
81
|
+
{
|
|
82
|
+
id: select.includes("id")
|
|
83
|
+
? 'meta::id("id") as id, '
|
|
84
|
+
: undefined,
|
|
85
|
+
thing: checkForIdInWhereClause(where) || undefined,
|
|
86
|
+
selects: select,
|
|
87
|
+
model: model,
|
|
88
|
+
wheres: wheres,
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
: new PreparedQuery(
|
|
92
|
+
`SELECT * FROM IF $thing {type::thing($model, $thing)} ELSE {type::table($model)} WHERE $wheres;`,
|
|
93
|
+
{
|
|
94
|
+
id: select.includes("id")
|
|
95
|
+
? 'meta::id("id") as id, '
|
|
96
|
+
: undefined,
|
|
97
|
+
thing: checkForIdInWhereClause(where) || undefined,
|
|
98
|
+
model: model,
|
|
99
|
+
wheres: wheres,
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const response = await db.query<[any[]]>(query);
|
|
104
|
+
const result = response[0][0];
|
|
105
|
+
|
|
106
|
+
if (!result) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
},
|
|
112
|
+
async findMany(data) {
|
|
113
|
+
const { model, where, limit, offset, sortBy } = data;
|
|
114
|
+
const clauses: string[] = [];
|
|
115
|
+
|
|
116
|
+
if (where) {
|
|
117
|
+
const wheres = composeWhereClause(where, model);
|
|
118
|
+
clauses.push(`WHERE ${wheres}`);
|
|
119
|
+
}
|
|
120
|
+
if (sortBy !== undefined) {
|
|
121
|
+
clauses.push(`ORDER BY ${sortBy.field} ${sortBy.direction}`);
|
|
122
|
+
}
|
|
123
|
+
if (limit !== undefined) {
|
|
124
|
+
clauses.push(`LIMIT type::number('${limit}')`);
|
|
125
|
+
}
|
|
126
|
+
if (offset !== undefined) {
|
|
127
|
+
clauses.push(`START type::number('${offset}')`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const query = new PreparedQuery(
|
|
131
|
+
`SELECT * FROM type::table($model) ${
|
|
132
|
+
clauses.length > 0 ? clauses.join(" ") : ""
|
|
133
|
+
}`,
|
|
134
|
+
{
|
|
135
|
+
model: model,
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const response = await db.query<[any[]]>(query);
|
|
140
|
+
const result = response[0];
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
},
|
|
144
|
+
async update(data) {
|
|
145
|
+
const { model, where, update } = data;
|
|
146
|
+
const wheres = composeWhereClause(where, model);
|
|
147
|
+
if (!wheres)
|
|
148
|
+
throw new Error("Empty conditions - possible unintended operation");
|
|
149
|
+
|
|
150
|
+
if (update.id) {
|
|
151
|
+
update.id = undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const query = new PreparedQuery(
|
|
155
|
+
"UPDATE type::table($model) MERGE { $update } WHERE $wheres",
|
|
156
|
+
{
|
|
157
|
+
model: model,
|
|
158
|
+
update: update,
|
|
159
|
+
wheres: wheres,
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const response = await db.query<[any[]]>(query);
|
|
164
|
+
const result = response[0][0];
|
|
165
|
+
|
|
166
|
+
return result;
|
|
167
|
+
},
|
|
168
|
+
async updateMany(data) {
|
|
169
|
+
const { model, where, update } = data;
|
|
170
|
+
const wheres = composeWhereClause(where, model);
|
|
171
|
+
if (!wheres)
|
|
172
|
+
throw new Error("Empty conditions - possible unintended operation");
|
|
173
|
+
|
|
174
|
+
if (update.id) {
|
|
175
|
+
update.id = undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const query = new PreparedQuery(
|
|
179
|
+
"UPDATE type::table($model) MERGE { $update } WHERE $wheres",
|
|
180
|
+
{
|
|
181
|
+
model: model,
|
|
182
|
+
update: update,
|
|
183
|
+
wheres: wheres,
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const response = await db.query<[any[]]>(query);
|
|
188
|
+
const result = response[0];
|
|
189
|
+
|
|
190
|
+
return result.length;
|
|
191
|
+
},
|
|
192
|
+
async delete(data) {
|
|
193
|
+
const { model, where } = data;
|
|
194
|
+
const wheres = composeWhereClause(where, model);
|
|
195
|
+
if (!wheres)
|
|
196
|
+
throw new Error("Empty conditions - possible unintended operation");
|
|
197
|
+
|
|
198
|
+
const query = new PreparedQuery(
|
|
199
|
+
`DELETE type::table($model) WHERE ${wheres}`,
|
|
200
|
+
{
|
|
201
|
+
model: model,
|
|
202
|
+
wheres: wheres,
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await db.query(query);
|
|
207
|
+
},
|
|
208
|
+
async deleteMany(data) {
|
|
209
|
+
const { model, where } = data;
|
|
210
|
+
const wheres = composeWhereClause(where, model);
|
|
211
|
+
if (!wheres)
|
|
212
|
+
throw new Error("Empty conditions - possible unintended operation");
|
|
213
|
+
|
|
214
|
+
const query = new PreparedQuery(
|
|
215
|
+
`DELETE type::table($model) WHERE ${wheres}`,
|
|
216
|
+
{
|
|
217
|
+
model: model,
|
|
218
|
+
wheres: wheres,
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const response = await db.query<[any[]]>(query);
|
|
223
|
+
const result = response[0];
|
|
224
|
+
|
|
225
|
+
return result.length;
|
|
226
|
+
},
|
|
227
|
+
} satisfies Adapter;
|
|
228
|
+
};
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { generateId } from "better-auth";
|
|
3
|
+
import type { Adapter, BetterAuthOptions, User } from "better-auth/types";
|
|
4
|
+
|
|
5
|
+
interface AdapterTestOptions {
|
|
6
|
+
getAdapter: (
|
|
7
|
+
customOptions?: Omit<BetterAuthOptions, "database">,
|
|
8
|
+
) => Promise<Adapter>;
|
|
9
|
+
skipGenerateIdTest?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runAdapterTest(opts: AdapterTestOptions) {
|
|
13
|
+
const adapter = await opts.getAdapter();
|
|
14
|
+
const user = {
|
|
15
|
+
id: "1",
|
|
16
|
+
name: "user",
|
|
17
|
+
email: "user@email.com",
|
|
18
|
+
emailVerified: true,
|
|
19
|
+
createdAt: new Date(),
|
|
20
|
+
updatedAt: new Date(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
test("create model", async () => {
|
|
24
|
+
const res = await adapter.create({
|
|
25
|
+
model: "user",
|
|
26
|
+
data: user,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect({
|
|
30
|
+
name: res.name,
|
|
31
|
+
email: res.email,
|
|
32
|
+
}).toEqual({
|
|
33
|
+
name: user.name,
|
|
34
|
+
email: user.email,
|
|
35
|
+
});
|
|
36
|
+
user.id = res.id;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("find model", async () => {
|
|
40
|
+
const res = await adapter.findOne<User>({
|
|
41
|
+
model: "user",
|
|
42
|
+
where: [
|
|
43
|
+
{
|
|
44
|
+
field: "id",
|
|
45
|
+
value: user.id,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
expect({
|
|
50
|
+
name: res?.name,
|
|
51
|
+
email: res?.email,
|
|
52
|
+
}).toEqual({
|
|
53
|
+
name: user.name,
|
|
54
|
+
email: user.email,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("find model without id", async () => {
|
|
59
|
+
const res = await adapter.findOne<User>({
|
|
60
|
+
model: "user",
|
|
61
|
+
where: [
|
|
62
|
+
{
|
|
63
|
+
field: "email",
|
|
64
|
+
value: user.email,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
expect({
|
|
69
|
+
name: res?.name,
|
|
70
|
+
email: res?.email,
|
|
71
|
+
}).toEqual({
|
|
72
|
+
name: user.name,
|
|
73
|
+
email: user.email,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("find model with select", async () => {
|
|
78
|
+
const res = await adapter.findOne({
|
|
79
|
+
model: "user",
|
|
80
|
+
where: [
|
|
81
|
+
{
|
|
82
|
+
field: "id",
|
|
83
|
+
value: user.id,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
select: ["email"],
|
|
87
|
+
});
|
|
88
|
+
expect(res).toEqual({ email: user.email });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("update model", async () => {
|
|
92
|
+
const newEmail = "updated@email.com";
|
|
93
|
+
|
|
94
|
+
const res = await adapter.update<User>({
|
|
95
|
+
model: "user",
|
|
96
|
+
where: [
|
|
97
|
+
{
|
|
98
|
+
field: "id",
|
|
99
|
+
value: user.id,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
update: {
|
|
103
|
+
email: newEmail,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
expect(res).toMatchObject({
|
|
107
|
+
email: newEmail,
|
|
108
|
+
name: user.name,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("should find many", async () => {
|
|
113
|
+
const res = await adapter.findMany({
|
|
114
|
+
model: "user",
|
|
115
|
+
});
|
|
116
|
+
expect(res.length).toBe(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("should find many with where", async () => {
|
|
120
|
+
const user = await adapter.create<User>({
|
|
121
|
+
model: "user",
|
|
122
|
+
data: {
|
|
123
|
+
id: "2",
|
|
124
|
+
name: "user2",
|
|
125
|
+
email: "test@email.com",
|
|
126
|
+
emailVerified: true,
|
|
127
|
+
createdAt: new Date(),
|
|
128
|
+
updatedAt: new Date(),
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const res = await adapter.findMany({
|
|
132
|
+
model: "user",
|
|
133
|
+
where: [
|
|
134
|
+
{
|
|
135
|
+
field: "id",
|
|
136
|
+
value: user.id,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
expect(res.length).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("should find many with operators", async () => {
|
|
144
|
+
const newUser = await adapter.create<User>({
|
|
145
|
+
model: "user",
|
|
146
|
+
data: {
|
|
147
|
+
id: "3",
|
|
148
|
+
name: "user",
|
|
149
|
+
email: "test-email2@email.com",
|
|
150
|
+
emailVerified: true,
|
|
151
|
+
createdAt: new Date(),
|
|
152
|
+
updatedAt: new Date(),
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const res = await adapter.findMany({
|
|
156
|
+
model: "user",
|
|
157
|
+
where: [
|
|
158
|
+
{
|
|
159
|
+
field: "id",
|
|
160
|
+
operator: "in",
|
|
161
|
+
value: [user.id, newUser.id],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
expect(res.length).toBe(2);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("should work with reference fields", async () => {
|
|
169
|
+
const user = await adapter.create<{ id: string } & Record<string, any>>({
|
|
170
|
+
model: "user",
|
|
171
|
+
data: {
|
|
172
|
+
id: "4",
|
|
173
|
+
name: "user",
|
|
174
|
+
email: "my-email@email.com",
|
|
175
|
+
emailVerified: true,
|
|
176
|
+
createdAt: new Date(),
|
|
177
|
+
updatedAt: new Date(),
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
await adapter.create({
|
|
181
|
+
model: "session",
|
|
182
|
+
data: {
|
|
183
|
+
id: "1",
|
|
184
|
+
token: generateId(),
|
|
185
|
+
createdAt: new Date(),
|
|
186
|
+
updatedAt: new Date(),
|
|
187
|
+
userId: user.id,
|
|
188
|
+
expiresAt: new Date(),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const res = await adapter.findOne({
|
|
192
|
+
model: "session",
|
|
193
|
+
where: [
|
|
194
|
+
{
|
|
195
|
+
field: "userId",
|
|
196
|
+
value: user.id,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
expect(res).toMatchObject({
|
|
201
|
+
userId: user.id,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("should find many with sortBy", async () => {
|
|
206
|
+
await adapter.create({
|
|
207
|
+
model: "user",
|
|
208
|
+
data: {
|
|
209
|
+
id: "5",
|
|
210
|
+
name: "a",
|
|
211
|
+
email: "a@email.com",
|
|
212
|
+
emailVerified: true,
|
|
213
|
+
createdAt: new Date(),
|
|
214
|
+
updatedAt: new Date(),
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
const res = await adapter.findMany<User>({
|
|
218
|
+
model: "user",
|
|
219
|
+
sortBy: {
|
|
220
|
+
field: "name",
|
|
221
|
+
direction: "asc",
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
expect(res[0].name).toBe("a");
|
|
225
|
+
|
|
226
|
+
const res2 = await adapter.findMany<User>({
|
|
227
|
+
model: "user",
|
|
228
|
+
sortBy: {
|
|
229
|
+
field: "name",
|
|
230
|
+
direction: "desc",
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(res2[res2.length - 1].name).toBe("a");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("should find many with limit", async () => {
|
|
238
|
+
const res = await adapter.findMany({
|
|
239
|
+
model: "user",
|
|
240
|
+
limit: 1,
|
|
241
|
+
});
|
|
242
|
+
expect(res.length).toBe(1);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("should find many with offset", async () => {
|
|
246
|
+
const res = await adapter.findMany({
|
|
247
|
+
model: "user",
|
|
248
|
+
offset: 2,
|
|
249
|
+
});
|
|
250
|
+
expect(res.length).toBe(3);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("should update with multiple where", async () => {
|
|
254
|
+
await adapter.updateMany({
|
|
255
|
+
model: "user",
|
|
256
|
+
where: [
|
|
257
|
+
{
|
|
258
|
+
field: "name",
|
|
259
|
+
value: user.name,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
field: "email",
|
|
263
|
+
value: user.email,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
update: {
|
|
267
|
+
email: "updated@email.com",
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
const updatedUser = await adapter.findOne<User>({
|
|
271
|
+
model: "user",
|
|
272
|
+
where: [
|
|
273
|
+
{
|
|
274
|
+
field: "email",
|
|
275
|
+
value: "updated@email.com",
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
expect(updatedUser).toMatchObject({
|
|
280
|
+
name: user.name,
|
|
281
|
+
email: "updated@email.com",
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("delete model", async () => {
|
|
286
|
+
await adapter.delete({
|
|
287
|
+
model: "user",
|
|
288
|
+
where: [
|
|
289
|
+
{
|
|
290
|
+
field: "id",
|
|
291
|
+
value: user.id,
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
});
|
|
295
|
+
const findRes = await adapter.findOne({
|
|
296
|
+
model: "user",
|
|
297
|
+
where: [
|
|
298
|
+
{
|
|
299
|
+
field: "id",
|
|
300
|
+
value: user.id,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
});
|
|
304
|
+
expect(findRes).toBeNull();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("should delete many", async () => {
|
|
308
|
+
for (const id of ["to-be-delete1", "to-be-delete2", "to-be-delete3"]) {
|
|
309
|
+
await adapter.create({
|
|
310
|
+
model: "user",
|
|
311
|
+
data: {
|
|
312
|
+
id,
|
|
313
|
+
name: "to-be-deleted",
|
|
314
|
+
email: `email@test-${id}.com`,
|
|
315
|
+
emailVerified: true,
|
|
316
|
+
createdAt: new Date(),
|
|
317
|
+
updatedAt: new Date(),
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const findResFirst = await adapter.findMany({
|
|
322
|
+
model: "user",
|
|
323
|
+
where: [
|
|
324
|
+
{
|
|
325
|
+
field: "name",
|
|
326
|
+
value: "to-be-deleted",
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
});
|
|
330
|
+
expect(findResFirst.length).toBe(3);
|
|
331
|
+
await adapter.deleteMany({
|
|
332
|
+
model: "user",
|
|
333
|
+
where: [
|
|
334
|
+
{
|
|
335
|
+
field: "name",
|
|
336
|
+
value: "to-be-deleted",
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
});
|
|
340
|
+
const findRes = await adapter.findMany({
|
|
341
|
+
model: "user",
|
|
342
|
+
where: [
|
|
343
|
+
{
|
|
344
|
+
field: "name",
|
|
345
|
+
value: "to-be-deleted",
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
});
|
|
349
|
+
expect(findRes.length).toBe(0);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("shouldn't throw on delete record not found", async () => {
|
|
353
|
+
await adapter.delete({
|
|
354
|
+
model: "user",
|
|
355
|
+
where: [
|
|
356
|
+
{
|
|
357
|
+
field: "id",
|
|
358
|
+
value: "5",
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("shouldn't throw on record not found", async () => {
|
|
365
|
+
const res = await adapter.findOne({
|
|
366
|
+
model: "user",
|
|
367
|
+
where: [
|
|
368
|
+
{
|
|
369
|
+
field: "id",
|
|
370
|
+
value: "5",
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
});
|
|
374
|
+
expect(res).toBeNull();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("should find many with contains operator", async () => {
|
|
378
|
+
const res = await adapter.findMany({
|
|
379
|
+
model: "user",
|
|
380
|
+
where: [
|
|
381
|
+
{
|
|
382
|
+
field: "name",
|
|
383
|
+
operator: "contains",
|
|
384
|
+
value: "user2",
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
});
|
|
388
|
+
expect(res.length).toBe(1);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("should search users with startsWith", async () => {
|
|
392
|
+
const res = await adapter.findMany({
|
|
393
|
+
model: "user",
|
|
394
|
+
where: [
|
|
395
|
+
{
|
|
396
|
+
field: "name",
|
|
397
|
+
operator: "starts_with",
|
|
398
|
+
value: "us",
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
});
|
|
402
|
+
expect(res.length).toBe(3);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("should search users with endsWith", async () => {
|
|
406
|
+
const res = await adapter.findMany({
|
|
407
|
+
model: "user",
|
|
408
|
+
where: [
|
|
409
|
+
{
|
|
410
|
+
field: "name",
|
|
411
|
+
operator: "ends_with",
|
|
412
|
+
value: "er2",
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
});
|
|
416
|
+
expect(res.length).toBe(1);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test.skipIf(opts.skipGenerateIdTest || false)(
|
|
420
|
+
"should prefer generateId if provided",
|
|
421
|
+
async () => {
|
|
422
|
+
const customAdapter = await opts.getAdapter({
|
|
423
|
+
advanced: {
|
|
424
|
+
generateId: () => "mocked-id",
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const res = await customAdapter.create({
|
|
429
|
+
model: "user",
|
|
430
|
+
data: {
|
|
431
|
+
id: "1",
|
|
432
|
+
name: "user4",
|
|
433
|
+
email: "user4@email.com",
|
|
434
|
+
emailVerified: true,
|
|
435
|
+
createdAt: new Date(),
|
|
436
|
+
updatedAt: new Date(),
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(res.id).toBe("mocked-id");
|
|
441
|
+
},
|
|
442
|
+
);
|
|
443
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "bun:test";
|
|
2
|
+
import { surrealAdapter } from "../src";
|
|
3
|
+
import { getDatabase } from "../db/surreal";
|
|
4
|
+
import { runAdapterTest } from "./runAdapterTest";
|
|
5
|
+
import { getTestInstance } from "./testInstance";
|
|
6
|
+
|
|
7
|
+
describe("adapter test", async () => {
|
|
8
|
+
const db = await getDatabase();
|
|
9
|
+
|
|
10
|
+
async function setupDB() {
|
|
11
|
+
await db.query(
|
|
12
|
+
`
|
|
13
|
+
DEFINE NAMESPACE IF NOT EXISTS better_auth;
|
|
14
|
+
DEFINE DATABASE IF NOT EXISTS better_auth;
|
|
15
|
+
DELETE user;
|
|
16
|
+
DELETE session
|
|
17
|
+
`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
await setupDB();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const adapter = surrealAdapter(db);
|
|
26
|
+
await runAdapterTest({
|
|
27
|
+
getAdapter: async (customOptions = {}) => {
|
|
28
|
+
return adapter({
|
|
29
|
+
user: {
|
|
30
|
+
fields: {
|
|
31
|
+
email: "email_address",
|
|
32
|
+
},
|
|
33
|
+
additionalFields: {
|
|
34
|
+
test: {
|
|
35
|
+
type: "string",
|
|
36
|
+
defaultValue: "test",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
session: {
|
|
41
|
+
modelName: "sessions",
|
|
42
|
+
},
|
|
43
|
+
...customOptions,
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
skipGenerateIdTest: true,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("simple-flow", async () => {
|
|
51
|
+
const { auth, client, sessionSetter } = await getTestInstance(
|
|
52
|
+
{},
|
|
53
|
+
{
|
|
54
|
+
disableTestUser: true,
|
|
55
|
+
testWith: "surreal",
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
const testUser = {
|
|
59
|
+
email: "test-eamil@email.com",
|
|
60
|
+
password: "password",
|
|
61
|
+
name: "Test Name",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
it("should sign up", async () => {
|
|
65
|
+
const user = await auth.api.signUpEmail({
|
|
66
|
+
body: testUser,
|
|
67
|
+
});
|
|
68
|
+
expect(user).toBeDefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should sign in", async () => {
|
|
72
|
+
const user = await auth.api.signInEmail({
|
|
73
|
+
body: testUser,
|
|
74
|
+
});
|
|
75
|
+
expect(user).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should get session", async () => {
|
|
79
|
+
const headers = new Headers();
|
|
80
|
+
await client.signIn.email(
|
|
81
|
+
{
|
|
82
|
+
email: testUser.email,
|
|
83
|
+
password: testUser.password,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
onSuccess: sessionSetter(headers),
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
const { data: session } = await client.getSession({
|
|
90
|
+
fetchOptions: { headers },
|
|
91
|
+
});
|
|
92
|
+
expect(session?.user).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { afterAll } from "bun:test";
|
|
2
|
+
import type { SuccessContext } from "@better-fetch/fetch";
|
|
3
|
+
import { betterAuth } from "better-auth";
|
|
4
|
+
import { createAuthClient } from "better-auth/client";
|
|
5
|
+
import { parseSetCookieHeader } from "better-auth/cookies";
|
|
6
|
+
import { getAdapter } from "better-auth/db";
|
|
7
|
+
import type {
|
|
8
|
+
BetterAuthOptions,
|
|
9
|
+
ClientOptions,
|
|
10
|
+
Session,
|
|
11
|
+
User,
|
|
12
|
+
} from "better-auth/types";
|
|
13
|
+
|
|
14
|
+
import { getBaseURL } from "../utlis/getBaseURL";
|
|
15
|
+
|
|
16
|
+
import { surrealAdapter } from "..";
|
|
17
|
+
import { getDatabase } from "../db/surreal";
|
|
18
|
+
|
|
19
|
+
export async function getTestInstance<
|
|
20
|
+
O extends Partial<BetterAuthOptions>,
|
|
21
|
+
C extends ClientOptions,
|
|
22
|
+
>(
|
|
23
|
+
options?: O,
|
|
24
|
+
config?: {
|
|
25
|
+
clientOptions?: C;
|
|
26
|
+
port?: number;
|
|
27
|
+
disableTestUser?: boolean;
|
|
28
|
+
testUser?: Partial<User>;
|
|
29
|
+
testWith?: "surreal";
|
|
30
|
+
},
|
|
31
|
+
) {
|
|
32
|
+
const db = await getDatabase({
|
|
33
|
+
url: "http://127.0.0.1:8000/rpc",
|
|
34
|
+
namespace: "better_auth_test",
|
|
35
|
+
database: "better_auth_test",
|
|
36
|
+
auth: { username: "root", password: "root" },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const opts = {
|
|
40
|
+
socialProviders: {
|
|
41
|
+
github: {
|
|
42
|
+
clientId: "test",
|
|
43
|
+
clientSecret: "test",
|
|
44
|
+
},
|
|
45
|
+
google: {
|
|
46
|
+
clientId: "test",
|
|
47
|
+
clientSecret: "test",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
secret: "better-auth.secret",
|
|
51
|
+
database: surrealAdapter(db),
|
|
52
|
+
emailAndPassword: {
|
|
53
|
+
enabled: true,
|
|
54
|
+
},
|
|
55
|
+
rateLimit: {
|
|
56
|
+
enabled: false,
|
|
57
|
+
},
|
|
58
|
+
advanced: {
|
|
59
|
+
cookies: {},
|
|
60
|
+
},
|
|
61
|
+
} satisfies BetterAuthOptions;
|
|
62
|
+
|
|
63
|
+
const auth = betterAuth({
|
|
64
|
+
baseURL: `http://localhost:${config?.port || 3000}`,
|
|
65
|
+
...opts,
|
|
66
|
+
...options,
|
|
67
|
+
advanced: {
|
|
68
|
+
disableCSRFCheck: true,
|
|
69
|
+
...options?.advanced,
|
|
70
|
+
},
|
|
71
|
+
} as O extends undefined ? typeof opts : O & typeof opts);
|
|
72
|
+
|
|
73
|
+
const testUser = {
|
|
74
|
+
email: "test@test.com",
|
|
75
|
+
password: "test123456",
|
|
76
|
+
name: "test user",
|
|
77
|
+
...config?.testUser,
|
|
78
|
+
};
|
|
79
|
+
async function createTestUser() {
|
|
80
|
+
if (config?.disableTestUser) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
//@ts-expect-error
|
|
84
|
+
await auth.api.signUpEmail({
|
|
85
|
+
body: testUser,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await createTestUser();
|
|
90
|
+
|
|
91
|
+
afterAll(async () => {
|
|
92
|
+
await db.query("DELETE account; DELETE session; DELETE user;");
|
|
93
|
+
return;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
async function signInWithTestUser() {
|
|
97
|
+
if (config?.disableTestUser) {
|
|
98
|
+
throw new Error("Test user is disabled");
|
|
99
|
+
}
|
|
100
|
+
const headers = new Headers();
|
|
101
|
+
const setCookie = (name: string, value: string) => {
|
|
102
|
+
const current = headers.get("cookie");
|
|
103
|
+
headers.set("cookie", `${current || ""}; ${name}=${value}`);
|
|
104
|
+
};
|
|
105
|
+
//@ts-expect-error
|
|
106
|
+
const { data } = await client.signIn.email({
|
|
107
|
+
email: testUser.email,
|
|
108
|
+
password: testUser.password,
|
|
109
|
+
|
|
110
|
+
fetchOptions: {
|
|
111
|
+
// @ts-expect-error
|
|
112
|
+
onSuccess(context) {
|
|
113
|
+
const header = context.response.headers.get("set-cookie");
|
|
114
|
+
const cookies = parseSetCookieHeader(header || "");
|
|
115
|
+
const signedCookie = cookies.get("better-auth.session_token")?.value;
|
|
116
|
+
headers.set("cookie", `better-auth.session_token=${signedCookie}`);
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
session: data.session as Session,
|
|
122
|
+
user: data.user as User,
|
|
123
|
+
headers,
|
|
124
|
+
setCookie,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function signInWithUser(email: string, password: string) {
|
|
128
|
+
const headers = new Headers();
|
|
129
|
+
//@ts-expect-error
|
|
130
|
+
const { data } = await client.signIn.email({
|
|
131
|
+
email,
|
|
132
|
+
password,
|
|
133
|
+
fetchOptions: {
|
|
134
|
+
//@ts-expect-error
|
|
135
|
+
onSuccess(context) {
|
|
136
|
+
const header = context.response.headers.get("set-cookie");
|
|
137
|
+
const cookies = parseSetCookieHeader(header || "");
|
|
138
|
+
const signedCookie = cookies.get("better-auth.session_token")?.value;
|
|
139
|
+
headers.set("cookie", `better-auth.session_token=${signedCookie}`);
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
res: data as {
|
|
145
|
+
user: User;
|
|
146
|
+
session: Session;
|
|
147
|
+
},
|
|
148
|
+
headers,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const customFetchImpl = async (
|
|
153
|
+
url: string | URL | Request,
|
|
154
|
+
init?: RequestInit,
|
|
155
|
+
) => {
|
|
156
|
+
const req = new Request(url.toString(), init);
|
|
157
|
+
return auth.handler(req);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
function sessionSetter(headers: Headers) {
|
|
161
|
+
return (context: SuccessContext) => {
|
|
162
|
+
const header = context.response.headers.get("set-cookie");
|
|
163
|
+
if (header) {
|
|
164
|
+
const cookies = parseSetCookieHeader(header || "");
|
|
165
|
+
const signedCookie = cookies.get("better-auth.session_token")?.value;
|
|
166
|
+
headers.set("cookie", `better-auth.session_token=${signedCookie}`);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function cookieSetter(headers: Headers) {
|
|
171
|
+
return (context: { response: Response }) => {
|
|
172
|
+
const setCookieHeader = context.response.headers.get("set-cookie");
|
|
173
|
+
if (!setCookieHeader) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const cookieMap = new Map<string, string>();
|
|
178
|
+
|
|
179
|
+
const existingCookiesHeader = headers.get("cookie") || "";
|
|
180
|
+
existingCookiesHeader.split(";").forEach((cookie) => {
|
|
181
|
+
const [name, ...rest] = cookie.trim().split("=");
|
|
182
|
+
if (name && rest.length > 0) {
|
|
183
|
+
cookieMap.set(name, rest.join("="));
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const setCookieHeaders = setCookieHeader.split(",");
|
|
188
|
+
setCookieHeaders.forEach((header) => {
|
|
189
|
+
const cookies = parseSetCookieHeader(header);
|
|
190
|
+
cookies.forEach((value, name) => {
|
|
191
|
+
cookieMap.set(name, value.value);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const updatedCookies = Array.from(cookieMap.entries())
|
|
196
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
197
|
+
.join("; ");
|
|
198
|
+
headers.set("cookie", updatedCookies);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const client = createAuthClient({
|
|
202
|
+
...(config?.clientOptions as C extends undefined ? {} : C),
|
|
203
|
+
baseURL: getBaseURL(
|
|
204
|
+
options?.baseURL || "http://localhost:" + (config?.port || 3000),
|
|
205
|
+
options?.basePath || "/api/auth",
|
|
206
|
+
),
|
|
207
|
+
fetchOptions: {
|
|
208
|
+
customFetchImpl,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
auth,
|
|
213
|
+
client,
|
|
214
|
+
testUser,
|
|
215
|
+
signInWithTestUser,
|
|
216
|
+
signInWithUser,
|
|
217
|
+
cookieSetter,
|
|
218
|
+
customFetchImpl,
|
|
219
|
+
sessionSetter,
|
|
220
|
+
db: await getAdapter(auth.options),
|
|
221
|
+
};
|
|
222
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BetterAuthError } from "better-auth";
|
|
2
|
+
|
|
3
|
+
function checkHasPath(url: string): boolean {
|
|
4
|
+
try {
|
|
5
|
+
const parsedUrl = new URL(url);
|
|
6
|
+
return parsedUrl.pathname !== "/";
|
|
7
|
+
} catch (error) {
|
|
8
|
+
throw new BetterAuthError(
|
|
9
|
+
`Invalid base URL: ${url}. Please provide a valid base URL.`,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function withPath(url: string, path = "/api/auth") {
|
|
15
|
+
const hasPath = checkHasPath(url);
|
|
16
|
+
if (hasPath) {
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
path = path.startsWith("/") ? path : `/${path}`;
|
|
20
|
+
return `${url}${path}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getBaseURL(url?: string, path?: string) {
|
|
24
|
+
if (url) {
|
|
25
|
+
return withPath(url, path);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof window !== "undefined" && window.location) {
|
|
29
|
+
return withPath(window.location.origin, path);
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|