@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 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
+ });
@@ -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 };
@@ -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
+ }