在后端 Javascript 中处理多个 Firebase 项目

Handle multiple Firebase Projects in the BackEnd Javascript

提问人:Joao Victor 提问时间:10/20/2023 最后编辑:Joao Victor 更新时间:10/20/2023 访问量:32

问:

我在许多项目中都有很多应用程序,我想获取有关它们的信息以便以后更新它们,所以我决定使用 NodeJS 创建一个 API,并且我正在配置我的端点可以从不同项目中检索信息的方式,例如计算特定项目中的应用程序数量,我知道我需要每个项目的服务帐户密钥,并且我已经下载了它们,所以我的核心逻辑是像这样的端点

const { initProject } = require('../config/firebaseConfig')
async function getAllApps(req, res) {
  const projectId = req.params.projectId;
  const project = await initProject(projectId);

  try {
    androidApps = await project.projectManagement().listAndroidApps();
    iosApps = await project.projectManagement().listIosApps();

    const allApps = [...androidApps, ...iosApps];

    const appList = allApps.map((app) => ({
      appId: app.appId
    }));

    res.status(200).json(appList);

  } catch (error) {
    console.error('Error retrieving apps:', error);
    res.status(500).json({ error: 'Failed to retrieve apps for the project' });
  }
}

initProject 是

const admin = require('firebase-admin');

// Initialize a project cache
const projectCache = new Map();

//initialize the project based on it's id name
async function initProject(projectId) {
    // Check if the project has already been initialized
    if (projectCache.has(projectId)) {
        return projectCache.get(projectId);
    }

    try {
        // Construct the path to the service account key file using the provided path or a default value
        if (process.env.FIREBASE_ACCOUNT_SERVICE_PATH !== null) {
            var serviceAccountPath = `${process.env.FIREBASE_ACCOUNT_SERVICE_PATH}\\${projectId}.json`;
        } else {
            var serviceAccountPath = `C:\\path\\to\\cred\\${projectId}.json`;
        }

        // Initialize the Firebase Admin SDK
        const serviceAccount = require(serviceAccountPath);

        admin.initializeApp({
            credential: admin.credential.cert(serviceAccount),
            databaseURL: `https://${projectId}.firebaseio.com`,
        });

        // Get the initialized Firebase project
        const firebaseProject = admin;

        // Cache the initialized project
        projectCache.set(projectId, firebaseProject);

        return firebaseProject;

    } catch (error) {

        console.error('Error initializing Firebase project: ', error);
        throw new Error('Failed to initialize Firebase project');
    }
}

module.exports = {
    initProject
}

路线是这样的

router.get('/:projectId/apps', projectController.getAllApps);

所以我也启动了 express,并且对第一个端点的调用是这样的,所以它工作得很好,我的第一次调用成功了,我可以收到我需要的东西,但是如果我尝试再次调用这个端点,但使用另一个项目,它将失败,因为我已经在没有指定应用程序的情况下初始化了 firebase-admin, 但我的需求正是如此,例如,计算每个项目的应用程序数量,所以我需要初始化它,我试图更改它以在我像这样退出 firebaseConfig 后立即杀死需求http://localhost:99/projects/project-name/appshttp://localhost:99/projects/project-name-2/apps

// Initialize a project cache
const projectCache = new Map();

// Function factory to create a new Firebase App instance
function createFirebaseApp(projectId) {
    return () => {
        if (projectCache.has(projectId)) {
            const cachedProject = projectCache.get(projectId);
            return cachedProject;
        }

        try {
            // Construct the path to the service account key file using the provided path or a default value
            if (process.env.FIREBASE_ACCOUNT_SERVICE_PATH !== null) {
                var serviceAccountPath = `${process.env.FIREBASE_ACCOUNT_SERVICE_PATH}\\${projectId}.json`;
            } else {
                var serviceAccountPath = `C:\\EscolarManager\\Dados\\AppsCred\\${projectId}.json`;
            }

            // Initialize a new Firebase Admin SDK for each project
            const admin = require('firebase-admin');
            const serviceAccount = require(serviceAccountPath);

            const firebaseProject = admin.initializeApp({
                credential: admin.credential.cert(serviceAccount),
                databaseURL: `https://${projectId}.firebaseio.com`,
            });

            // Cache the initialized project
            projectCache.set(projectId, firebaseProject);

            return firebaseProject;
        } catch (error) {
            console.error('Error initializing Firebase project: ', error);
            throw new Error('Failed to initialize Firebase project');
        }
    }
}

