From f96be2bdcd56740652ee8aedd59a28a232ce6cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Thu, 19 Jan 2023 15:38:17 +0100 Subject: [PATCH] pass config instead of instance, publish as ESM --- .gitignore | 13 +- packages/adapter-firebase/package.json | 12 +- packages/adapter-firebase/src/index.ts | 186 ++++-------------------- packages/adapter-firebase/src/utils.ts | 149 +++++++++++++++++++ packages/adapter-firebase/tsconfig.json | 23 ++- 5 files changed, 207 insertions(+), 176 deletions(-) create mode 100644 packages/adapter-firebase/src/utils.ts diff --git a/.gitignore b/.gitignore index 2106f1aa..8e73647c 100644 --- a/.gitignore +++ b/.gitignore @@ -35,13 +35,11 @@ packages/next-auth/utils packages/next-auth/core packages/next-auth/jwt packages/next-auth/react -packages/next-auth/adapters.d.ts -packages/next-auth/adapters.js -packages/next-auth/index.d.ts -packages/next-auth/index.js packages/next-auth/next -packages/next-auth/middleware.d.ts -packages/next-auth/middleware.js +packages/*/*.js +packages/*/*.d.ts +packages/*/*.d.ts.map + # Development app apps/dev/src/css @@ -82,9 +80,6 @@ docs/.docusaurus docs/providers.json # Core -packages/core/*.js -packages/core/*.d.ts -packages/core/*.d.ts.map packages/core/lib packages/core/providers packages/core/src/lib/pages/styles.ts diff --git a/packages/adapter-firebase/package.json b/packages/adapter-firebase/package.json index 801841e3..de1ef163 100644 --- a/packages/adapter-firebase/package.json +++ b/packages/adapter-firebase/package.json @@ -12,10 +12,16 @@ "Nico Domino ", "Alex Meuer " ], - "main": "dist/index.js", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js" + } + }, "files": [ - "dist", - "index.d.ts" + "*.js", + "*.d.ts" ], "license": "ISC", "keywords": [ diff --git a/packages/adapter-firebase/src/index.ts b/packages/adapter-firebase/src/index.ts index 019d2301..0512f031 100644 --- a/packages/adapter-firebase/src/index.ts +++ b/packages/adapter-firebase/src/index.ts @@ -18,156 +18,25 @@ * @module @next-auth/firebase-adapter */ -import { firestore } from "firebase-admin" +import { type AppOptions } from "firebase-admin/app" -import type { - Adapter, - AdapterUser, - AdapterAccount, - AdapterSession, - VerificationToken, -} from "next-auth/adapters" - -// for consistency, store all fields as snake_case in the database -const MAP_TO_FIRESTORE: Record = { - userId: "user_id", - sessionToken: "session_token", - providerAccountId: "provider_account_id", - emailVerified: "email_verified", -} -const MAP_FROM_FIRESTORE: Record = {} - -for (const key in MAP_TO_FIRESTORE) { - MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]!] = key -} - -const identity = (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>(options: { - excludeId?: boolean - preferSnakeCase?: boolean -}): FirebaseFirestore.FirestoreDataConverter { - const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false) - - return { - toFirestore(object) { - const document: Record = {} - - for (const key in object) { - if (key === "id") continue - const value = object[key] - if (value !== undefined) { - document[mapper.toDb(key)] = value - } else { - console.warn( - `FirestoreAdapterAdmin: value for key "${key}" is undefined` - ) - } - } - - return document - }, - - fromFirestore( - snapshot: FirebaseFirestore.QueryDocumentSnapshot - ): Document { - const document = snapshot.data()! // we can guarentee it exists - - const object: Record = {} - - if (!options?.excludeId) { - object.id = snapshot.id - } - - for (const key in document) { - let value: any = document[key] - if (value instanceof firestore.Timestamp) value = value.toDate() - - object[mapper.fromDb(key)] = value - } - - return object as Document - }, - } -} - -/** @internal */ -export async function getOneDoc( - querySnapshot: FirebaseFirestore.Query -): Promise { - const querySnap = await querySnapshot.limit(1).get() - return querySnap.docs[0]?.data() ?? null -} - -/** @internal */ -export async function deleteDocs( - querySnapshot: FirebaseFirestore.Query -): Promise { - const querySnap = await querySnapshot.get() - for (const doc of querySnap.docs) { - await doc.ref.delete() - } -} - -/** @internal */ -export async function getDoc( - docRef: FirebaseFirestore.DocumentReference -): Promise { - 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({ preferSnakeCase })), - sessions: db - .collection("sessions") - .withConverter(getConverter({ preferSnakeCase })), - accounts: db - .collection("accounts") - .withConverter(getConverter({ preferSnakeCase })), - verification_tokens: db - .collection( - preferSnakeCase ? "verification_tokens" : "verificationTokens" - ) - .withConverter( - getConverter({ preferSnakeCase, excludeId: true }) - ), - } -} +import type { Adapter, AdapterUser } from "next-auth/adapters" +import { + collestionsFactory, + deleteDocs, + firestore, + getDoc, + getOneDoc, + mapFieldsFactory, +} from "./utils" /** Configure the Firebase Adapter. */ -export interface FirestoreAdapterConfig { +export interface FirebaseAdapterConfig extends AppOptions { /** - * A Firestore instance using the Firebase Admin SDK. - * @example - * ```ts - * import admin from "firebase-admin" - * const app = admin.initializeApp() - * const firestore = app.firestore() - * ``` - * - * @see [Firebase Admin SDK setup](https://firebase.google.com/docs/admin/setup) + * The ID of the Google Cloud project associated with the App. + * @default "authjs-firebase-adapter" */ - db: FirebaseFirestore.Firestore + projectId?: string /** * Use this option if mixed `snake_case` and `camelCase` field names in the database is an issue for you. * Passing `snake_case` convert all field and collection names to `snake_case`. @@ -183,26 +52,23 @@ export interface FirestoreAdapterConfig { * * #### Usage * - * 1. Create a Firebase project and generate a service account key. Refer to [Firebase Admin SDK setup](https://firebase.google.com/docs/admin/setup). - * 2. Add the adapter to your Auth.js / NextAuth.js configuration. + * 1. Create a Firebase project and generate a service account key. + * 2. Add `GOOGLE_APPLICATION_CREDENTIALS` to your environment variables. + * 3. Add the adapter to your Auth.js / NextAuth.js configuration. * - * @example + * ##### References + * - [Firebase Admin SDK setup](https://firebase.google.com/docs/admin/setup) + * - [`GOOGLE_APPLICATION_CREDENTIALS`](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) + * + * ##### Example * * ```ts title="pages/api/auth/[...nextauth].ts" * import NextAuth from "next-auth" * import GoogleProvider from "next-auth/providers/google" * import { FirestoreAdapter } from "@next-auth/firebase-adapter" - * import admin from "firebase-admin" - * - * // Initialize the Firebase admin app. By default, the Firebase Admin SDK will - * // look for the GOOGLE_APPLICATION_CREDENTIALS environment variable and use - * // that to authenticate with the firebase project. See other authentication - * // methods here: https://firebase.google.com/docs/admin/setup - * const app = admin.initializeApp() - * const db = app.firestore() * * export default NextAuth({ - * adapter: FirestoreAdapter({ db }), + * adapter: FirestoreAdapter(), * providers: [ * GoogleProvider({ * clientId: process.env.GOOGLE_ID, @@ -213,8 +79,10 @@ export interface FirestoreAdapterConfig { * }) * ``` */ -export function FirestoreAdapter(config: FirestoreAdapterConfig): Adapter { - const { db, namingStrategy } = config +export function FirestoreAdapter(config?: FirebaseAdapterConfig): Adapter { + const { namingStrategy = "default", ...appOptions } = config ?? {} + const db = firestore(appOptions) + const preferSnakeCase = namingStrategy === "snake_case" const C = collestionsFactory(db, preferSnakeCase) const mapper = mapFieldsFactory(preferSnakeCase) diff --git a/packages/adapter-firebase/src/utils.ts b/packages/adapter-firebase/src/utils.ts new file mode 100644 index 00000000..bc044ed8 --- /dev/null +++ b/packages/adapter-firebase/src/utils.ts @@ -0,0 +1,149 @@ +import { type 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" + +// for consistency, store all fields as snake_case in the database +const MAP_TO_FIRESTORE: Record = { + userId: "user_id", + sessionToken: "session_token", + providerAccountId: "provider_account_id", + emailVerified: "email_verified", +} +const MAP_FROM_FIRESTORE: Record = {} + +for (const key in MAP_TO_FIRESTORE) { + MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]!] = key +} + +const identity = (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>(options: { + excludeId?: boolean + preferSnakeCase?: boolean +}): FirebaseFirestore.FirestoreDataConverter { + const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false) + + return { + toFirestore(object) { + const document: Record = {} + + 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 { + const document = snapshot.data()! // we can guarentee it exists + + const object: Record = {} + + 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( + querySnapshot: FirebaseFirestore.Query +): Promise { + const querySnap = await querySnapshot.limit(1).get() + return querySnap.docs[0]?.data() ?? null +} + +/** @internal */ +export async function deleteDocs( + querySnapshot: FirebaseFirestore.Query +): Promise { + const querySnap = await querySnapshot.get() + for (const doc of querySnap.docs) { + await doc.ref.delete() + } +} + +/** @internal */ +export async function getDoc( + docRef: FirebaseFirestore.DocumentReference +): Promise { + 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({ preferSnakeCase })), + sessions: db + .collection("sessions") + .withConverter(getConverter({ preferSnakeCase })), + accounts: db + .collection("accounts") + .withConverter(getConverter({ preferSnakeCase })), + verification_tokens: db + .collection( + preferSnakeCase ? "verification_tokens" : "verificationTokens" + ) + .withConverter( + getConverter({ preferSnakeCase, excludeId: true }) + ), + } +} + +export function firestore(appOptions: AppOptions) { + appOptions.projectId ??= "authjs-firebase-adapter" + const apps = getApps() + const app = apps.find((app) => app.name === appOptions.projectId) ?? apps[0] + if (app) { + return getFirestore(app) + } else { + const app = initializeApp(appOptions) + return initializeFirestore(app) + } +} diff --git a/packages/adapter-firebase/tsconfig.json b/packages/adapter-firebase/tsconfig.json index aecae70f..f7e99bd0 100644 --- a/packages/adapter-firebase/tsconfig.json +++ b/packages/adapter-firebase/tsconfig.json @@ -1,11 +1,24 @@ { "extends": "@next-auth/tsconfig/tsconfig.adapters.json", "compilerOptions": { - "rootDir": "src", - "outDir": "dist", "strict": true, "noUncheckedIndexedAccess": true, - "moduleResolution": "node" + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "outDir": ".", + "rootDir": "src", + "skipDefaultLibCheck": true, + "strictNullChecks": true, + "stripInternal": true, + "declarationMap": true, + "declaration": true }, - "exclude": ["tests", "dist", "jest.config.js"] -} + "include": [ + "src/**/*" + ], + "exclude": [ + "tests", + "jest.config.js" + ] +} \ No newline at end of file