Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created September 5, 2023 11:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mizchi/46bae6c8302e0954350094d2a3215028 to your computer and use it in GitHub Desktop.
Save mizchi/46bae6c8302e0954350094d2a3215028 to your computer and use it in GitHub Desktop.
WIP
import { Kysely } from "kysely";
import type { Adapter, AdapterSession, AdapterUser } from "@auth/core/adapters";
import type { Generated, GeneratedAlways } from "kysely";
export interface Database {
User: {
id: Generated<string>;
name: string | null;
email: string;
emailVerified: Date | string | null;
image: string | null;
};
Account: {
id: Generated<string>;
userId: string;
type: string;
provider: string;
providerAccountId: string;
refresh_token: string | null;
access_token: string | null;
expires_at: number | null;
token_type: string | null;
scope: string | null;
id_token: string | null;
session_state: string | null;
};
Session: {
id: GeneratedAlways<string>;
userId: string;
sessionToken: string;
expires: Date | string;
};
VerificationToken: {
identifier: string;
token: string;
expires: Date | string;
};
}
export const format = {
/**
* Helper function to return the passed in object and its specified prop
* as an ISO string if SQLite is being used.
*/
from<T extends Partial<Record<K, Date | null>>, K extends keyof T>(
data: T,
key: K,
isSqlite: boolean,
) {
const value = data[key];
return {
...data,
[key]: value && isSqlite ? value.toISOString() : value,
};
},
to,
};
type ReturnData<T = never> = Record<string, Date | string | T>;
/**
* Helper function to return the passed in object and its specified prop as a date.
* Necessary because SQLite has no date type so we store dates as ISO strings.
*/
function to<T extends Partial<ReturnData>, K extends keyof T>(
data: T,
key: K,
): Omit<T, K> & Record<K, Date>;
function to<T extends Partial<ReturnData<null>>, K extends keyof T>(
data: T,
key: K,
): Omit<T, K> & Record<K, Date | null>;
function to<T extends Partial<ReturnData<null>>, K extends keyof T>(
data: T,
key: K,
) {
const value = data[key];
return Object.assign(data, {
[key]: value && typeof value === "string" ? new Date(value) : value,
});
}
export function KyselyD1LazyAdapter(getDb: () => Promise<Kysely<Database>>): Adapter {
return {
async createUser(data) {
const db = await getDb();
// const userData = format.from(data, "emailVerified", isSqlite)
const query = db
.insertInto("User")
.values({ ...data, emailVerified: data.emailVerified?.toISOString() });
const result = await query.returningAll().executeTakeFirstOrThrow();
return {
...result,
emailVerified: result.emailVerified
? new Date(result.emailVerified)
: null,
} as AdapterUser;
},
async getUser(id) {
const db = await getDb();
const result =
(await db
.selectFrom("User")
.selectAll()
.where("id", "=", id)
.executeTakeFirst()) ?? null;
if (!result) return null;
return {
...result,
emailVerified: result.emailVerified
? new Date(result.emailVerified)
: null,
} as AdapterUser;
},
async getUserByEmail(email) {
const db = await getDb();
const result =
(await db
.selectFrom("User")
.selectAll()
.where("email", "=", email)
.executeTakeFirst()) ?? null;
if (!result) return null;
return {
...result,
emailVerified: result.emailVerified
? new Date(result.emailVerified)
: null,
} as AdapterUser;
// return to(result, "emailVerified")
},
async getUserByAccount({ providerAccountId, provider }) {
const db = await getDb();
const result =
(await db
.selectFrom("User")
.innerJoin("Account", "User.id", "Account.userId")
.selectAll("User")
.where("Account.providerAccountId", "=", providerAccountId)
.where("Account.provider", "=", provider)
.executeTakeFirst()) ?? null;
if (!result) return null;
return {
...result,
emailVerified: result.emailVerified
? new Date(result.emailVerified)
: null,
} as AdapterUser;
},
async updateUser({ id, ...user }) {
const db = await getDb();
if (!id) throw new Error("User not found");
// const userData = format.from(user, "emailVerified", isSqlite)
const query = db
.updateTable("User")
.set({
...user,
emailVerified: user.emailVerified?.toISOString(),
})
.where("id", "=", id);
const result = await query.returningAll().executeTakeFirstOrThrow();
return {
...result,
emailVerified: result.emailVerified
? new Date(result.emailVerified)
: null,
} as AdapterUser;
},
async deleteUser(userId) {
const db = await getDb();
await db.deleteFrom("User").where("User.id", "=", userId).execute();
},
async linkAccount(account) {
const db = await getDb();
await db.insertInto("Account").values(account).executeTakeFirstOrThrow();
},
async unlinkAccount({ providerAccountId, provider }) {
const db = await getDb();
await db
.deleteFrom("Account")
.where("Account.providerAccountId", "=", providerAccountId)
.where("Account.provider", "=", provider)
.executeTakeFirstOrThrow();
},
async createSession(data) {
const db = await getDb();
const sessionData = {
...data,
expires: data.expires.toISOString(),
};
const query = db.insertInto("Session").values(sessionData);
const result = await query.returningAll().executeTakeFirstOrThrow();
return to(result, "expires");
},
async getSessionAndUser(sessionTokenArg) {
const db = await getDb();
const result = await db
.selectFrom("Session")
.innerJoin("User", "User.id", "Session.userId")
.selectAll("User")
.select([
"Session.id as sessionId",
"Session.userId",
"Session.sessionToken",
"Session.expires",
])
.where("Session.sessionToken", "=", sessionTokenArg)
.executeTakeFirst();
if (!result) return null;
const { sessionId: id, userId, sessionToken, expires, ...user } = result;
return {
user: {
...user,
emailVerified: user.emailVerified ? new Date() : null,
} as AdapterUser,
session: {
id,
userId,
sessionToken,
expires: new Date(expires),
} as AdapterSession,
};
},
async updateSession(session) {
const db = await getDb();
// const sessionData = format.from(session, "expires", isSqlite);
const sessionData = {
...session,
expires: session.expires?.toISOString(),
};
const query = db
.updateTable("Session")
.set(sessionData)
.where("Session.sessionToken", "=", session.sessionToken);
const result = await query.returningAll().executeTakeFirstOrThrow();
return to(result, "expires");
},
async deleteSession(sessionToken) {
const db = await getDb();
await db
.deleteFrom("Session")
.where("Session.sessionToken", "=", sessionToken)
.executeTakeFirstOrThrow();
},
async createVerificationToken(verificationToken) {
const db = await getDb();
const verificationTokenData = {
...verificationToken,
expires: verificationToken.expires.toISOString(),
};
const query = db
.insertInto("VerificationToken")
.values(verificationTokenData);
const result = await query.returningAll().executeTakeFirstOrThrow();
return to(result, "expires");
},
async useVerificationToken({ identifier, token }) {
const db = await getDb();
const query = db
.deleteFrom("VerificationToken")
.where("VerificationToken.token", "=", token)
.where("VerificationToken.identifier", "=", identifier);
const result = (await query.returningAll().executeTakeFirst()) ?? null;
if (!result) return null;
return to(result, "expires");
},
};
}
/**
* Wrapper over the original `Kysely` class in order to validate the passed in
* database interface. A regular Kysely instance may also be used, but wrapping
* it ensures the database interface implements the fields that Auth.js
* requires. When used with `kysely-codegen`, the `Codegen` type can be passed as
* the second generic argument. The generated types will be used, and
* `KyselyAuth` will only verify that the correct fields exist.
**/
export class KyselyAuth<DB extends T, T = Database> extends Kysely<DB> {}
export type Codegen = {
[K in keyof Database]: { [J in keyof Database[K]]: unknown };
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment