Skip to content

Instantly share code, notes, and snippets.

@eropple
Last active July 29, 2022 19:13
Show Gist options
  • Save eropple/f6d99103cc125e74fb714642af7ffe79 to your computer and use it in GitHub Desktop.
Save eropple/f6d99103cc125e74fb714642af7ffe79 to your computer and use it in GitHub Desktop.
next-auth: EdgeDBAdapter (early take)
# don't blame me for the mixed case, it's a next-auth implementation detail
module default {
type Account {
required property type -> str;
required property provider -> str;
required property providerAccountId -> str;
optional property refresh_token -> str;
optional property access_token -> str;
optional property id_token -> str;
optional property expires_at -> int64;
optional property token_type -> str;
optional property scope -> str;
optional property session_state -> str;
required link user -> User;
}
type Session {
required property sessionToken -> str {
constraint exclusive;
}
required property expires -> datetime;
required link user -> User;
}
type User {
optional property name -> str;
optional property email -> str {
constraint exclusive on (str_lower(__subject__));
}
optional property emailVerified -> datetime;
optional property image -> str;
multi link accounts := .<user[is Account];
multi link sessions := .<user[is Session];
}
type VerificationToken {
required property identifier -> str { constraint exclusive };
required property token -> str;
required property expires -> datetime;
constraint exclusive on ( (.identifier, .token) );
}
}
import { Account, DefaultAccount, DefaultUser } from 'next-auth';
import { Adapter, AdapterSession, AdapterUser, VerificationToken } from 'next-auth/adapters';
import { ProviderType } from 'next-auth/providers';
import { Client as EdgeDBClient } from 'edgedb';
import { edgeQuery, edgeTypes as t } from '../edgedb';
export class EdgeDBAdapterError extends Error {}
const USER_SHAPE = {
id: true,
name: true,
email: true,
emailVerified: true,
image: true,
} as const;
const ACCOUNT_SHAPE = {
type: true,
provider: true,
providerAccountId: true,
refresh_token: true,
access_token: true,
id_token: true,
expires_at: true,
token_type: true,
scope: true,
session_state: true,
} as const;
const SESSION_SHAPE = {
id: true,
sessionToken: true,
expires: true,
} as const;
const VERIFICATION_TOKEN_SHAPE = {
identifier: true,
token: true,
expires: true,
} as const;
export default function EdgeDBAdapter(
edgedb: EdgeDBClient,
e: typeof edgeQuery,
): Adapter {
async function createUser(rawUser: Omit<AdapterUser, 'id'>): Promise<AdapterUser> {
const user = rawUser as Omit<DefaultUser, 'id'>;
const { id } = await e.insert(e.User, {
...user,
email: user.email?.toLowerCase(),
}).run(edgedb);
const result = await getUser(id);
if (!result) {
throw new EdgeDBAdapterError("When creating user, edgedb allowed insert but returned no items.");
}
return result;
}
async function getUser(id: string): Promise<AdapterUser | null> {
return e.select(e.User, u => ({
...USER_SHAPE,
filter: e.op(u.id, '=', e.uuid(id)),
})).run(edgedb);
}
async function getUserByEmail(email: string): Promise<AdapterUser | null> {
return (await e.select(e.User, u => ({
...USER_SHAPE,
filter: e.op(u.email, 'ilike', email),
})).run(edgedb))[0];
}
async function getUserByAccount(p: Pick<Account, 'provider' | 'providerAccountId'>): Promise<AdapterUser | null> {
const result = await e.select(e.Account, a => ({
...ACCOUNT_SHAPE,
user: {
...USER_SHAPE,
},
filter: e.op(
e.op(a.provider, '=', p.provider),
'and',
e.op(a.providerAccountId, '=', p.providerAccountId),
),
})).run(edgedb);
return result[0]?.user;
}
async function updateUser(user: Partial<AdapterUser>): Promise<AdapterUser> {
if (!user.id) {
throw new EdgeDBAdapterError("updateUser: user.id must be defined.");
}
const updateResult = await e.update(e.User, u => ({
filter: e.op(u.id, '=', e.uuid(user.id!)),
set: {
...user,
}
})).run(edgedb);
if (!updateResult) {
throw new EdgeDBAdapterError("updateUser: attempted to update, but got null back from edgedb");
}
return (await getUser(updateResult.id))!;
}
async function deleteUser(userId: string): Promise<AdapterUser | null | undefined> {
const user = await getUser(userId);
if (!user) {
return undefined;
}
const deleteResult = await e.delete(e.User, u => ({
filter: e.op(u.id, '=', e.uuid(user.id)),
})).run(edgedb);
if (deleteResult === null || deleteResult.id !== user.id) {
throw new Error("Error while deleting; found user but no result when deleting.");
}
return user;
}
async function linkAccount(acct: Account): Promise<Account | null | undefined> {
const account = { ...acct } as DefaultAccount;
delete (account as any).userId;
const insertResult = await e.insert(e.Account, {
...account,
user: e.select(e.User, u => ({
filter: e.op(u.id, '=', e.uuid(acct.userId)),
})),
}).run(edgedb);
const ret = await e.select(e.Account, a => ({
...ACCOUNT_SHAPE,
user: USER_SHAPE,
filter: e.op(a.id, '=', e.uuid(insertResult.id)),
})).run(edgedb);
if (!ret) {
throw new EdgeDBAdapterError(`linkAccount: inserted account but not found on select: ${insertResult.id}`);
}
const userId = ret.user.id;
delete (ret as any).user;
return {
...ret,
type: ret.type as ProviderType,
scope: ret.scope ?? undefined,
access_token: ret.access_token ?? undefined,
token_type: ret.token_type ?? undefined,
id_token: ret.id_token ?? undefined,
refresh_token: ret.refresh_token ?? undefined,
expires_at: ret.expires_at ?? undefined,
session_state: ret.session_state ?? undefined,
userId,
};
}
async function unlinkAccount(acct: Pick<Account, 'provider' | 'providerAccountId'>): Promise<Account | undefined> {
const foundAccount = (await e.select(e.Account, a => ({
...ACCOUNT_SHAPE,
user: USER_SHAPE,
filter: e.op(
e.op(a.provider, '=', acct.provider),
'and',
e.op(a.providerAccountId, '=', acct.providerAccountId),
),
})).run(edgedb))[0];
if (!foundAccount) {
return undefined;
}
const deleteResult = (await e.delete(e.Account, a => ({
filter: e.op(
e.op(a.provider, '=', acct.provider),
'and',
e.op(a.providerAccountId, '=', acct.providerAccountId),
)
})).run(edgedb))[0];
const user = foundAccount.user;
delete (foundAccount as any).user;
return deleteResult ? {
...foundAccount,
type: foundAccount.type as ProviderType,
scope: foundAccount.scope ?? undefined,
access_token: foundAccount.access_token ?? undefined,
token_type: foundAccount.token_type ?? undefined,
id_token: foundAccount.id_token ?? undefined,
refresh_token: foundAccount.refresh_token ?? undefined,
expires_at: foundAccount.expires_at ?? undefined,
session_state: foundAccount.session_state ?? undefined,
userId: user.id,
} : undefined;
}
async function createSession(session: { sessionToken: string; userId: string; expires: Date; }): Promise<AdapterSession> {
const createResult = await e.insert(e.Session, {
sessionToken: session.sessionToken,
expires: session.expires,
user: e.select(e.User, u => ({
filter: e.op(u.id, '=', e.uuid(session.userId)),
})),
}).run(edgedb);
const ret = await e.select(e.Session, s => ({
...SESSION_SHAPE,
user: {
...USER_SHAPE,
},
filter: e.op(s.id, '=', e.uuid(createResult.id)),
})).run(edgedb);
if (!ret) {
throw new EdgeDBAdapterError("Attempted to select just-inserted session; returned null.");
}
const userId = ret.user.id;
delete (ret as any).user;
return { ...ret, userId };
}
async function getSessionAndUser(sessionToken: string): Promise<{ session: AdapterSession; user: AdapterUser; } | null> {
const session = await e.select(e.Session, s => ({
...SESSION_SHAPE,
user: {
...USER_SHAPE,
},
filter: e.op(s.sessionToken, '=', sessionToken),
})).run(edgedb);
if (!session) {
return null;
}
const user = session.user;
delete (session as any).user;
return { session: { ...session, userId: user.id }, user };
}
async function updateSession(session: Partial<AdapterSession> & Pick<AdapterSession, 'sessionToken'>): Promise<AdapterSession | null | undefined> {
const updateResult = await e.update(e.Session, s => ({
filter: e.op(s.sessionToken, '=', session.sessionToken),
set: {
...session,
},
})).run(edgedb);
// we are not going to yell if this failed because it's probably pretty reasonable
// for the session to have been invalidated/deleted since then (active use cases)
return (await getSessionAndUser(session.sessionToken))?.session;
}
async function deleteSession(sessionToken: string): Promise<AdapterSession | null | undefined> {
const session = await e.select(e.Session, s => ({
...SESSION_SHAPE,
user: { id: true },
filter: e.op(s.sessionToken, '=', sessionToken),
})).run(edgedb);
if (!session) {
return undefined;
}
const deleteResult = await e.delete(e.Session, s => ({
...SESSION_SHAPE,
filter: e.op(s.sessionToken, '=', sessionToken),
})).run(edgedb);
if (deleteResult === null || deleteResult.id !== session.id) {
throw new Error("Error while deleting; found session but no result when deleting.");
}
return { ...(session as any), userId: session.user.id };
}
async function createVerificationToken(verificationToken: VerificationToken): Promise<VerificationToken | null | undefined> {
const createResult = await e.insert(e.VerificationToken, {
...verificationToken,
}).run(edgedb);
return useVerificationToken(verificationToken);
}
async function useVerificationToken(params: { identifier: string; token: string; }): Promise<VerificationToken | null> {
return (await e.select(e.VerificationToken, vt => ({
...VERIFICATION_TOKEN_SHAPE,
filter: e.op(
e.op(vt.identifier, '=', params.identifier),
'and',
e.op(vt.token, '=', params.token),
),
})).run(edgedb))[0];
};
return {
createUser,
createSession,
createVerificationToken,
linkAccount,
getUser,
getSessionAndUser,
getUserByAccount,
getUserByEmail,
useVerificationToken,
updateSession,
updateUser,
deleteSession,
deleteUser,
unlinkAccount
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment