Compare commits

...

3 Commits

Author SHA1 Message Date
Balázs Orbán
5cb75a5b0a use single file 2023-02-17 15:36:28 +01:00
Balázs Orbán
c1d8b446aa fix import 2023-02-17 15:31:20 +01:00
Thang Vu
d10d6e20ca fix(adapters): comply to Node.js native ESM resolver for Firebase adapter 2023-02-17 21:19:22 +07:00
3 changed files with 167 additions and 180 deletions

View File

@@ -22,20 +22,22 @@
* @module @next-auth/firebase-adapter
*/
import { type AppOptions } from "firebase-admin"
import { Firestore } from "firebase-admin/firestore"
import { type AppOptions, getApps, initializeApp } from "firebase-admin/app"
import type { Adapter, AdapterUser } from "next-auth/adapters"
import {
collestionsFactory,
deleteDocs,
initFirestore,
getDoc,
getOneDoc,
mapFieldsFactory,
} from "./utils"
Firestore,
getFirestore,
initializeFirestore,
Timestamp,
} from "firebase-admin/firestore"
export { initFirestore } from "./utils"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"
/** Configure the Firebase Adapter. */
export interface FirebaseAdapterConfig extends AppOptions {
@@ -300,3 +302,156 @@ export function FirestoreAdapter(
},
}
}
// for consistency, store all fields as snake_case in the database
const MAP_TO_FIRESTORE: Record<string, string | undefined> = {
userId: "user_id",
sessionToken: "session_token",
providerAccountId: "provider_account_id",
emailVerified: "email_verified",
}
const MAP_FROM_FIRESTORE: Record<string, string | undefined> = {}
for (const key in MAP_TO_FIRESTORE) {
MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]!] = key
}
const identity = <T>(x: T) => x
/** @internal */
export function mapFieldsFactory(preferSnakeCase?: boolean) {
if (preferSnakeCase) {
return {
toDb: (field: string) => MAP_TO_FIRESTORE[field] ?? field,
fromDb: (field: string) => MAP_FROM_FIRESTORE[field] ?? field,
}
}
return { toDb: identity, fromDb: identity }
}
/** @internal */
function getConverter<Document extends Record<string, any>>(options: {
excludeId?: boolean
preferSnakeCase?: boolean
}): FirebaseFirestore.FirestoreDataConverter<Document> {
const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false)
return {
toFirestore(object) {
const document: Record<string, unknown> = {}
for (const key in object) {
if (key === "id") continue
const value = object[key]
if (value !== undefined) {
document[mapper.toDb(key)] = value
} else {
console.warn(`FirebaseAdapter: value for key "${key}" is undefined`)
}
}
return document
},
fromFirestore(
snapshot: FirebaseFirestore.QueryDocumentSnapshot<Document>
): Document {
const document = snapshot.data()! // we can guarantee it exists
const object: Record<string, unknown> = {}
if (!options?.excludeId) {
object.id = snapshot.id
}
for (const key in document) {
let value: any = document[key]
if (value instanceof Timestamp) value = value.toDate()
object[mapper.fromDb(key)] = value
}
return object as Document
},
}
}
/** @internal */
export async function getOneDoc<T>(
querySnapshot: FirebaseFirestore.Query<T>
): Promise<T | null> {
const querySnap = await querySnapshot.limit(1).get()
return querySnap.docs[0]?.data() ?? null
}
/** @internal */
async function deleteDocs<T>(
querySnapshot: FirebaseFirestore.Query<T>
): Promise<void> {
const querySnap = await querySnapshot.get()
for (const doc of querySnap.docs) {
await doc.ref.delete()
}
}
/** @internal */
export async function getDoc<T>(
docRef: FirebaseFirestore.DocumentReference<T>
): Promise<T | null> {
const docSnap = await docRef.get()
return docSnap.data() ?? null
}
/** @internal */
export function collestionsFactory(
db: FirebaseFirestore.Firestore,
preferSnakeCase = false
) {
return {
users: db
.collection("users")
.withConverter(getConverter<AdapterUser>({ preferSnakeCase })),
sessions: db
.collection("sessions")
.withConverter(getConverter<AdapterSession>({ preferSnakeCase })),
accounts: db
.collection("accounts")
.withConverter(getConverter<AdapterAccount>({ preferSnakeCase })),
verification_tokens: db
.collection(
preferSnakeCase ? "verification_tokens" : "verificationTokens"
)
.withConverter(
getConverter<VerificationToken>({ preferSnakeCase, excludeId: true })
),
}
}
/**
* Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
* If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
*
* @example
* ```ts title="lib/firestore.ts"
* import { initFirestore } from "@next-auth/firebase-adapter"
* import { cert } from "firebase-admin/app"
*
* export const firestore = initFirestore({
* credential: cert({
* projectId: process.env.FIREBASE_PROJECT_ID,
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
* })
* })
* ```
*/
export function initFirestore(
options: AppOptions & { name?: FirebaseAdapterConfig["name"] } = {}
) {
const apps = getApps()
const app = options.name ? apps.find((a) => a.name === options.name) : apps[0]
if (app) return getFirestore(app)
return initializeFirestore(initializeApp(options, options.name))
}

View File

@@ -1,168 +0,0 @@
import { AppOptions, getApps, initializeApp } from "firebase-admin/app"
import {
getFirestore,
initializeFirestore,
Timestamp,
} from "firebase-admin/firestore"
import type {
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"
import { FirebaseAdapterConfig } from "."
// for consistency, store all fields as snake_case in the database
const MAP_TO_FIRESTORE: Record<string, string | undefined> = {
userId: "user_id",
sessionToken: "session_token",
providerAccountId: "provider_account_id",
emailVerified: "email_verified",
}
const MAP_FROM_FIRESTORE: Record<string, string | undefined> = {}
for (const key in MAP_TO_FIRESTORE) {
MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]!] = key
}
const identity = <T>(x: T) => x
/** @internal */
export function mapFieldsFactory(preferSnakeCase?: boolean) {
if (preferSnakeCase) {
return {
toDb: (field: string) => MAP_TO_FIRESTORE[field] ?? field,
fromDb: (field: string) => MAP_FROM_FIRESTORE[field] ?? field,
}
}
return { toDb: identity, fromDb: identity }
}
/** @internal */
export function getConverter<Document extends Record<string, any>>(options: {
excludeId?: boolean
preferSnakeCase?: boolean
}): FirebaseFirestore.FirestoreDataConverter<Document> {
const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false)
return {
toFirestore(object) {
const document: Record<string, unknown> = {}
for (const key in object) {
if (key === "id") continue
const value = object[key]
if (value !== undefined) {
document[mapper.toDb(key)] = value
} else {
console.warn(`FirebaseAdapter: value for key "${key}" is undefined`)
}
}
return document
},
fromFirestore(
snapshot: FirebaseFirestore.QueryDocumentSnapshot<Document>
): Document {
const document = snapshot.data()! // we can guarantee it exists
const object: Record<string, unknown> = {}
if (!options?.excludeId) {
object.id = snapshot.id
}
for (const key in document) {
let value: any = document[key]
if (value instanceof Timestamp) value = value.toDate()
object[mapper.fromDb(key)] = value
}
return object as Document
},
}
}
/** @internal */
export async function getOneDoc<T>(
querySnapshot: FirebaseFirestore.Query<T>
): Promise<T | null> {
const querySnap = await querySnapshot.limit(1).get()
return querySnap.docs[0]?.data() ?? null
}
/** @internal */
export async function deleteDocs<T>(
querySnapshot: FirebaseFirestore.Query<T>
): Promise<void> {
const querySnap = await querySnapshot.get()
for (const doc of querySnap.docs) {
await doc.ref.delete()
}
}
/** @internal */
export async function getDoc<T>(
docRef: FirebaseFirestore.DocumentReference<T>
): Promise<T | null> {
const docSnap = await docRef.get()
return docSnap.data() ?? null
}
/** @internal */
export function collestionsFactory(
db: FirebaseFirestore.Firestore,
preferSnakeCase = false
) {
return {
users: db
.collection("users")
.withConverter(getConverter<AdapterUser>({ preferSnakeCase })),
sessions: db
.collection("sessions")
.withConverter(getConverter<AdapterSession>({ preferSnakeCase })),
accounts: db
.collection("accounts")
.withConverter(getConverter<AdapterAccount>({ preferSnakeCase })),
verification_tokens: db
.collection(
preferSnakeCase ? "verification_tokens" : "verificationTokens"
)
.withConverter(
getConverter<VerificationToken>({ preferSnakeCase, excludeId: true })
),
}
}
/**
* Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
* If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
*
* @example
* ```ts title="lib/firestore.ts"
* import { initFirestore } from "@next-auth/firebase-adapter"
* import { cert } from "firebase-admin/app"
*
* export const firestore = initFirestore({
* credential: cert({
* projectId: process.env.FIREBASE_PROJECT_ID,
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
* })
* })
* ```
*/
export function initFirestore(
options: AppOptions & { name?: FirebaseAdapterConfig["name"] } = {}
) {
const apps = getApps()
const app = options.name ? apps.find((a) => a.name === options.name) : apps[0]
if (app) return getFirestore(app)
return initializeFirestore(initializeApp(options, options.name))
}

View File

@@ -7,7 +7,7 @@ import {
getDoc,
getOneDoc,
mapFieldsFactory,
} from "../src/utils"
} from "../src"
describe.each([
{ namingStrategy: "snake_case" },