@coursebuilder/better-auth 0.1.0-canary.0.0c9beb2ae
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/LICENSE +21 -0
- package/dist/index.cjs +289 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +281 -0
- package/package.json +43 -0
- package/src/index.ts +358 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Skill Recordings Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc2) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc2 = __getOwnPropDesc(from, key)) || desc2.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
courseBuilderBetterAuthAdapter: () => courseBuilderBetterAuthAdapter
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(src_exports);
|
|
26
|
+
var import_adapters = require("better-auth/adapters");
|
|
27
|
+
var import_drizzle_orm = require("drizzle-orm");
|
|
28
|
+
var accountFields = {
|
|
29
|
+
accessToken: "access_token",
|
|
30
|
+
accessTokenExpiresAt: "expires_at",
|
|
31
|
+
idToken: "id_token",
|
|
32
|
+
refreshToken: "refresh_token",
|
|
33
|
+
refreshTokenExpiresAt: "refresh_token_expires_in",
|
|
34
|
+
tokenType: "token_type",
|
|
35
|
+
sessionState: "session_state"
|
|
36
|
+
};
|
|
37
|
+
function accountId(record) {
|
|
38
|
+
return `${String(record.provider)}:${String(record.providerAccountId)}`;
|
|
39
|
+
}
|
|
40
|
+
function toEpochSeconds(value) {
|
|
41
|
+
return value instanceof Date ? Math.floor(value.getTime() / 1e3) : value;
|
|
42
|
+
}
|
|
43
|
+
function fromEpochSeconds(value) {
|
|
44
|
+
return typeof value === "number" ? new Date(value * 1e3) : value;
|
|
45
|
+
}
|
|
46
|
+
function toStorage(model, data) {
|
|
47
|
+
if (model === "session") {
|
|
48
|
+
const { token, expiresAt, ...rest } = data;
|
|
49
|
+
return { ...rest, sessionToken: token, expires: expiresAt };
|
|
50
|
+
}
|
|
51
|
+
if (model === "account") {
|
|
52
|
+
const { id: _id, providerId, accountId: providerAccountId, ...rest } = data;
|
|
53
|
+
return Object.fromEntries(
|
|
54
|
+
Object.entries({ ...rest, provider: providerId, providerAccountId }).filter(([key]) => key !== "createdAt" && key !== "updatedAt").map(([key, value]) => [
|
|
55
|
+
accountFields[key] ?? key,
|
|
56
|
+
key === "accessTokenExpiresAt" || key === "refreshTokenExpiresAt" ? toEpochSeconds(value) : value
|
|
57
|
+
])
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (model === "verification") {
|
|
61
|
+
const { value, expiresAt, id: _id, ...rest } = data;
|
|
62
|
+
return { ...rest, token: value, expires: expiresAt };
|
|
63
|
+
}
|
|
64
|
+
if (model === "user") {
|
|
65
|
+
const { updatedAt: _updatedAt, ...rest } = data;
|
|
66
|
+
return {
|
|
67
|
+
...rest,
|
|
68
|
+
emailVerified: data.emailVerified ? /* @__PURE__ */ new Date() : null
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
function fromStorage(model, data) {
|
|
74
|
+
if (model === "session") {
|
|
75
|
+
const { sessionToken, expires, ...rest } = data;
|
|
76
|
+
return {
|
|
77
|
+
...rest,
|
|
78
|
+
id: sessionToken,
|
|
79
|
+
token: sessionToken,
|
|
80
|
+
expiresAt: expires
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (model === "account") {
|
|
84
|
+
const translated = Object.fromEntries(
|
|
85
|
+
Object.entries(data).map(([key, value]) => {
|
|
86
|
+
const field = Object.entries(accountFields).find(
|
|
87
|
+
([, storage]) => storage === key
|
|
88
|
+
)?.[0] ?? key;
|
|
89
|
+
return [
|
|
90
|
+
field,
|
|
91
|
+
field === "accessTokenExpiresAt" || field === "refreshTokenExpiresAt" ? fromEpochSeconds(value) : value
|
|
92
|
+
];
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
return {
|
|
96
|
+
...translated,
|
|
97
|
+
id: accountId(data),
|
|
98
|
+
providerId: data.provider,
|
|
99
|
+
accountId: data.providerAccountId
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (model === "verification") {
|
|
103
|
+
const { token, expires, ...rest } = data;
|
|
104
|
+
return {
|
|
105
|
+
...rest,
|
|
106
|
+
id: `${String(data.identifier)}:${String(token)}`,
|
|
107
|
+
value: token,
|
|
108
|
+
expiresAt: expires
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (model === "user") {
|
|
112
|
+
return { ...data, emailVerified: data.emailVerified instanceof Date };
|
|
113
|
+
}
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
function storageField(model, field, value) {
|
|
117
|
+
if (model === "session") {
|
|
118
|
+
if (field === "token" || field === "id")
|
|
119
|
+
return ["sessionToken", value];
|
|
120
|
+
if (field === "expiresAt")
|
|
121
|
+
return ["expires", value];
|
|
122
|
+
}
|
|
123
|
+
if (model === "account") {
|
|
124
|
+
if (field === "providerId")
|
|
125
|
+
return ["provider", value];
|
|
126
|
+
if (field === "accountId")
|
|
127
|
+
return ["providerAccountId", value];
|
|
128
|
+
if (field === "id" && typeof value === "string") {
|
|
129
|
+
const separator = value.indexOf(":");
|
|
130
|
+
return separator < 0 ? ["providerAccountId", value] : ["providerAccountId", value.slice(separator + 1)];
|
|
131
|
+
}
|
|
132
|
+
return [accountFields[field] ?? field, value];
|
|
133
|
+
}
|
|
134
|
+
if (model === "verification") {
|
|
135
|
+
if (field === "value")
|
|
136
|
+
return ["token", value];
|
|
137
|
+
if (field === "expiresAt")
|
|
138
|
+
return ["expires", value];
|
|
139
|
+
}
|
|
140
|
+
if (model === "user" && field === "emailVerified" && typeof value === "boolean") {
|
|
141
|
+
return ["emailVerified", value ? /* @__PURE__ */ new Date(0) : null];
|
|
142
|
+
}
|
|
143
|
+
return [field, value];
|
|
144
|
+
}
|
|
145
|
+
function predicate(table, model, where) {
|
|
146
|
+
const expressions = where.map((condition) => {
|
|
147
|
+
if (model === "account" && condition.field === "id" && typeof condition.value === "string" && condition.value.includes(":")) {
|
|
148
|
+
const separator = condition.value.indexOf(":");
|
|
149
|
+
const provider = table["provider"];
|
|
150
|
+
const providerAccountId = table["providerAccountId"];
|
|
151
|
+
return (0, import_drizzle_orm.and)(
|
|
152
|
+
(0, import_drizzle_orm.eq)(provider, condition.value.slice(0, separator)),
|
|
153
|
+
(0, import_drizzle_orm.eq)(providerAccountId, condition.value.slice(separator + 1))
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const [field, value] = storageField(model, condition.field, condition.value);
|
|
157
|
+
const column = table[field];
|
|
158
|
+
switch (condition.operator) {
|
|
159
|
+
case "ne":
|
|
160
|
+
return (0, import_drizzle_orm.ne)(column, value);
|
|
161
|
+
case "lt":
|
|
162
|
+
return (0, import_drizzle_orm.lt)(column, value);
|
|
163
|
+
case "lte":
|
|
164
|
+
return (0, import_drizzle_orm.lte)(column, value);
|
|
165
|
+
case "gt":
|
|
166
|
+
return (0, import_drizzle_orm.gt)(column, value);
|
|
167
|
+
case "gte":
|
|
168
|
+
return (0, import_drizzle_orm.gte)(column, value);
|
|
169
|
+
case "in":
|
|
170
|
+
return (0, import_drizzle_orm.inArray)(column, value);
|
|
171
|
+
case "not_in":
|
|
172
|
+
return (0, import_drizzle_orm.notInArray)(column, value);
|
|
173
|
+
case "contains":
|
|
174
|
+
return (0, import_drizzle_orm.like)(column, `%${String(value)}%`);
|
|
175
|
+
case "starts_with":
|
|
176
|
+
return (0, import_drizzle_orm.like)(column, `${String(value)}%`);
|
|
177
|
+
case "ends_with":
|
|
178
|
+
return (0, import_drizzle_orm.like)(column, `%${String(value)}`);
|
|
179
|
+
default:
|
|
180
|
+
return (0, import_drizzle_orm.eq)(column, value);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
if (!expressions.length)
|
|
184
|
+
return void 0;
|
|
185
|
+
const groups = [];
|
|
186
|
+
let current = [];
|
|
187
|
+
for (let index = 0; index < expressions.length; index++) {
|
|
188
|
+
current.push(expressions[index]);
|
|
189
|
+
if (where[index]?.connector === "OR") {
|
|
190
|
+
groups.push((0, import_drizzle_orm.and)(...current));
|
|
191
|
+
current = [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (current.length)
|
|
195
|
+
groups.push((0, import_drizzle_orm.and)(...current));
|
|
196
|
+
return groups.length === 1 ? groups[0] : (0, import_drizzle_orm.or)(...groups);
|
|
197
|
+
}
|
|
198
|
+
function courseBuilderBetterAuthAdapter(options) {
|
|
199
|
+
const { db, tables } = options;
|
|
200
|
+
return (0, import_adapters.createAdapterFactory)({
|
|
201
|
+
config: {
|
|
202
|
+
adapterId: "coursebuilder",
|
|
203
|
+
adapterName: "Course Builder",
|
|
204
|
+
debugLogs: options.debugLogs ?? false,
|
|
205
|
+
supportsDates: true,
|
|
206
|
+
supportsBooleans: true,
|
|
207
|
+
supportsJSON: true,
|
|
208
|
+
supportsNumericIds: false,
|
|
209
|
+
transaction: false
|
|
210
|
+
},
|
|
211
|
+
adapter: ({ getDefaultModelName }) => {
|
|
212
|
+
const resolve = (model) => {
|
|
213
|
+
const canonical = getDefaultModelName(model);
|
|
214
|
+
const table = tables[canonical];
|
|
215
|
+
if (!table)
|
|
216
|
+
throw new Error(`Unsupported Better Auth model: ${model}`);
|
|
217
|
+
return { canonical, table };
|
|
218
|
+
};
|
|
219
|
+
return {
|
|
220
|
+
create: async ({ model, data }) => {
|
|
221
|
+
const { canonical, table } = resolve(model);
|
|
222
|
+
await db.insert(table).values(toStorage(canonical, data));
|
|
223
|
+
return data;
|
|
224
|
+
},
|
|
225
|
+
findOne: async ({ model, where, join }) => {
|
|
226
|
+
const { canonical, table } = resolve(model);
|
|
227
|
+
const [row] = await db.select().from(table).where(predicate(table, canonical, where)).limit(1);
|
|
228
|
+
if (!row)
|
|
229
|
+
return null;
|
|
230
|
+
const result = fromStorage(canonical, row);
|
|
231
|
+
if (canonical === "session" && join?.user) {
|
|
232
|
+
const userId = tables.user["id"];
|
|
233
|
+
const [user] = await db.select().from(tables.user).where((0, import_drizzle_orm.eq)(userId, row.userId)).limit(1);
|
|
234
|
+
if (user)
|
|
235
|
+
result.user = fromStorage("user", user);
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
},
|
|
239
|
+
findMany: async ({ model, where, limit, offset, sortBy }) => {
|
|
240
|
+
const { canonical, table } = resolve(model);
|
|
241
|
+
let query = db.select().from(table).$dynamic();
|
|
242
|
+
const filter = where ? predicate(table, canonical, where) : void 0;
|
|
243
|
+
if (filter)
|
|
244
|
+
query = query.where(filter);
|
|
245
|
+
if (sortBy) {
|
|
246
|
+
const [field] = storageField(canonical, sortBy.field, null);
|
|
247
|
+
const column = table[field];
|
|
248
|
+
query = query.orderBy(
|
|
249
|
+
sortBy.direction === "asc" ? (0, import_drizzle_orm.asc)(column) : (0, import_drizzle_orm.desc)(column)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const rows = await query.limit(limit).offset(offset ?? 0);
|
|
253
|
+
return rows.map((row) => fromStorage(canonical, row));
|
|
254
|
+
},
|
|
255
|
+
update: async ({ model, where, update }) => {
|
|
256
|
+
const { canonical, table } = resolve(model);
|
|
257
|
+
await db.update(table).set(
|
|
258
|
+
toStorage(canonical, update)
|
|
259
|
+
).where(predicate(table, canonical, where));
|
|
260
|
+
const [row] = await db.select().from(table).where(predicate(table, canonical, where)).limit(1);
|
|
261
|
+
return row ? fromStorage(canonical, row) : null;
|
|
262
|
+
},
|
|
263
|
+
updateMany: async ({ model, where, update }) => {
|
|
264
|
+
const { canonical, table } = resolve(model);
|
|
265
|
+
const result = await db.update(table).set(toStorage(canonical, update)).where(predicate(table, canonical, where));
|
|
266
|
+
return Number(result[0].affectedRows);
|
|
267
|
+
},
|
|
268
|
+
delete: async ({ model, where }) => {
|
|
269
|
+
const { canonical, table } = resolve(model);
|
|
270
|
+
await db.delete(table).where(predicate(table, canonical, where));
|
|
271
|
+
},
|
|
272
|
+
deleteMany: async ({ model, where }) => {
|
|
273
|
+
const { canonical, table } = resolve(model);
|
|
274
|
+
const result = await db.delete(table).where(predicate(table, canonical, where));
|
|
275
|
+
return Number(result[0].affectedRows);
|
|
276
|
+
},
|
|
277
|
+
count: async ({ model, where }) => {
|
|
278
|
+
const { canonical, table } = resolve(model);
|
|
279
|
+
const [result] = await db.select({ count: (0, import_drizzle_orm.count)() }).from(table).where(where ? predicate(table, canonical, where) : void 0);
|
|
280
|
+
return Number(result?.count ?? 0);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
287
|
+
0 && (module.exports = {
|
|
288
|
+
courseBuilderBetterAuthAdapter
|
|
289
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DBAdapterDebugLogOption, createAdapterFactory } from 'better-auth/adapters';
|
|
2
|
+
import { MySqlDatabase, MySqlTable } from 'drizzle-orm/mysql-core';
|
|
3
|
+
|
|
4
|
+
type CourseBuilderBetterAuthTables = {
|
|
5
|
+
user: MySqlTable;
|
|
6
|
+
session: MySqlTable;
|
|
7
|
+
account: MySqlTable;
|
|
8
|
+
verification: MySqlTable;
|
|
9
|
+
};
|
|
10
|
+
type CourseBuilderBetterAuthAdapterOptions = {
|
|
11
|
+
db: InstanceType<typeof MySqlDatabase>;
|
|
12
|
+
tables: CourseBuilderBetterAuthTables;
|
|
13
|
+
debugLogs?: DBAdapterDebugLogOption;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Creates a Better Auth database adapter over Course Builder's existing MySQL
|
|
17
|
+
* auth tables. This is a translation boundary only and never creates schema.
|
|
18
|
+
*
|
|
19
|
+
* @param options - Existing Drizzle database and canonical CB auth tables.
|
|
20
|
+
* @returns A Better Auth database adapter factory.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const database = courseBuilderBetterAuthAdapter({ db, tables })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare function courseBuilderBetterAuthAdapter(options: CourseBuilderBetterAuthAdapterOptions): ReturnType<typeof createAdapterFactory>;
|
|
28
|
+
|
|
29
|
+
export { type CourseBuilderBetterAuthAdapterOptions, type CourseBuilderBetterAuthTables, courseBuilderBetterAuthAdapter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DBAdapterDebugLogOption, createAdapterFactory } from 'better-auth/adapters';
|
|
2
|
+
import { MySqlDatabase, MySqlTable } from 'drizzle-orm/mysql-core';
|
|
3
|
+
|
|
4
|
+
type CourseBuilderBetterAuthTables = {
|
|
5
|
+
user: MySqlTable;
|
|
6
|
+
session: MySqlTable;
|
|
7
|
+
account: MySqlTable;
|
|
8
|
+
verification: MySqlTable;
|
|
9
|
+
};
|
|
10
|
+
type CourseBuilderBetterAuthAdapterOptions = {
|
|
11
|
+
db: InstanceType<typeof MySqlDatabase>;
|
|
12
|
+
tables: CourseBuilderBetterAuthTables;
|
|
13
|
+
debugLogs?: DBAdapterDebugLogOption;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Creates a Better Auth database adapter over Course Builder's existing MySQL
|
|
17
|
+
* auth tables. This is a translation boundary only and never creates schema.
|
|
18
|
+
*
|
|
19
|
+
* @param options - Existing Drizzle database and canonical CB auth tables.
|
|
20
|
+
* @returns A Better Auth database adapter factory.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const database = courseBuilderBetterAuthAdapter({ db, tables })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare function courseBuilderBetterAuthAdapter(options: CourseBuilderBetterAuthAdapterOptions): ReturnType<typeof createAdapterFactory>;
|
|
28
|
+
|
|
29
|
+
export { type CourseBuilderBetterAuthAdapterOptions, type CourseBuilderBetterAuthTables, courseBuilderBetterAuthAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
createAdapterFactory
|
|
4
|
+
} from "better-auth/adapters";
|
|
5
|
+
import {
|
|
6
|
+
and,
|
|
7
|
+
asc,
|
|
8
|
+
count,
|
|
9
|
+
desc,
|
|
10
|
+
eq,
|
|
11
|
+
gt,
|
|
12
|
+
gte,
|
|
13
|
+
inArray,
|
|
14
|
+
like,
|
|
15
|
+
lt,
|
|
16
|
+
lte,
|
|
17
|
+
ne,
|
|
18
|
+
notInArray,
|
|
19
|
+
or
|
|
20
|
+
} from "drizzle-orm";
|
|
21
|
+
var accountFields = {
|
|
22
|
+
accessToken: "access_token",
|
|
23
|
+
accessTokenExpiresAt: "expires_at",
|
|
24
|
+
idToken: "id_token",
|
|
25
|
+
refreshToken: "refresh_token",
|
|
26
|
+
refreshTokenExpiresAt: "refresh_token_expires_in",
|
|
27
|
+
tokenType: "token_type",
|
|
28
|
+
sessionState: "session_state"
|
|
29
|
+
};
|
|
30
|
+
function accountId(record) {
|
|
31
|
+
return `${String(record.provider)}:${String(record.providerAccountId)}`;
|
|
32
|
+
}
|
|
33
|
+
function toEpochSeconds(value) {
|
|
34
|
+
return value instanceof Date ? Math.floor(value.getTime() / 1e3) : value;
|
|
35
|
+
}
|
|
36
|
+
function fromEpochSeconds(value) {
|
|
37
|
+
return typeof value === "number" ? new Date(value * 1e3) : value;
|
|
38
|
+
}
|
|
39
|
+
function toStorage(model, data) {
|
|
40
|
+
if (model === "session") {
|
|
41
|
+
const { token, expiresAt, ...rest } = data;
|
|
42
|
+
return { ...rest, sessionToken: token, expires: expiresAt };
|
|
43
|
+
}
|
|
44
|
+
if (model === "account") {
|
|
45
|
+
const { id: _id, providerId, accountId: providerAccountId, ...rest } = data;
|
|
46
|
+
return Object.fromEntries(
|
|
47
|
+
Object.entries({ ...rest, provider: providerId, providerAccountId }).filter(([key]) => key !== "createdAt" && key !== "updatedAt").map(([key, value]) => [
|
|
48
|
+
accountFields[key] ?? key,
|
|
49
|
+
key === "accessTokenExpiresAt" || key === "refreshTokenExpiresAt" ? toEpochSeconds(value) : value
|
|
50
|
+
])
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (model === "verification") {
|
|
54
|
+
const { value, expiresAt, id: _id, ...rest } = data;
|
|
55
|
+
return { ...rest, token: value, expires: expiresAt };
|
|
56
|
+
}
|
|
57
|
+
if (model === "user") {
|
|
58
|
+
const { updatedAt: _updatedAt, ...rest } = data;
|
|
59
|
+
return {
|
|
60
|
+
...rest,
|
|
61
|
+
emailVerified: data.emailVerified ? /* @__PURE__ */ new Date() : null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
function fromStorage(model, data) {
|
|
67
|
+
if (model === "session") {
|
|
68
|
+
const { sessionToken, expires, ...rest } = data;
|
|
69
|
+
return {
|
|
70
|
+
...rest,
|
|
71
|
+
id: sessionToken,
|
|
72
|
+
token: sessionToken,
|
|
73
|
+
expiresAt: expires
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (model === "account") {
|
|
77
|
+
const translated = Object.fromEntries(
|
|
78
|
+
Object.entries(data).map(([key, value]) => {
|
|
79
|
+
const field = Object.entries(accountFields).find(
|
|
80
|
+
([, storage]) => storage === key
|
|
81
|
+
)?.[0] ?? key;
|
|
82
|
+
return [
|
|
83
|
+
field,
|
|
84
|
+
field === "accessTokenExpiresAt" || field === "refreshTokenExpiresAt" ? fromEpochSeconds(value) : value
|
|
85
|
+
];
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
return {
|
|
89
|
+
...translated,
|
|
90
|
+
id: accountId(data),
|
|
91
|
+
providerId: data.provider,
|
|
92
|
+
accountId: data.providerAccountId
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (model === "verification") {
|
|
96
|
+
const { token, expires, ...rest } = data;
|
|
97
|
+
return {
|
|
98
|
+
...rest,
|
|
99
|
+
id: `${String(data.identifier)}:${String(token)}`,
|
|
100
|
+
value: token,
|
|
101
|
+
expiresAt: expires
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (model === "user") {
|
|
105
|
+
return { ...data, emailVerified: data.emailVerified instanceof Date };
|
|
106
|
+
}
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
function storageField(model, field, value) {
|
|
110
|
+
if (model === "session") {
|
|
111
|
+
if (field === "token" || field === "id")
|
|
112
|
+
return ["sessionToken", value];
|
|
113
|
+
if (field === "expiresAt")
|
|
114
|
+
return ["expires", value];
|
|
115
|
+
}
|
|
116
|
+
if (model === "account") {
|
|
117
|
+
if (field === "providerId")
|
|
118
|
+
return ["provider", value];
|
|
119
|
+
if (field === "accountId")
|
|
120
|
+
return ["providerAccountId", value];
|
|
121
|
+
if (field === "id" && typeof value === "string") {
|
|
122
|
+
const separator = value.indexOf(":");
|
|
123
|
+
return separator < 0 ? ["providerAccountId", value] : ["providerAccountId", value.slice(separator + 1)];
|
|
124
|
+
}
|
|
125
|
+
return [accountFields[field] ?? field, value];
|
|
126
|
+
}
|
|
127
|
+
if (model === "verification") {
|
|
128
|
+
if (field === "value")
|
|
129
|
+
return ["token", value];
|
|
130
|
+
if (field === "expiresAt")
|
|
131
|
+
return ["expires", value];
|
|
132
|
+
}
|
|
133
|
+
if (model === "user" && field === "emailVerified" && typeof value === "boolean") {
|
|
134
|
+
return ["emailVerified", value ? /* @__PURE__ */ new Date(0) : null];
|
|
135
|
+
}
|
|
136
|
+
return [field, value];
|
|
137
|
+
}
|
|
138
|
+
function predicate(table, model, where) {
|
|
139
|
+
const expressions = where.map((condition) => {
|
|
140
|
+
if (model === "account" && condition.field === "id" && typeof condition.value === "string" && condition.value.includes(":")) {
|
|
141
|
+
const separator = condition.value.indexOf(":");
|
|
142
|
+
const provider = table["provider"];
|
|
143
|
+
const providerAccountId = table["providerAccountId"];
|
|
144
|
+
return and(
|
|
145
|
+
eq(provider, condition.value.slice(0, separator)),
|
|
146
|
+
eq(providerAccountId, condition.value.slice(separator + 1))
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const [field, value] = storageField(model, condition.field, condition.value);
|
|
150
|
+
const column = table[field];
|
|
151
|
+
switch (condition.operator) {
|
|
152
|
+
case "ne":
|
|
153
|
+
return ne(column, value);
|
|
154
|
+
case "lt":
|
|
155
|
+
return lt(column, value);
|
|
156
|
+
case "lte":
|
|
157
|
+
return lte(column, value);
|
|
158
|
+
case "gt":
|
|
159
|
+
return gt(column, value);
|
|
160
|
+
case "gte":
|
|
161
|
+
return gte(column, value);
|
|
162
|
+
case "in":
|
|
163
|
+
return inArray(column, value);
|
|
164
|
+
case "not_in":
|
|
165
|
+
return notInArray(column, value);
|
|
166
|
+
case "contains":
|
|
167
|
+
return like(column, `%${String(value)}%`);
|
|
168
|
+
case "starts_with":
|
|
169
|
+
return like(column, `${String(value)}%`);
|
|
170
|
+
case "ends_with":
|
|
171
|
+
return like(column, `%${String(value)}`);
|
|
172
|
+
default:
|
|
173
|
+
return eq(column, value);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
if (!expressions.length)
|
|
177
|
+
return void 0;
|
|
178
|
+
const groups = [];
|
|
179
|
+
let current = [];
|
|
180
|
+
for (let index = 0; index < expressions.length; index++) {
|
|
181
|
+
current.push(expressions[index]);
|
|
182
|
+
if (where[index]?.connector === "OR") {
|
|
183
|
+
groups.push(and(...current));
|
|
184
|
+
current = [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (current.length)
|
|
188
|
+
groups.push(and(...current));
|
|
189
|
+
return groups.length === 1 ? groups[0] : or(...groups);
|
|
190
|
+
}
|
|
191
|
+
function courseBuilderBetterAuthAdapter(options) {
|
|
192
|
+
const { db, tables } = options;
|
|
193
|
+
return createAdapterFactory({
|
|
194
|
+
config: {
|
|
195
|
+
adapterId: "coursebuilder",
|
|
196
|
+
adapterName: "Course Builder",
|
|
197
|
+
debugLogs: options.debugLogs ?? false,
|
|
198
|
+
supportsDates: true,
|
|
199
|
+
supportsBooleans: true,
|
|
200
|
+
supportsJSON: true,
|
|
201
|
+
supportsNumericIds: false,
|
|
202
|
+
transaction: false
|
|
203
|
+
},
|
|
204
|
+
adapter: ({ getDefaultModelName }) => {
|
|
205
|
+
const resolve = (model) => {
|
|
206
|
+
const canonical = getDefaultModelName(model);
|
|
207
|
+
const table = tables[canonical];
|
|
208
|
+
if (!table)
|
|
209
|
+
throw new Error(`Unsupported Better Auth model: ${model}`);
|
|
210
|
+
return { canonical, table };
|
|
211
|
+
};
|
|
212
|
+
return {
|
|
213
|
+
create: async ({ model, data }) => {
|
|
214
|
+
const { canonical, table } = resolve(model);
|
|
215
|
+
await db.insert(table).values(toStorage(canonical, data));
|
|
216
|
+
return data;
|
|
217
|
+
},
|
|
218
|
+
findOne: async ({ model, where, join }) => {
|
|
219
|
+
const { canonical, table } = resolve(model);
|
|
220
|
+
const [row] = await db.select().from(table).where(predicate(table, canonical, where)).limit(1);
|
|
221
|
+
if (!row)
|
|
222
|
+
return null;
|
|
223
|
+
const result = fromStorage(canonical, row);
|
|
224
|
+
if (canonical === "session" && join?.user) {
|
|
225
|
+
const userId = tables.user["id"];
|
|
226
|
+
const [user] = await db.select().from(tables.user).where(eq(userId, row.userId)).limit(1);
|
|
227
|
+
if (user)
|
|
228
|
+
result.user = fromStorage("user", user);
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
},
|
|
232
|
+
findMany: async ({ model, where, limit, offset, sortBy }) => {
|
|
233
|
+
const { canonical, table } = resolve(model);
|
|
234
|
+
let query = db.select().from(table).$dynamic();
|
|
235
|
+
const filter = where ? predicate(table, canonical, where) : void 0;
|
|
236
|
+
if (filter)
|
|
237
|
+
query = query.where(filter);
|
|
238
|
+
if (sortBy) {
|
|
239
|
+
const [field] = storageField(canonical, sortBy.field, null);
|
|
240
|
+
const column = table[field];
|
|
241
|
+
query = query.orderBy(
|
|
242
|
+
sortBy.direction === "asc" ? asc(column) : desc(column)
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const rows = await query.limit(limit).offset(offset ?? 0);
|
|
246
|
+
return rows.map((row) => fromStorage(canonical, row));
|
|
247
|
+
},
|
|
248
|
+
update: async ({ model, where, update }) => {
|
|
249
|
+
const { canonical, table } = resolve(model);
|
|
250
|
+
await db.update(table).set(
|
|
251
|
+
toStorage(canonical, update)
|
|
252
|
+
).where(predicate(table, canonical, where));
|
|
253
|
+
const [row] = await db.select().from(table).where(predicate(table, canonical, where)).limit(1);
|
|
254
|
+
return row ? fromStorage(canonical, row) : null;
|
|
255
|
+
},
|
|
256
|
+
updateMany: async ({ model, where, update }) => {
|
|
257
|
+
const { canonical, table } = resolve(model);
|
|
258
|
+
const result = await db.update(table).set(toStorage(canonical, update)).where(predicate(table, canonical, where));
|
|
259
|
+
return Number(result[0].affectedRows);
|
|
260
|
+
},
|
|
261
|
+
delete: async ({ model, where }) => {
|
|
262
|
+
const { canonical, table } = resolve(model);
|
|
263
|
+
await db.delete(table).where(predicate(table, canonical, where));
|
|
264
|
+
},
|
|
265
|
+
deleteMany: async ({ model, where }) => {
|
|
266
|
+
const { canonical, table } = resolve(model);
|
|
267
|
+
const result = await db.delete(table).where(predicate(table, canonical, where));
|
|
268
|
+
return Number(result[0].affectedRows);
|
|
269
|
+
},
|
|
270
|
+
count: async ({ model, where }) => {
|
|
271
|
+
const { canonical, table } = resolve(model);
|
|
272
|
+
const [result] = await db.select({ count: count() }).from(table).where(where ? predicate(table, canonical, where) : void 0);
|
|
273
|
+
return Number(result?.count ?? 0);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
export {
|
|
280
|
+
courseBuilderBetterAuthAdapter
|
|
281
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coursebuilder/better-auth",
|
|
3
|
+
"version": "0.1.0-canary.0.0c9beb2ae",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Better Auth database adapter for existing Course Builder MySQL tables",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"require": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"better-auth": "1.6.23",
|
|
25
|
+
"drizzle-orm": "0.36.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"drizzle-kit": "0.27.2",
|
|
29
|
+
"mysql2": "^3.6.1",
|
|
30
|
+
"tsup": "8.0.2",
|
|
31
|
+
"tsx": "^4.7.1",
|
|
32
|
+
"typescript": "5.4.5",
|
|
33
|
+
"vitest": "1.6.0",
|
|
34
|
+
"@coursebuilder/adapter-drizzle": "3.0.0-canary.0.0c9beb2ae"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
38
|
+
"test": "pnpm test:mysql",
|
|
39
|
+
"test:mysql": "bash test/mysql/test.sh",
|
|
40
|
+
"test:mysql:ci": "bash test/mysql/test.sh ci",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAdapterFactory,
|
|
3
|
+
type DBAdapterDebugLogOption,
|
|
4
|
+
} from 'better-auth/adapters'
|
|
5
|
+
import {
|
|
6
|
+
and,
|
|
7
|
+
asc,
|
|
8
|
+
count,
|
|
9
|
+
desc,
|
|
10
|
+
eq,
|
|
11
|
+
gt,
|
|
12
|
+
gte,
|
|
13
|
+
inArray,
|
|
14
|
+
like,
|
|
15
|
+
lt,
|
|
16
|
+
lte,
|
|
17
|
+
ne,
|
|
18
|
+
notInArray,
|
|
19
|
+
or,
|
|
20
|
+
type SQL,
|
|
21
|
+
} from 'drizzle-orm'
|
|
22
|
+
import type { MySqlDatabase, MySqlTable } from 'drizzle-orm/mysql-core'
|
|
23
|
+
|
|
24
|
+
export type CourseBuilderBetterAuthTables = {
|
|
25
|
+
user: MySqlTable
|
|
26
|
+
session: MySqlTable
|
|
27
|
+
account: MySqlTable
|
|
28
|
+
verification: MySqlTable
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type CourseBuilderBetterAuthAdapterOptions = {
|
|
32
|
+
db: InstanceType<typeof MySqlDatabase>
|
|
33
|
+
tables: CourseBuilderBetterAuthTables
|
|
34
|
+
debugLogs?: DBAdapterDebugLogOption
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Where = {
|
|
38
|
+
field: string
|
|
39
|
+
operator: string
|
|
40
|
+
value: unknown
|
|
41
|
+
connector: 'AND' | 'OR'
|
|
42
|
+
mode: 'sensitive' | 'insensitive'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const accountFields: Record<string, string> = {
|
|
46
|
+
accessToken: 'access_token',
|
|
47
|
+
accessTokenExpiresAt: 'expires_at',
|
|
48
|
+
idToken: 'id_token',
|
|
49
|
+
refreshToken: 'refresh_token',
|
|
50
|
+
refreshTokenExpiresAt: 'refresh_token_expires_in',
|
|
51
|
+
tokenType: 'token_type',
|
|
52
|
+
sessionState: 'session_state',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function accountId(record: Record<string, unknown>) {
|
|
56
|
+
return `${String(record.provider)}:${String(record.providerAccountId)}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toEpochSeconds(value: unknown) {
|
|
60
|
+
return value instanceof Date ? Math.floor(value.getTime() / 1000) : value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fromEpochSeconds(value: unknown) {
|
|
64
|
+
return typeof value === 'number' ? new Date(value * 1000) : value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toStorage(model: string, data: Record<string, unknown>) {
|
|
68
|
+
if (model === 'session') {
|
|
69
|
+
const { token, expiresAt, ...rest } = data
|
|
70
|
+
return { ...rest, sessionToken: token, expires: expiresAt }
|
|
71
|
+
}
|
|
72
|
+
if (model === 'account') {
|
|
73
|
+
const { id: _id, providerId, accountId: providerAccountId, ...rest } = data
|
|
74
|
+
return Object.fromEntries(
|
|
75
|
+
Object.entries({ ...rest, provider: providerId, providerAccountId })
|
|
76
|
+
.filter(([key]) => key !== 'createdAt' && key !== 'updatedAt')
|
|
77
|
+
.map(([key, value]) => [
|
|
78
|
+
accountFields[key] ?? key,
|
|
79
|
+
key === 'accessTokenExpiresAt' || key === 'refreshTokenExpiresAt'
|
|
80
|
+
? toEpochSeconds(value)
|
|
81
|
+
: value,
|
|
82
|
+
]),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
if (model === 'verification') {
|
|
86
|
+
const { value, expiresAt, id: _id, ...rest } = data
|
|
87
|
+
return { ...rest, token: value, expires: expiresAt }
|
|
88
|
+
}
|
|
89
|
+
if (model === 'user') {
|
|
90
|
+
const { updatedAt: _updatedAt, ...rest } = data
|
|
91
|
+
return {
|
|
92
|
+
...rest,
|
|
93
|
+
emailVerified: data.emailVerified ? new Date() : null,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return data
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function fromStorage(model: string, data: Record<string, unknown>) {
|
|
100
|
+
if (model === 'session') {
|
|
101
|
+
const { sessionToken, expires, ...rest } = data
|
|
102
|
+
return {
|
|
103
|
+
...rest,
|
|
104
|
+
id: sessionToken,
|
|
105
|
+
token: sessionToken,
|
|
106
|
+
expiresAt: expires,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (model === 'account') {
|
|
110
|
+
const translated = Object.fromEntries(
|
|
111
|
+
Object.entries(data).map(([key, value]) => {
|
|
112
|
+
const field =
|
|
113
|
+
Object.entries(accountFields).find(
|
|
114
|
+
([, storage]) => storage === key,
|
|
115
|
+
)?.[0] ?? key
|
|
116
|
+
return [
|
|
117
|
+
field,
|
|
118
|
+
field === 'accessTokenExpiresAt' || field === 'refreshTokenExpiresAt'
|
|
119
|
+
? fromEpochSeconds(value)
|
|
120
|
+
: value,
|
|
121
|
+
]
|
|
122
|
+
}),
|
|
123
|
+
)
|
|
124
|
+
return {
|
|
125
|
+
...translated,
|
|
126
|
+
id: accountId(data),
|
|
127
|
+
providerId: data.provider,
|
|
128
|
+
accountId: data.providerAccountId,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (model === 'verification') {
|
|
132
|
+
const { token, expires, ...rest } = data
|
|
133
|
+
return {
|
|
134
|
+
...rest,
|
|
135
|
+
id: `${String(data.identifier)}:${String(token)}`,
|
|
136
|
+
value: token,
|
|
137
|
+
expiresAt: expires,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (model === 'user') {
|
|
141
|
+
return { ...data, emailVerified: data.emailVerified instanceof Date }
|
|
142
|
+
}
|
|
143
|
+
return data
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function storageField(model: string, field: string, value: unknown) {
|
|
147
|
+
if (model === 'session') {
|
|
148
|
+
if (field === 'token' || field === 'id')
|
|
149
|
+
return ['sessionToken', value] as const
|
|
150
|
+
if (field === 'expiresAt') return ['expires', value] as const
|
|
151
|
+
}
|
|
152
|
+
if (model === 'account') {
|
|
153
|
+
if (field === 'providerId') return ['provider', value] as const
|
|
154
|
+
if (field === 'accountId') return ['providerAccountId', value] as const
|
|
155
|
+
if (field === 'id' && typeof value === 'string') {
|
|
156
|
+
const separator = value.indexOf(':')
|
|
157
|
+
return separator < 0
|
|
158
|
+
? (['providerAccountId', value] as const)
|
|
159
|
+
: (['providerAccountId', value.slice(separator + 1)] as const)
|
|
160
|
+
}
|
|
161
|
+
return [accountFields[field] ?? field, value] as const
|
|
162
|
+
}
|
|
163
|
+
if (model === 'verification') {
|
|
164
|
+
if (field === 'value') return ['token', value] as const
|
|
165
|
+
if (field === 'expiresAt') return ['expires', value] as const
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
model === 'user' &&
|
|
169
|
+
field === 'emailVerified' &&
|
|
170
|
+
typeof value === 'boolean'
|
|
171
|
+
) {
|
|
172
|
+
return ['emailVerified', value ? new Date(0) : null] as const
|
|
173
|
+
}
|
|
174
|
+
return [field, value] as const
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function predicate(table: MySqlTable, model: string, where: Where[]) {
|
|
178
|
+
const expressions = where.map((condition) => {
|
|
179
|
+
if (
|
|
180
|
+
model === 'account' &&
|
|
181
|
+
condition.field === 'id' &&
|
|
182
|
+
typeof condition.value === 'string' &&
|
|
183
|
+
condition.value.includes(':')
|
|
184
|
+
) {
|
|
185
|
+
const separator = condition.value.indexOf(':')
|
|
186
|
+
const provider = table['provider' as keyof typeof table] as never
|
|
187
|
+
const providerAccountId = table[
|
|
188
|
+
'providerAccountId' as keyof typeof table
|
|
189
|
+
] as never
|
|
190
|
+
return and(
|
|
191
|
+
eq(provider, condition.value.slice(0, separator)),
|
|
192
|
+
eq(providerAccountId, condition.value.slice(separator + 1)),
|
|
193
|
+
)!
|
|
194
|
+
}
|
|
195
|
+
const [field, value] = storageField(model, condition.field, condition.value)
|
|
196
|
+
const column = table[field as keyof typeof table] as never
|
|
197
|
+
switch (condition.operator) {
|
|
198
|
+
case 'ne':
|
|
199
|
+
return ne(column, value as never)
|
|
200
|
+
case 'lt':
|
|
201
|
+
return lt(column, value as never)
|
|
202
|
+
case 'lte':
|
|
203
|
+
return lte(column, value as never)
|
|
204
|
+
case 'gt':
|
|
205
|
+
return gt(column, value as never)
|
|
206
|
+
case 'gte':
|
|
207
|
+
return gte(column, value as never)
|
|
208
|
+
case 'in':
|
|
209
|
+
return inArray(column, value as never[])
|
|
210
|
+
case 'not_in':
|
|
211
|
+
return notInArray(column, value as never[])
|
|
212
|
+
case 'contains':
|
|
213
|
+
return like(column, `%${String(value)}%`)
|
|
214
|
+
case 'starts_with':
|
|
215
|
+
return like(column, `${String(value)}%`)
|
|
216
|
+
case 'ends_with':
|
|
217
|
+
return like(column, `%${String(value)}`)
|
|
218
|
+
default:
|
|
219
|
+
return eq(column, value as never)
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
if (!expressions.length) return undefined
|
|
223
|
+
const groups: SQL[] = []
|
|
224
|
+
let current: SQL[] = []
|
|
225
|
+
for (let index = 0; index < expressions.length; index++) {
|
|
226
|
+
current.push(expressions[index]!)
|
|
227
|
+
if (where[index]?.connector === 'OR') {
|
|
228
|
+
groups.push(and(...current)!)
|
|
229
|
+
current = []
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (current.length) groups.push(and(...current)!)
|
|
233
|
+
return groups.length === 1 ? groups[0] : or(...groups)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Creates a Better Auth database adapter over Course Builder's existing MySQL
|
|
238
|
+
* auth tables. This is a translation boundary only and never creates schema.
|
|
239
|
+
*
|
|
240
|
+
* @param options - Existing Drizzle database and canonical CB auth tables.
|
|
241
|
+
* @returns A Better Auth database adapter factory.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* const database = courseBuilderBetterAuthAdapter({ db, tables })
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
export function courseBuilderBetterAuthAdapter(
|
|
249
|
+
options: CourseBuilderBetterAuthAdapterOptions,
|
|
250
|
+
): ReturnType<typeof createAdapterFactory> {
|
|
251
|
+
const { db, tables } = options
|
|
252
|
+
return createAdapterFactory({
|
|
253
|
+
config: {
|
|
254
|
+
adapterId: 'coursebuilder',
|
|
255
|
+
adapterName: 'Course Builder',
|
|
256
|
+
debugLogs: options.debugLogs ?? false,
|
|
257
|
+
supportsDates: true,
|
|
258
|
+
supportsBooleans: true,
|
|
259
|
+
supportsJSON: true,
|
|
260
|
+
supportsNumericIds: false,
|
|
261
|
+
transaction: false,
|
|
262
|
+
},
|
|
263
|
+
adapter: ({ getDefaultModelName }) => {
|
|
264
|
+
const resolve = (model: string) => {
|
|
265
|
+
const canonical = getDefaultModelName(model) as keyof typeof tables
|
|
266
|
+
const table = tables[canonical]
|
|
267
|
+
if (!table) throw new Error(`Unsupported Better Auth model: ${model}`)
|
|
268
|
+
return { canonical, table }
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
create: async ({ model, data }) => {
|
|
272
|
+
const { canonical, table } = resolve(model)
|
|
273
|
+
await db.insert(table).values(toStorage(canonical, data) as never)
|
|
274
|
+
return data
|
|
275
|
+
},
|
|
276
|
+
findOne: async ({ model, where, join }) => {
|
|
277
|
+
const { canonical, table } = resolve(model)
|
|
278
|
+
const [row] = await db
|
|
279
|
+
.select()
|
|
280
|
+
.from(table)
|
|
281
|
+
.where(predicate(table, canonical, where))
|
|
282
|
+
.limit(1)
|
|
283
|
+
if (!row) return null
|
|
284
|
+
const result = fromStorage(canonical, row)
|
|
285
|
+
if (canonical === 'session' && join?.user) {
|
|
286
|
+
const userId = tables.user[
|
|
287
|
+
'id' as keyof typeof tables.user
|
|
288
|
+
] as never
|
|
289
|
+
const [user] = await db
|
|
290
|
+
.select()
|
|
291
|
+
.from(tables.user)
|
|
292
|
+
.where(eq(userId, row.userId as never))
|
|
293
|
+
.limit(1)
|
|
294
|
+
if (user) result.user = fromStorage('user', user)
|
|
295
|
+
}
|
|
296
|
+
return result as never
|
|
297
|
+
},
|
|
298
|
+
findMany: async ({ model, where, limit, offset, sortBy }) => {
|
|
299
|
+
const { canonical, table } = resolve(model)
|
|
300
|
+
let query = db.select().from(table).$dynamic()
|
|
301
|
+
const filter = where ? predicate(table, canonical, where) : undefined
|
|
302
|
+
if (filter) query = query.where(filter)
|
|
303
|
+
if (sortBy) {
|
|
304
|
+
const [field] = storageField(canonical, sortBy.field, null)
|
|
305
|
+
const column = table[field as keyof typeof table] as never
|
|
306
|
+
query = query.orderBy(
|
|
307
|
+
sortBy.direction === 'asc' ? asc(column) : desc(column),
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
const rows = await query.limit(limit).offset(offset ?? 0)
|
|
311
|
+
return rows.map((row) => fromStorage(canonical, row)) as never
|
|
312
|
+
},
|
|
313
|
+
update: async ({ model, where, update }) => {
|
|
314
|
+
const { canonical, table } = resolve(model)
|
|
315
|
+
await db
|
|
316
|
+
.update(table)
|
|
317
|
+
.set(
|
|
318
|
+
toStorage(canonical, update as Record<string, unknown>) as never,
|
|
319
|
+
)
|
|
320
|
+
.where(predicate(table, canonical, where))
|
|
321
|
+
const [row] = await db
|
|
322
|
+
.select()
|
|
323
|
+
.from(table)
|
|
324
|
+
.where(predicate(table, canonical, where))
|
|
325
|
+
.limit(1)
|
|
326
|
+
return row ? (fromStorage(canonical, row) as never) : null
|
|
327
|
+
},
|
|
328
|
+
updateMany: async ({ model, where, update }) => {
|
|
329
|
+
const { canonical, table } = resolve(model)
|
|
330
|
+
const result = await db
|
|
331
|
+
.update(table)
|
|
332
|
+
.set(toStorage(canonical, update) as never)
|
|
333
|
+
.where(predicate(table, canonical, where))
|
|
334
|
+
return Number((result as [{ affectedRows: number }])[0].affectedRows)
|
|
335
|
+
},
|
|
336
|
+
delete: async ({ model, where }) => {
|
|
337
|
+
const { canonical, table } = resolve(model)
|
|
338
|
+
await db.delete(table).where(predicate(table, canonical, where))
|
|
339
|
+
},
|
|
340
|
+
deleteMany: async ({ model, where }) => {
|
|
341
|
+
const { canonical, table } = resolve(model)
|
|
342
|
+
const result = await db
|
|
343
|
+
.delete(table)
|
|
344
|
+
.where(predicate(table, canonical, where))
|
|
345
|
+
return Number((result as [{ affectedRows: number }])[0].affectedRows)
|
|
346
|
+
},
|
|
347
|
+
count: async ({ model, where }) => {
|
|
348
|
+
const { canonical, table } = resolve(model)
|
|
349
|
+
const [result] = await db
|
|
350
|
+
.select({ count: count() })
|
|
351
|
+
.from(table)
|
|
352
|
+
.where(where ? predicate(table, canonical, where) : undefined)
|
|
353
|
+
return Number(result?.count ?? 0)
|
|
354
|
+
},
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
})
|
|
358
|
+
}
|