@auth/fauna-adapter
Version:
Fauna Adapter for Auth.js
224 lines (212 loc) • 7.48 kB
text/typescript
/**
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: "16px"}}>
* <p>Official <a href="https://docs.fauna.com/fauna/current/">Fauna</a> adapter for Auth.js / NextAuth.js.</p>
* <a href="https://fauna.com/features">
* <img style={{display: "block"}} src="https://authjs.dev/img/adapters/fauna.svg" width="64" />
* </a>
* </div>
*
* ## Installation
*
* ```bash npm2yarn
* npm install @auth/fauna-adapter fauna
* ```
*
* @module @auth/fauna-adapter
*/
import {
Client,
TimeStub,
fql,
NullDocument,
QueryValue,
QueryValueObject,
} from "fauna"
import type {
Adapter,
AdapterUser,
AdapterSession,
VerificationToken,
AdapterAccount,
} from "@auth/core/adapters"
type ToFauna<T> = {
[P in keyof T]: T[P] extends Date | null
? TimeStub | null
: T[P] extends undefined
? null
: T[P] extends QueryValue
? T[P]
: QueryValueObject
}
export type FaunaUser = ToFauna<AdapterUser>
export type FaunaSession = ToFauna<AdapterSession>
export type FaunaVerificationToken = ToFauna<VerificationToken> & { id: string }
export type FaunaAccount = ToFauna<AdapterAccount>
type AdapterConfig = {
collectionNames: {
user: string
session: string
account: string
verificationToken: string
}
}
const defaultCollectionNames = {
user: "User",
session: "Session",
account: "Account",
verificationToken: "VerificationToken",
}
export function FaunaAdapter(client: Client, config?: AdapterConfig): Adapter {
const { collectionNames = defaultCollectionNames } = config || {}
return {
async createUser(user) {
const response = await client.query<FaunaUser>(
fql`Collection(${collectionNames.user}).create(${format.to(user)})`
)
return format.from(response.data)
},
async getUser(id) {
const response = await client.query<FaunaUser | NullDocument>(
fql`Collection(${collectionNames.user}).byId(${id})`
)
if (response.data instanceof NullDocument) return null
return format.from(response.data)
},
async getUserByEmail(email) {
const response = await client.query<FaunaUser>(
fql`Collection(${collectionNames.user}).byEmail(${email}).first()`
)
if (response.data === null) return null
return format.from(response.data)
},
async getUserByAccount({ provider, providerAccountId }) {
const response = await client.query<FaunaUser>(fql`
let account = Collection(${collectionNames.account}).byProviderAndProviderAccountId(${provider}, ${providerAccountId}).first()
if (account != null) {
Collection(${collectionNames.user}).byId(account.userId)
} else {
null
}
`)
return format.from(response.data)
},
async updateUser(user) {
const _user: Partial<AdapterUser> = { ...user }
delete _user.id
const response = await client.query<FaunaUser>(
fql`Collection(${collectionNames.user}).byId(${
user.id
}).update(${format.to(_user)})`
)
return format.from(response.data)
},
async deleteUser(userId) {
await client.query(fql`
// Delete the user's sessions
Collection(${collectionNames.session}).byUserId(${userId}).forEach(session => session.delete())
// Delete the user's accounts
Collection(${collectionNames.account}).byUserId(${userId}).forEach(account => account.delete())
// Delete the user
Collection(${collectionNames.user}).byId(${userId}).delete()
`)
},
async linkAccount(account) {
await client.query<FaunaAccount>(
fql`Collection(${collectionNames.account}).create(${format.to(
account
)})`
)
return account
},
async unlinkAccount({ provider, providerAccountId }) {
const response = await client.query<FaunaAccount>(
fql`Collection(${collectionNames.account}).byProviderAndProviderAccountId(${provider}, ${providerAccountId}).first().delete()`
)
return format.from<AdapterAccount>(response.data)
},
async getSessionAndUser(sessionToken) {
const response = await client.query<[FaunaUser, FaunaSession]>(fql`
let session = Collection(${collectionNames.session}).bySessionToken(${sessionToken}).first()
if (session != null) {
let user = Collection(${collectionNames.user}).byId(session.userId)
if (user != null) {
[user, session]
} else {
null
}
} else {
null
}
`)
if (response.data === null) return null
const [user, session] = response.data ?? []
return { session: format.from(session), user: format.from(user) }
},
async createSession(session) {
await client.query<FaunaSession>(
fql`Collection(${collectionNames.session}).create(${format.to(
session
)})`
)
return session
},
async updateSession(session) {
const response = await client.query<FaunaSession>(
fql`Collection(${collectionNames.session}).bySessionToken(${
session.sessionToken
}).first().update(${format.to(session)})`
)
return format.from(response.data)
},
async deleteSession(sessionToken) {
await client.query(
fql`Collection(${collectionNames.session}).bySessionToken(${sessionToken}).first().delete()`
)
},
async createVerificationToken(verificationToken) {
await client.query<FaunaVerificationToken>(
fql`Collection(${collectionNames.verificationToken}).create(${format.to(
verificationToken
)})`
)
return verificationToken
},
async useVerificationToken({ identifier, token }) {
const response = await client.query<FaunaVerificationToken>(
fql`Collection(${collectionNames.verificationToken}).byIdentifierAndToken(${identifier}, ${token}).first()`
)
if (response.data === null) return null
// Delete the verification token so it can only be used once
await client.query(
fql`Collection(${collectionNames.verificationToken}).byId(${response.data.id}).delete()`
)
const _verificationToken: Partial<FaunaVerificationToken> = {
...response.data,
}
delete _verificationToken.id
return format.from(_verificationToken)
},
}
}
export const format = {
/** Takes an object that's coming from the database and converts it to plain JavaScript. */
from<T>(object: Record<string, any> = {}): T {
if (!object) return null as unknown as T
const newObject: Record<string, unknown> = {}
for (const [key, value] of Object.entries(object))
if (key === "coll" || key === "ts") continue
else if (value instanceof TimeStub) newObject[key] = value.toDate()
else newObject[key] = value
return newObject as T
},
/** Takes an object that's coming from Auth.js and prepares it to be written to the database. */
to<T>(object: Record<string, any>): T {
const newObject: Record<string, unknown> = {}
for (const [key, value] of Object.entries(object))
if (value instanceof Date) newObject[key] = TimeStub.fromDate(value)
else if (typeof value === "string" && !isNaN(Date.parse(value)))
newObject[key] = TimeStub.from(value)
else newObject[key] = value ?? null
return newObject as T
},
}