fix: mysql returning failing on update

This commit is contained in:
Bereket Engida
2024-11-16 00:34:57 +03:00
parent 1dc5544dd0
commit f9a30c357d
6 changed files with 107 additions and 40 deletions

View File

@@ -13,6 +13,8 @@ import { reactInvitationEmail } from "./email/invitation";
import { LibsqlDialect } from "@libsql/kysely-libsql"; import { LibsqlDialect } from "@libsql/kysely-libsql";
import { reactResetPasswordEmail } from "./email/rest-password"; import { reactResetPasswordEmail } from "./email/rest-password";
import { resend } from "./email/resend"; import { resend } from "./email/resend";
import { MysqlDialect } from "kysely";
import { createPool } from "mysql2/promise";
const from = process.env.BETTER_AUTH_EMAIL || "delivered@resend.dev"; const from = process.env.BETTER_AUTH_EMAIL || "delivered@resend.dev";
const to = process.env.TEST_EMAIL || ""; const to = process.env.TEST_EMAIL || "";
@@ -22,11 +24,15 @@ const libsql = new LibsqlDialect({
authToken: process.env.TURSO_AUTH_TOKEN || "", authToken: process.env.TURSO_AUTH_TOKEN || "",
}); });
const mysql = new MysqlDialect(
createPool("mysql://user:password@localhost:3306/better_auth"),
);
export const auth = betterAuth({ export const auth = betterAuth({
appName: "Better Auth Demo", appName: "Better Auth Demo",
database: { database: {
dialect: libsql, dialect: mysql,
type: "sqlite", type: "mysql",
}, },
session: { session: {
cookieCache: { cookieCache: {

View File

@@ -64,11 +64,12 @@
"kysely": "^0.27.4", "kysely": "^0.27.4",
"lucide-react": "^0.439.0", "lucide-react": "^0.439.0",
"mini-svg-data-uri": "^1.4.4", "mini-svg-data-uri": "^1.4.4",
"mysql2": "^3.11.0",
"next": "15.0.0-rc.1", "next": "15.0.0-rc.1",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"prisma": "^5.19.1", "prisma": "^5.19.1",
"react-day-picker": "8.10.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-day-picker": "8.10.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.53.0", "react-hook-form": "^7.53.0",
"react-qr-code": "^2.0.15", "react-qr-code": "^2.0.15",

View File

@@ -19,6 +19,23 @@ interface KyselyAdapterConfig {
generateId?: ((size?: number) => string) | false; generateId?: ((size?: number) => string) | false;
} }
function formatDateForMySQL(date: Date): string {
const pad = (n: number) => (n < 10 ? "0" + n : n);
return (
date.getFullYear() +
"-" +
pad(date.getMonth() + 1) +
"-" +
pad(date.getDate()) +
" " +
pad(date.getHours()) +
":" +
pad(date.getMinutes()) +
":" +
pad(date.getSeconds())
);
}
const createTransform = ( const createTransform = (
db: Kysely<any>, db: Kysely<any>,
options: BetterAuthOptions, options: BetterAuthOptions,
@@ -43,7 +60,10 @@ const createTransform = (
if (f.type === "boolean" && type === "sqlite") { if (f.type === "boolean" && type === "sqlite") {
return value ? 1 : 0; return value ? 1 : 0;
} }
if (f.type === "date" && value) { if (f.type === "date" && value && value instanceof Date) {
if (type === "mysql") {
return formatDateForMySQL(value);
}
return value.toISOString(); return value.toISOString();
} }
return value; return value;
@@ -62,6 +82,10 @@ const createTransform = (
return value; return value;
} }
function getModelName(model: string) {
return schema[model].tableName;
}
const shouldGenerateId = config?.generateId !== false; const shouldGenerateId = config?.generateId !== false;
return { return {
transformInput(data: Record<string, any>, model: string) { transformInput(data: Record<string, any>, model: string) {
@@ -194,25 +218,25 @@ const createTransform = (
| InsertQueryBuilder<any, any, any> | InsertQueryBuilder<any, any, any>
| UpdateQueryBuilder<any, string, string, any>, | UpdateQueryBuilder<any, string, string, any>,
model: string, model: string,
where: Where[],
) { ) {
let res: any; let res: any;
if (config?.type !== "mysql") { if (config?.type !== "mysql") {
res = await builder.returningAll().executeTakeFirst(); res = await builder.returningAll().executeTakeFirst();
} else { } else {
//this isn't good, but kysely doesn't support returning in mysql and it doesn't return the inserted id. Change this if there is a better way.
await builder.execute(); await builder.execute();
const primaryKey = "id"; const field = values.id ? "id" : where[0].field ? where[0].field : "id";
const insertedId = values[primaryKey]; const value = values[field] || where[0].value;
res = await db res = await db
.selectFrom(this.getModelName(model)) .selectFrom(getModelName(model))
.selectAll() .selectAll()
.where(primaryKey, "=", insertedId) .where(getField(model, field), "=", value)
.executeTakeFirst(); .executeTakeFirst();
} }
return res; return res;
}, },
getModelName(model: string) { getModelName,
return schema[model].tableName;
},
getField, getField,
}; };
}; };
@@ -235,7 +259,7 @@ export const kyselyAdapter =
const transformed = transformInput(values, model); const transformed = transformInput(values, model);
const builder = db.insertInto(getModelName(model)).values(transformed); const builder = db.insertInto(getModelName(model)).values(transformed);
return transformOutput( return transformOutput(
await withReturning(transformed, builder, model), await withReturning(transformed, builder, model, []),
model, model,
select, select,
); );
@@ -257,7 +281,7 @@ export const kyselyAdapter =
async findMany(data) { async findMany(data) {
const { model, where, limit, offset, sortBy } = data; const { model, where, limit, offset, sortBy } = data;
const { and, or } = convertWhereClause(model, where); const { and, or } = convertWhereClause(model, where);
let query = db.selectFrom(getModelName(model)).selectAll(); let query = db.selectFrom(getModelName(model));
if (and) { if (and) {
query = query.where((eb) => eb.and(and.map((expr) => expr(eb)))); query = query.where((eb) => eb.and(and.map((expr) => expr(eb))));
} }
@@ -265,7 +289,9 @@ export const kyselyAdapter =
query = query.where((eb) => eb.or(or.map((expr) => expr(eb)))); query = query.where((eb) => eb.or(or.map((expr) => expr(eb))));
} }
query = query.limit(limit || 100); query = query.limit(limit || 100);
if (offset) query = query.offset(offset); if (offset) {
query = query.offset(offset);
}
if (sortBy) { if (sortBy) {
query = query.orderBy( query = query.orderBy(
getField(model, sortBy.field), getField(model, sortBy.field),
@@ -288,7 +314,7 @@ export const kyselyAdapter =
query = query.where((eb) => eb.or(or.map((expr) => expr(eb)))); query = query.where((eb) => eb.or(or.map((expr) => expr(eb))));
} }
return transformOutput( return transformOutput(
await withReturning(transformedData, query, model), await withReturning(transformedData, query, model, where),
model, model,
); );
}, },

View File

@@ -1,42 +1,71 @@
import fs from "fs/promises"; import fs from "fs/promises";
import { afterAll, beforeEach, describe, expect, it } from "vitest"; import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { runAdapterTest } from "../../test"; import { runAdapterTest } from "../../test";
import { getMigrations } from "../../../db/get-migration"; import { getMigrations } from "../../../db/get-migration";
import path from "path"; import path from "path";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import { kyselyAdapter } from ".."; import { kyselyAdapter } from "..";
import { Kysely, SqliteDialect } from "kysely"; import { Kysely, MysqlDialect, SqliteDialect } from "kysely";
import type { BetterAuthOptions } from "../../../types"; import type { BetterAuthOptions } from "../../../types";
import { createPool } from "mysql2/promise";
describe("adapter test", async () => { describe("adapter test", async () => {
const database = new Database(path.join(__dirname, "test.db"));
const opts = {
database,
user: {
fields: {
email: "email_address",
},
},
session: {
modelName: "sessions",
},
} satisfies BetterAuthOptions;
beforeEach(async () => {
const { runMigrations } = await getMigrations(opts);
await runMigrations();
});
afterAll(async () => {
await fs.unlink(path.join(__dirname, "test.db"));
});
const sqlite = new Database(path.join(__dirname, "test.db")); const sqlite = new Database(path.join(__dirname, "test.db"));
const db = new Kysely({ const mysql = createPool("mysql://user:password@localhost:3306/better_auth");
const sqliteKy = new Kysely({
dialect: new SqliteDialect({ dialect: new SqliteDialect({
database: sqlite, database: sqlite,
}), }),
}); });
const adapter = kyselyAdapter(db)(opts); const mysqlKy = new Kysely({
dialect: new MysqlDialect(mysql),
});
const opts = (database: BetterAuthOptions["database"]) =>
({
database: database,
user: {
fields: {
email: "email_address",
},
},
session: {
modelName: "sessions",
},
}) satisfies BetterAuthOptions;
const mysqlOptions = opts({
db: mysqlKy,
type: "mysql",
});
const sqliteOptions = opts({
db: sqliteKy,
type: "sqlite",
});
beforeAll(async () => {
const { runMigrations } = await getMigrations(mysqlOptions);
await runMigrations();
const { runMigrations: runMigrationsSqlite } =
await getMigrations(sqliteOptions);
await runMigrationsSqlite();
});
afterAll(async () => {
await mysql.query("DROP DATABASE IF EXISTS better_auth");
await mysql.query("CREATE DATABASE better_auth");
await mysql.end();
await fs.unlink(path.join(__dirname, "test.db"));
});
const mysqlAdapter = kyselyAdapter(mysqlKy, {
type: "mysql",
})(mysqlOptions);
await runAdapterTest({ await runAdapterTest({
adapter, adapter: mysqlAdapter,
});
const sqliteAdapter = kyselyAdapter(sqliteKy, {
type: "sqlite",
})(sqliteOptions);
await runAdapterTest({
adapter: sqliteAdapter,
}); });
}); });

View File

@@ -59,7 +59,9 @@ export async function generateState(
export async function parseState(c: GenericEndpointContext) { export async function parseState(c: GenericEndpointContext) {
const state = c.query.state || c.body.state; const state = c.query.state || c.body.state;
console.log("finding verification value", state);
const data = await c.context.internalAdapter.findVerificationValue(state); const data = await c.context.internalAdapter.findVerificationValue(state);
console.log("data", data);
if (!data) { if (!data) {
logger.error("State Mismatch. Verification not found", { logger.error("State Mismatch. Verification not found", {
state, state,

3
pnpm-lock.yaml generated
View File

@@ -203,6 +203,9 @@ importers:
mini-svg-data-uri: mini-svg-data-uri:
specifier: ^1.4.4 specifier: ^1.4.4
version: 1.4.4 version: 1.4.4
mysql2:
specifier: ^3.11.0
version: 3.11.3
next: next:
specifier: 15.0.0-rc.1 specifier: 15.0.0-rc.1
version: 15.0.0-rc.1(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 15.0.0-rc.1(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)