module.exports = {
    createFirebaseApp
}

我也尝试过制作一个工厂功能

const admin = require('firebase-admin');

// Initialize a project cache
const projectCache = new Map();

// Function factory to create a new Firebase App instance
function createFirebaseApp(projectId) {
    return () => {
        if (projectCache.has(projectId)) {
            const cachedProject = projectCache.get(projectId);
            return cachedProject;
        }

        try {
            // Construct the path to the service account key file using the provided path or a default value
            if (process.env.FIREBASE_ACCOUNT_SERVICE_PATH !== null) {
                var serviceAccountPath = `${process.env.FIREBASE_ACCOUNT_SERVICE_PATH}\\${projectId}.json`;
            } else {
                var serviceAccountPath = `C:\\EscolarManager\\Dados\\AppsCred\\${projectId}.json`;
            }

            // Initialize the Firebase Admin SDK without specifying an app name
            const serviceAccount = require(serviceAccountPath);

            const firebaseProject = admin.initializeApp({
                credential: admin.credential.cert(serviceAccount),
                databaseURL: `https://${projectId}.firebaseio.com`,
            });

            // Cache the initialized project
            projectCache.set(projectId, firebaseProject);

            return firebaseProject;
        } catch (error) {
            console.error('Error initializing Firebase project: ', error);
            throw new Error('Failed to initialize Firebase project');
        }
    }
}

// Example usage of the factory to create new Firebase App instances
const projectFactory = createFirebaseApp('project1');

const app1 = projectFactory();
const app2 = projectFactory();

console.log(app1 === app2); // This should print false, indicating they are different instances

但它们都不起作用,结果相同。我正在缓存项目,不需要初始化同一个项目两次,但调用不同的项目是我的问题


“samthecodingman”提供的解决方案优雅地解决了这个问题。现在,我将分享完整的解决方案,并进行细微调整,这涉及导入服务。我想向他表达我的感激之情;多亏了他的帮助,我现在对这个图书馆有了更清晰的了解。它的功能类似于一组函数,允许我简单地调用函数并利用其类的内容,例如 ProjectManagement 类。

这是控制器

// Should return the list of apps id for this project
async function getAllApps(req, res) {
  const projectId = req.params.projectId;

  const project = getFirebaseAdminForProject(projectId);
  const projectManagement = getProjectManagement(project);

  try {
    androidApps = await projectManagement.listAndroidApps();
    iosApps = await projectManagement.listIosApps();

    const allApps = [...androidApps, ...iosApps];

    const appList = allApps.map((app) => ({
      appId: app.appId
    }));

    res.status(200).json(appList);

  } catch (error) {
    console.error('Error retrieving apps:', error);
    res.status(500).json({ error: 'Failed to retrieve apps for the project' });
  }
}

所以他提供的功能

const { initializeApp, getApps, cert } = require('firebase-admin/app');

function getFirebaseAdminForProject(projectId) {
    if (!projectId)
        throw new Error('Project ID is required!');

    let projectApp = getApps().find(app => app.name === projectId);

    if (projectApp)
        return projectApp;

    // if here, project is not yet initialized
    let serviceAccountPath = process.env.FIREBASE_ACCOUNT_SERVICE_PATH != null
        ? `${process.env.FIREBASE_ACCOUNT_SERVICE_PATH}\\${projectId}.json`
        : `C:\\path\\to\\${projectId}.json`;

    // Initialize the Firebase Admin SDK
    const serviceAccount = require(serviceAccountPath);

    return initializeApp({
        credential: cert(serviceAccount),
        databaseURL: `https://${projectId}.firebaseio.com`
    }, projectId); // <-- using projectId as instance name
}

module.exports = {
    getFirebaseAdminForProject
};

我想我不再需要将项目保存在地图变量中,它是在 firebase-admin 内部完成的(如果我错了,您可以在这里发表评论并解释到底在做什么)。

