@better-auth/kysely-adapter 1.7.0-beta.0 → 1.7.0-beta.10

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.
@@ -57,7 +57,14 @@ var BunSqliteConnection = class {
57
57
  executeQuery(compiledQuery) {
58
58
  const { sql, parameters } = compiledQuery;
59
59
  const stmt = this.#db.prepare(sql);
60
- return Promise.resolve({ rows: stmt.all(parameters) });
60
+ const params = parameters;
61
+ if (stmt.columnNames.length > 0) return Promise.resolve({ rows: stmt.all(...params) });
62
+ const { changes, lastInsertRowid } = stmt.run(...params);
63
+ return Promise.resolve({
64
+ rows: [],
65
+ numAffectedRows: BigInt(changes),
66
+ insertId: typeof lastInsertRowid === "bigint" ? lastInsertRowid : BigInt(lastInsertRowid)
67
+ });
61
68
  }
62
69
  async *streamQuery() {
63
70
  throw new Error("Streaming query is not supported by SQLite driver.");
@@ -113,7 +120,7 @@ var BunSqliteIntrospector = class {
113
120
  isAutoIncrementing: col.name === autoIncrementCol,
114
121
  hasDefaultValue: col.dflt_value != null
115
122
  })),
116
- isView: true
123
+ isView: false
117
124
  };
118
125
  }
119
126
  };
