@auth/d1-adapter
Version:
A Cloudflare D1 adapter for Auth.js
241 lines (240 loc) • 9.49 kB
JavaScript
/**
* <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 { 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";
// format is borrowed from the supabase adapter, graciously
function format(obj) {
for (const [key, value] of Object.entries(obj)) {
if (value === null) {
delete obj[key];
}
if (isDate(value)) {
obj[key] = new Date(value);
}
}
return obj;
}
// D1 doesnt like undefined, it wants null when calling bind
function cleanBindings(bindings) {
return bindings.map((e) => (e === undefined ? null : e));
}
export async function createRecord(db, CREATE_SQL, bindings, GET_SQL, getBindings) {
try {
bindings = cleanBindings(bindings);
await db
.prepare(CREATE_SQL)
.bind(...bindings)
.run();
return await getRecord(db, GET_SQL, getBindings);
}
catch (e) {
console.error(e.message, e.cause?.message);
throw e;
}
}
export async function getRecord(db, SQL, bindings) {
try {
bindings = cleanBindings(bindings);
const res = await db
.prepare(SQL)
.bind(...bindings)
.first();
if (res) {
return format(res);
}
else {
return null;
}
}
catch (e) {
console.error(e.message, e.cause?.message);
throw e;
}
}
export async function updateRecord(db, SQL, bindings) {
try {
bindings = cleanBindings(bindings);
return await db
.prepare(SQL)
.bind(...bindings)
.run();
}
catch (e) {
console.error(e.message, e.cause?.message);
throw e;
}
}
export async function deleteRecord(db, SQL, bindings) {
try {
bindings = cleanBindings(bindings);
await db
.prepare(SQL)
.bind(...bindings)
.run();
}
catch (e) {
console.error(e.message, e.cause?.message);
throw e;
}
}
export function D1Adapter(db) {
// we need to run migrations if we dont have the right tables
return {
async createUser(user) {
const id = crypto.randomUUID();
const createBindings = [
id,
user.name,
user.email,
user.emailVerified?.toISOString(),
user.image,
];
const getBindings = [id];
const newUser = await createRecord(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(db, GET_USER_BY_ID_SQL, [id]);
},
async getUserByEmail(email) {
return await getRecord(db, GET_USER_BY_EMAIL_SQL, [email]);
},
async getUserByAccount({ providerAccountId, provider }) {
return await getRecord(db, GET_USER_BY_ACCOUNTL_SQL, [
providerAccountId,
provider,
]);
},
async updateUser(user) {
const params = await getRecord(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(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(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(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 = await getRecord(db, GET_SESSION_BY_TOKEN_SQL, [sessionToken]);
if (session === null)
return null;
const user = await getRecord(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(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(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;
},
};
}