Created
September 5, 2023 11:57
-
-
Save mizchi/46bae6c8302e0954350094d2a3215028 to your computer and use it in GitHub Desktop.
WIP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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