正如你所看到的,他提供的解决方案附带了一个非常重要的信息:firebase-admin 库中的“应用程序”是关于管理应用程序的,它实际上是项目,所以理解这一点非常重要。 谢谢Samthecodingman!

使用 '!=' '!==' 的修复也很重要。我感激不尽!

JavaScript 节点.js firebase express firebase-admin

评论


答:

1赞 samthecodingman 10/20/2023 #1

根据您的代码,您可能不知道 Firebase SDK 能够初始化多个应用实例。当您在未提供实例名称的情况下进行调用时,将初始化默认实例。在上面的代码中,您多次调用,然后尝试将初始化项目存储到字典中。调用同一个实例会引发异常,并且只是 SDK 的命名空间,它不代表您刚刚初始化的应用程序(返回的值是,正如您在第二次尝试中意识到的那样)。initializeAppadmin.initializeApp(config)admininitializeAppadmininitializeApp

注意:只需一个 Admin SDK 实例即可与该项目上的所有资源进行通信,为同一项目初始化多个实例是多余的。这里的“应用”不是在谈论 iOS/Android/Web 应用程序,而是一个初始化的管理 SDK 实例。

考虑到这一点,您可以将代码更新为:

// ./firebase.js
const { initializeApp, getApps } = require('firebase-admin/app');

function getFirebaseAdminForProject(projectId) {
  if (!projectId)
    throw new Error('Project ID is required!');

  let projectApp = getApps().find(app => app.name === projectId);

  if (projectApp)
    return projectApp;

  // if here, project is not yet initialized
  let serviceAccountPath = process.env.FIREBASE_ACCOUNT_SERVICE_PATH != null // <- note != instead of !==
    ? `${process.env.FIREBASE_ACCOUNT_SERVICE_PATH}\\${projectId}.json`
    : `C:\\path\\to\\${projectId}.json`;

  return initializeApp({
    credential: admin.credential.cert(serviceAccount),
    databaseURL: `https://${projectId}.firebaseio.com`
  }, projectId); // <-- using projectId as instance name
}

exports.getFirebaseAdminForProject = getFirebaseAdminForProject;
exports.project1App = getFirebaseAdminForProject('project-1');
exports.project2App = getFirebaseAdminForProject('project-2');

要获取给定应用的 Firestore,您可以使用:

const { getApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');

const db = getFirestore(getApp('project-1'));

const { project1App } = require('./firebase');
const { getFirestore } = require('firebase-admin/firestore');

const db = getFirestore(project1App);

请考虑将 require/exports 换成现代导入/导出等效项。

评论

0赞 Joao Victor 10/20/2023
1 - 在没有 admin 命名空间的情况下使用 require('firebase-admin/app') 中的 getApp 是否调用默认的全局项目初始化权限?我想是的,因为这是你提到的 projectApp(App 类),对吧?2 - getFirestore 函数是否可以检索同一 projectApp 中多个 mobileApp 之间共享的 Firestore 实例?还是仅适用于所选 projectApp 中的特定应用程序(iOS、Android、web、Flutter)?
0赞 samthecodingman 10/22/2023
@JoaoVictor 每当管理 SDK 谈论“应用程序”时,请考虑“配置的服务器配置文件”。它与客户端应用程序无关,仅用于使客户端代码和管理代码彼此相似以节省时间。RE 1) 使用是现代的等价物。当调用时,两者都将返回类的默认实例,而不带参数。要获取特定实例,您需要传递该实例的名称(即 或 )。getApp()admin.app()AppgetApp('project1')admin.app('project1')
1赞 samthecodingman 10/22/2023
@JoaoVictor RE 2) 类似地,是现代的等价物,并接受 的实例作为第一个参数。默认情况下,您的所有客户端应用之间共享一个 Cloud Firestore 数据库。如果在极不可能的情况下,您设置了多个 Cloud Firestore 数据库,则需要将要连接到的数据库的数据库 ID 作为第二个参数(即 或 )。getFirestore()admin.firebase()AppgetFirestore(someApp, 'db-ios')admin.firestore(someApp, 'db-ios')