package/dist/index.d.mts CHANGED
@@ -21,6 +21,19 @@ declare const createKyselyAdapter: (config: BetterAuthOptions) => Promise<{
21
21
  interface KyselyAdapterConfig {
22
22
  /**
23
23
  * Database type.
24
+ *
25
+ * For `"mysql"`, this adapter depends on the driver returning
26
+ * "rows matched" counts from `UPDATE`/`DELETE` operations (in
27
+ * mysql2: `affectedRows`, exposed by Kysely as `numUpdatedRows`).
28
+ * By default, `mysql2` enables this via the `FOUND_ROWS` client
29
+ * flag.
30
+ *
31
+ * Do not disable this flag. If you remove it (e.g. with
32
+ * `flags: '-FOUND_ROWS'` in your pool config), MySQL will report
33
+ * "rows changed" semantics: an idempotent `UPDATE` (where the new
34
+ * value equals the old value) will show zero affected rows, causing
35
+ * adapter methods like `update`, `incrementOne`, or `updateMany` to
36
+ * return `null` or `0` even if a row matched the predicate.
24
37
  */
25
38
  type?: KyselyDatabaseType | undefined;
26
39
  /**
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Kysely, MssqlDialect, MysqlDialect, PostgresDialect, SqliteDialect, sql } from "kysely";
2
2
  import { createAdapterFactory } from "@better-auth/core/db/adapter";
3
+ import { logger } from "@better-auth/core/env";
3
4
  import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
4
5
  //#region src/dialect.ts
5
6
  function getKyselyDatabaseType(db) {
@@ -43,7 +44,7 @@ const createKyselyAdapter = async (config) => {
43
44
  if ("getConnection" in db) dialect = new MysqlDialect(db);
44
45
  if ("connect" in db) dialect = new PostgresDialect({ pool: db });
45
46
  if ("fileControl" in db) {
46
- const { BunSqliteDialect } = await import("./bun-sqlite-dialect-na--YwnN.mjs");
47
+ const { BunSqliteDialect } = await import("./bun-sqlite-dialect-PuE7pPr-.mjs");
47
48
  dialect = new BunSqliteDialect({ database: db });
48
49
  }
49
50
  if ("createSession" in db) {
@@ -124,8 +125,13 @@ function insensitiveNe(columnRef, value) {
124
125
  //#region src/kysely-adapter.ts
125
126
  const kyselyAdapter = (db, config) => {
126
127
  let lazyOptions = null;
127
- const createCustomAdapter = (db) => {
128
- return ({ getFieldName, schema, getDefaultFieldName, getDefaultModelName, getFieldAttributes, getModelName }) => {
128
+ let mysqlNoIdWarned = false;
129
+ const createCustomAdapter = (db, inTransaction = false) => {
130
+ return ({ getFieldName, schema, getDefaultFieldName, getDefaultModelName, getFieldAttributes, getModelName, options }) => {
131
+ if (config?.type === "mysql" && options.advanced?.database?.generateId === false && !mysqlNoIdWarned) {
132
+ mysqlNoIdWarned = true;
133
+ logger.warn("[Kysely Adapter] MySQL does not support INSERT...RETURNING. With generateId set to false, the adapter uses best-effort fallback strategies (unique columns, full-field match) to retrieve inserted rows. For reliable behavior, use Better Auth's default ID generation, a custom generateId function, or generateId: \"serial\" for auto-increment.");
134
+ }
129
135
  const selectAllJoins = (join) => {
130
136
  const allSelects = [];
131
137
  const allSelectsStr = [];
@@ -149,33 +155,71 @@ const kyselyAdapter = (db, config) => {
149
155
  };
150
156
  };
151
157
  const withReturning = async (values, builder, model, where) => {
152
- let res;
153
158
  if (config?.type === "mysql") {
154
- await builder.execute();
155
- const field = values.id ? "id" : where.length > 0 && where[0]?.field ? where[0].field : "id";
156
- if (!values.id && where.length === 0) {
157
- res = await db.selectFrom(model).selectAll().orderBy(getFieldName({
159
+ if (where.length > 0) {
160
+ const updateResult = await builder.executeTakeFirst();
161
+ if (!updateResult || Number(updateResult.numUpdatedRows ?? 0) === 0) return null;
162
+ const idEqualityWhere = where.find((w) => w.field === "id" && (w.operator === void 0 || w.operator === "eq") && w.connector !== "OR" && w.value !== void 0 && w.value !== null);
163
+ let reselectField;
164
+ let reselectValue;
165
+ if (values.id !== void 0 && values.id !== null) {
166
+ reselectField = "id";
167
+ reselectValue = values.id;
168
+ } else if (idEqualityWhere) {
169
+ reselectField = "id";
170
+ reselectValue = idEqualityWhere.value;
171
+ } else if (where[0]?.field) {
172
+ reselectField = where[0].field;
173
+ reselectValue = values[reselectField] !== void 0 ? values[reselectField] : where[0].value;
174
+ } else return null;
175
+ return await db.selectFrom(model).selectAll().where(getFieldName({
158
176
  model,
159
- field
160
- }), "desc").limit(1).executeTakeFirst();
161
- return res;
177
+ field: reselectField
178
+ }), reselectValue === null ? "is" : "=", reselectValue).limit(1).executeTakeFirst();
162
179
  }
163
- const value = values[field] !== void 0 ? values[field] : where[0]?.value;
164
- res = await db.selectFrom(model).selectAll().orderBy(getFieldName({
165
- model,
166
- field
167
- }), "desc").where(getFieldName({
168
- model,
169
- field
170
- }), value === null ? "is" : "=", value).limit(1).executeTakeFirst();
171
- return res;
172
- }
173
- if (config?.type === "mssql") {
174
- res = await builder.outputAll("inserted").executeTakeFirst();
175
- return res;
180
+ await builder.execute();
181
+ const fetchInserted = async (trx) => {
182
+ if (values.id) return await trx.selectFrom(model).selectAll().where(getFieldName({
183
+ model,
184
+ field: "id"
185
+ }), "=", values.id).limit(1).executeTakeFirst();
186
+ if (options.advanced?.database?.generateId === "serial") {
187
+ const lastId = (await sql`SELECT LAST_INSERT_ID() as id`.execute(trx)).rows[0]?.id;
188
+ if (lastId) return await trx.selectFrom(model).selectAll().where(getFieldName({
189
+ model,
190
+ field: "id"
191
+ }), "=", lastId).limit(1).executeTakeFirst();
192
+ }
193
+ const modelSchema = schema[getDefaultModelName(model)]?.fields;
194
+ if (modelSchema) for (const [fieldKey, fieldAttr] of Object.entries(modelSchema)) {
195
+ if (!fieldAttr.unique) continue;
196
+ const dbFieldName = getFieldName({
197
+ model,
198
+ field: fieldKey
199
+ });
200
+ const val = values[dbFieldName];
201
+ if (val === void 0 || val === null) continue;
202
+ const row = await trx.selectFrom(model).selectAll().where(dbFieldName, "=", val).limit(1).executeTakeFirst();
203
+ if (row) return row;
204
+ }
205
+ let query = trx.selectFrom(model).selectAll();
206
+ let hasConditions = false;
207
+ for (const [key, val] of Object.entries(values)) {
208
+ if (val === void 0) continue;
209
+ query = query.where(key, val === null ? "is" : "=", val);
210
+ hasConditions = true;
211
+ }
212
+ if (hasConditions) {
213
+ const rows = await query.limit(2).execute();
214
+ if (rows.length === 1) return rows[0];
215
+ }
216
+ logger.warn(`[Kysely Adapter] Unable to safely identify the inserted "${model}" row on MySQL. Enable Better Auth ID generation or use generateId: "serial" for reliable behavior.`);
217
+ return null;
218
+ };
219
+ return inTransaction ? fetchInserted(db) : db.transaction().execute(fetchInserted);
176
220
  }
177
- res = await builder.returningAll().executeTakeFirst();
178
- return res;
221
+ if (config?.type === "mssql") return await builder.outputAll("inserted").executeTakeFirst();
222
+ return await builder.returningAll().executeTakeFirst();
179
223
  };
180
224
  function convertWhereClause(model, w) {
181
225
  if (!w) return {
@@ -386,6 +430,7 @@ const kyselyAdapter = (db, config) => {
386
430
  return res;
387
431
  },
388
432
  async update({ model, where, update: values }) {
433
+ if (where.length === 0) return null;
389
434
  const { and, or } = convertWhereClause(model, where);
390
435
  let query = db.updateTable(model).set(values);
391
436
  if (and) query = query.where((eb) => eb.and(and.map((expr) => expr(eb))));
@@ -425,6 +470,75 @@ const kyselyAdapter = (db, config) => {
425
470
  const res = (await query.executeTakeFirst()).numDeletedRows;
426
471
  return res > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : Number(res);
427
472
  },
473
+ async consumeOne({ model, where }) {
474
+ const { and, or } = convertWhereClause(model, where);
475
+ const applyWhere = (query) => {
476
+ if (and) query = query.where((eb) => eb.and(and.map((expr) => expr(eb))));
477
+ if (or) query = query.where((eb) => eb.or(or.map((expr) => expr(eb))));
478
+ return query;
479
+ };
480
+ const idField = getFieldName({
481
+ model,
482
+ field: "id"
483
+ });
484
+ const deleteSelectedRow = async (db, row) => {
485
+ const targetId = row[idField] ?? row.id;
486
+ if (targetId === void 0 || targetId === null) return null;
487
+ const query = db.deleteFrom(model).where(`${model}.${idField}`, "=", targetId);
488
+ if (config?.type === "mysql") {
489
+ const result = await query.executeTakeFirst();
490
+ return Number(result.numDeletedRows) > 0 ? row : null;
491
+ }
492
+ if (config?.type === "mssql") return await query.outputAll("deleted").executeTakeFirst() ?? null;
493
+ return await query.returningAll().executeTakeFirst() ?? null;
494
+ };
495
+ const deleteWithReturning = async (query) => {
496
+ if (config?.type === "mssql") return await query.outputAll("deleted").executeTakeFirst() ?? null;
497
+ return await query.returningAll().executeTakeFirst() ?? null;
498
+ };
499
+ if (config?.type === "mysql") {
500
+ const claimFromTransaction = async (trx) => {
501
+ const row = await applyWhere(trx.selectFrom(model).selectAll().forUpdate()).limit(1).executeTakeFirst();
502
+ if (!row) return null;
503
+ return deleteSelectedRow(trx, row);
504
+ };
505
+ return inTransaction ? claimFromTransaction(db) : db.transaction().execute(claimFromTransaction);
506
+ }
507
+ const selectIds = applyWhere(db.selectFrom(model).select(`${model}.${idField}`));
508
+ const targetIds = config?.type === "mssql" ? selectIds.top(1) : selectIds.limit(1);
509
+ return deleteWithReturning(db.deleteFrom(model).where(`${model}.${idField}`, "in", targetIds));
510
+ },
511
+ async incrementOne({ model, where, increment, set }) {
512
+ const { and, or } = convertWhereClause(model, where);
513
+ const applyWhere = (query) => {
514
+ if (and) query = query.where((eb) => eb.and(and.map((expr) => expr(eb))));
515
+ if (or) query = query.where((eb) => eb.or(or.map((expr) => expr(eb))));
516
+ return query;
517
+ };
518
+ const assignments = { ...set ?? {} };
519
+ for (const [field, delta] of Object.entries(increment)) assignments[field] = sql`${sql.ref(field)} + ${delta}`;
520
+ const idField = getFieldName({
521
+ model,
522
+ field: "id"
523
+ });
524
+ if (config?.type === "mysql") {
525
+ const incrementInTransaction = async (trx) => {
526
+ const target = await applyWhere(trx.selectFrom(model).select(`${model}.${idField}`).forUpdate()).limit(1).executeTakeFirst();
527
+ if (!target) return null;
528
+ const targetId = target[idField] ?? target.id;
529
+ if (targetId === void 0 || targetId === null) return null;
530
+ const updated = await applyWhere(trx.updateTable(model).set(assignments)).where(`${model}.${idField}`, "=", targetId).executeTakeFirst();
531
+ if (Number(updated.numUpdatedRows) === 0) return null;
532
+ return await trx.selectFrom(model).selectAll().where(`${model}.${idField}`, "=", targetId).limit(1).executeTakeFirst() ?? null;
533
+ };
534
+ return inTransaction ? incrementInTransaction(db) : db.transaction().execute(incrementInTransaction);
535
+ }
536
+ const selectIds = applyWhere(db.selectFrom(model).select(`${model}.${idField}`));
537
+ const targetIds = config?.type === "mssql" ? selectIds.top(1) : selectIds.limit(1);
538
+ const updateQuery = applyWhere(db.updateTable(model).set(assignments)).where(`${model}.${idField}`, "in", targetIds);
539
+ if (config?.type === "mssql") return await updateQuery.outputAll("inserted").executeTakeFirst() ?? null;
540
+ return await updateQuery.returningAll().executeTakeFirst() ?? null;
541
+ },
428
542
  options: config
429
543
  };
430
544
  };
@@ -443,8 +557,11 @@ const kyselyAdapter = (db, config) => {
443
557
  supportsUUIDs: config?.type === "postgres" ? true : false,
444
558
  transaction: config?.transaction ? (cb) => db.transaction().execute((trx) => {
445
559
  return cb(createAdapterFactory({
446
- config: adapterOptions.config,
447
- adapter: createCustomAdapter(trx)
560
+ config: {
561
+ ...adapterOptions.config,
562
+ transaction: false
563
+ },
564
+ adapter: createCustomAdapter(trx, true)
448
565
  })(lazyOptions));
449
566
  }) : false
450
567
  },
@@ -56,8 +56,15 @@ var NodeSqliteConnection = class {
56
56
  }
57
57
  executeQuery(compiledQuery) {
58
58
  const { sql, parameters } = compiledQuery;
59
- const rows = this.#db.prepare(sql).all(...parameters);
60
- return Promise.resolve({ rows });
59
+ const stmt = this.#db.prepare(sql);
60
+ const params = parameters;
61
+ if (stmt.columns().length > 0) return Promise.resolve({ rows: stmt.all(...params) });
62
+ const { changes, lastInsertRowid } = stmt.run(...params);
63
+ return Promise.resolve({
64
+ rows: [],
65
+ numAffectedRows: BigInt(changes),
66
+ insertId: typeof lastInsertRowid === "bigint" ? lastInsertRowid : BigInt(lastInsertRowid)
67
+ });
61
68
  }
62
69
  async *streamQuery() {
63
70
  throw new Error("Streaming query is not supported by SQLite driver.");
@@ -113,7 +120,7 @@ var NodeSqliteIntrospector = class {
113
120
  isAutoIncrementing: col.name === autoIncrementCol,
114
121
  hasDefaultValue: col.dflt_value != null
115
122
  })),
116
- isView: true
123
+ isView: false
117
124
  };
118
125
  }
119
126
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/kysely-adapter",
3
- "version": "1.7.0-beta.0",
3
+ "version": "1.7.0-beta.10",
4
4
  "description": "Kysely adapter for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,9 +40,9 @@
40
40
  }
41
41
  },
42
42
  "peerDependencies": {
43
- "@better-auth/utils": "0.4.0",
44
- "kysely": "^0.27.0 || ^0.28.0",
45
- "@better-auth/core": "^1.7.0-beta.0"
43
+ "@better-auth/utils": "0.4.2",
44
+ "kysely": "^0.28.17 || ^0.29.0",
45
+ "@better-auth/core": "^1.7.0-beta.10"
46
46
  },
47
47
  "peerDependenciesMeta": {
48
48
  "kysely": {
@@ -50,12 +50,12 @@
50
50
  }
51
51
  },
52
52
  "devDependencies": {
53
+ "@better-auth/utils": "0.4.2",
53
54
  "@cloudflare/workers-types": "^4.20250121.0",
54
- "@better-auth/utils": "0.4.0",
55
- "kysely": "^0.28.14",
55
+ "kysely": "^0.28.17 || ^0.29.0",
56
56
  "tsdown": "0.21.1",
57
57
  "typescript": "^5.9.3",
58
- "@better-auth/core": "1.7.0-beta.0"
58
+ "@better-auth/core": "1.7.0-beta.10"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsdown",