@auth/d1-adapter
Version:
A Cloudflare D1 adapter for Auth.js
337 lines (320 loc) • 9.54 kB
text/typescript
/**
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: "16px"}}>
* <p>An official <a href="https://developers.cloudflare.com/d1/">Cloudflare D1</a> adapter for Auth.js / NextAuth.js.</p>
* <a href="https://developers.cloudflare.com/d1/">
* <img style={{display: "block"}} src="/img/adapters/d1.svg" width="48" />
* </a>
* </div>
*
* ## Warning
* This adapter is not developed or maintained by Cloudflare and they haven't declared the D1 api stable. The author will make an effort to keep this adapter up to date.
* The adapter is compatible with the D1 api as of March 22, 2023.
*
* ## Installation
*
* ```bash npm2yarn
* npm install next-auth @auth/d1-adapter
* ```
*
* @module @auth/d1-adapter
*/
import type { D1Database as WorkerDatabase } from "@cloudflare/workers-types"
import type { D1Database as MiniflareD1Database } from "@miniflare/d1"
import {
type Adapter,
type AdapterSession,
type AdapterUser,
type AdapterAccount,
type VerificationToken as AdapterVerificationToken,
isDate,
} from "@auth/core/adapters"
import {
CREATE_ACCOUNT_SQL,
CREATE_SESSION_SQL,
CREATE_USER_SQL,
CREATE_VERIFICATION_TOKEN_SQL,
DELETE_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL,
DELETE_ACCOUNT_BY_USER_ID_SQL,
DELETE_SESSION_BY_USER_ID_SQL,
DELETE_SESSION_SQL,
DELETE_USER_SQL,
DELETE_VERIFICATION_TOKEN_SQL,
GET_ACCOUNT_BY_ID_SQL,
GET_SESSION_BY_TOKEN_SQL,
GET_USER_BY_ACCOUNTL_SQL,
GET_USER_BY_EMAIL_SQL,
GET_USER_BY_ID_SQL,
GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL,
UPDATE_SESSION_BY_SESSION_TOKEN_SQL,
UPDATE_USER_BY_ID_SQL,
} from "./queries.js"
export { up } from "./migrations.js"
/**
* @type @cloudflare/workers-types.D1Database | @miniflare/d1.D1Database
*/
export type D1Database = WorkerDatabase | MiniflareD1Database
// format is borrowed from the supabase adapter, graciously
function format<T>(obj: Record<string, any>): T {
for (const [key, value] of Object.entries(obj)) {
if (value === null) {
delete obj[key]
}
if (isDate(value)) {
obj[key] = new Date(value)
}
}
return obj as T
}
// D1 doesnt like undefined, it wants null when calling bind
function cleanBindings(bindings: any[]) {
return bindings.map((e) => (e === undefined ? null : e))
}
export async function createRecord<RecordType>(
db: D1Database,
CREATE_SQL: string,
bindings: any[],
GET_SQL: string,
getBindings: any[]
) {
try {
bindings = cleanBindings(bindings)
await db
.prepare(CREATE_SQL)
.bind(...bindings)
.run()
return await getRecord<RecordType>(db, GET_SQL, getBindings)
} catch (e: any) {
console.error(e.message, e.cause?.message)
throw e
}
}
export async function getRecord<RecordType>(
db: D1Database,
SQL: string,
bindings: any[]
): Promise<RecordType | null> {
try {
bindings = cleanBindings(bindings)
const res: any = await db
.prepare(SQL)
.bind(...bindings)
.first()
if (res) {
return format<RecordType>(res)
} else {
return null
}
} catch (e: any) {
console.error(e.message, e.cause?.message)
throw e
}
}
export async function updateRecord(
db: D1Database,
SQL: string,
bindings: any[]
) {
try {
bindings = cleanBindings(bindings)
return await db
.prepare(SQL)
.bind(...bindings)
.run()
} catch (e: any) {
console.error(e.message, e.cause?.message)
throw e
}
}
export async function deleteRecord(
db: D1Database,
SQL: string,
bindings: any[]
) {
try {
bindings = cleanBindings(bindings)
await db
.prepare(SQL)
.bind(...bindings)
.run()
} catch (e: any) {
console.error(e.message, e.cause?.message)
throw e
}
}
export function D1Adapter(db: D1Database): Adapter {
// we need to run migrations if we dont have the right tables
return {
async createUser(user) {
const id: string = crypto.randomUUID()
const createBindings = [
id,
user.name,
user.email,
user.emailVerified?.toISOString(),
user.image,
]
const getBindings = [id]
const newUser = await createRecord<AdapterUser>(
db,
CREATE_USER_SQL,
createBindings,
GET_USER_BY_ID_SQL,
getBindings
)
if (newUser) return newUser
throw new Error("Error creating user: Cannot get user after creation.")
},
async getUser(id) {
return await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [id])
},
async getUserByEmail(email) {
return await getRecord<AdapterUser>(db, GET_USER_BY_EMAIL_SQL, [email])
},
async getUserByAccount({ providerAccountId, provider }) {
return await getRecord<AdapterUser>(db, GET_USER_BY_ACCOUNTL_SQL, [
providerAccountId,
provider,
])
},
async updateUser(user) {
const params = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [
user.id,
])
if (params) {
// copy any properties not in the update into the existing one and use that for bind params
// covers the scenario where the user arg doesnt have all of the current users properties
Object.assign(params, user)
const res = await updateRecord(db, UPDATE_USER_BY_ID_SQL, [
params.name,
params.email,
params.emailVerified?.toISOString(),
params.image,
params.id,
])
if (res.success) {
const user = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [
params.id,
])
if (user) return user
throw new Error(
"Error updating user: Cannot get user after updating."
)
}
}
throw new Error("Error updating user: Failed to run the update SQL.")
},
async deleteUser(userId) {
// miniflare doesn't support batch operations or multiline sql statements
await deleteRecord(db, DELETE_ACCOUNT_BY_USER_ID_SQL, [userId])
await deleteRecord(db, DELETE_SESSION_BY_USER_ID_SQL, [userId])
await deleteRecord(db, DELETE_USER_SQL, [userId])
return null
},
async linkAccount(a) {
// convert user_id to userId and provider_account_id to providerAccountId
const id = crypto.randomUUID()
const createBindings = [
id,
a.userId,
a.type,
a.provider,
a.providerAccountId,
a.refresh_token,
a.access_token,
a.expires_at,
a.token_type,
a.scope,
a.id_token,
a.session_state,
a.oauth_token ?? null,
a.oauth_token_secret ?? null,
]
const getBindings = [id]
return await createRecord<AdapterAccount>(
db,
CREATE_ACCOUNT_SQL,
createBindings,
GET_ACCOUNT_BY_ID_SQL,
getBindings
)
},
async unlinkAccount({ providerAccountId, provider }) {
await deleteRecord(
db,
DELETE_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL,
[provider, providerAccountId]
)
},
async createSession({ sessionToken, userId, expires }) {
const id = crypto.randomUUID()
const createBindings = [id, sessionToken, userId, expires.toISOString()]
const getBindings = [sessionToken]
const session = await createRecord<AdapterSession>(
db,
CREATE_SESSION_SQL,
createBindings,
GET_SESSION_BY_TOKEN_SQL,
getBindings
)
if (session) return session
throw new Error(`Couldn't create session`)
},
async getSessionAndUser(sessionToken) {
const session: any = await getRecord<AdapterSession>(
db,
GET_SESSION_BY_TOKEN_SQL,
[sessionToken]
)
if (session === null) return null
const user = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [
session.userId,
])
if (user === null) return null
return { session, user }
},
async updateSession({ sessionToken, expires }) {
if (expires === undefined) {
await deleteRecord(db, DELETE_SESSION_SQL, [sessionToken])
return null
}
const session = await getRecord<AdapterSession>(
db,
GET_SESSION_BY_TOKEN_SQL,
[sessionToken]
)
if (!session) return null
session.expires = expires
await updateRecord(db, UPDATE_SESSION_BY_SESSION_TOKEN_SQL, [
expires?.toISOString(),
sessionToken,
])
return await db
.prepare(UPDATE_SESSION_BY_SESSION_TOKEN_SQL)
.bind(expires?.toISOString(), sessionToken)
.first()
},
async deleteSession(sessionToken) {
await deleteRecord(db, DELETE_SESSION_SQL, [sessionToken])
return null
},
async createVerificationToken({ identifier, expires, token }) {
return await createRecord(
db,
CREATE_VERIFICATION_TOKEN_SQL,
[identifier, expires.toISOString(), token],
GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL,
[identifier, token]
)
},
async useVerificationToken({ identifier, token }) {
const verificationToken = await getRecord<AdapterVerificationToken>(
db,
GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL,
[identifier, token]
)
if (!verificationToken) return null
await deleteRecord(db, DELETE_VERIFICATION_TOKEN_SQL, [identifier, token])
return verificationToken
},
}
}