Problem:
I have many apps in many projects and I want to get information about them to update them later, so I decide to create an API with NodeJS and I’m configuring the way my endpoint can retrieve information from different projects such as count the number of apps in the specific project, I know that I need the Service Account key for each project and I already download them so my core logic is an endpoint like this
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' });
}
}
and the initProject is
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
}
the route is like this
router.get('/:projectId/apps', projectController.getAllApps);
so I start the express as well and the call for the first endpoint is this http://localhost:99/projects/project-name/apps
so it works just fine my first call result in a success and I can receive what I need but if I try to call this endpoint again but with another project http://localhost:99/projects/project-name-2/apps
it will fail because I already have initialize the firebase-admin without specify an app, but my need is exactly this, count the number of apps for each project for example, so I need to initialize it, I’ve tried to change this to kill the require as soon I get out of the firebaseConfig like this
// 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
}
and I’ve tried to make a factory function too
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
but none of them works, same result. I’m caching the the project to not need to initialize the same project twice but call different projects is my issue here
The solution offered by ‘samthecodingman’ elegantly resolved the issue. I will now share the complete solution with a minor adjustment, which involves importing the service. I want to express my gratitude to him; thanks to his help, I now have a clearer understanding of this library. It functions in a manner similar to a set of functions, allowing me to simply call the functions and utilize the contents of its classes, such as the ProjectManagement class.
here is the controller
// 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' });
}
}
so the function he provided
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
};
I guess I don’t need to save the project in a map variable anymore, it’s done internally in the firebase-admin (if I’m wrong you can comment here and explain what is being done exactly).
As you can see the solution he provide came with a very important information the “App” in the firebase-admin lib is about the admin app that is actually the project so it’s very important to understand this.
thank you samthecodingman!
the fix with the ‘!=’ ‘!==’ is also important. I can’t thank you enough!
Solution:
Based on your code, you may not be aware that the Firebase SDKs have the ability to initialize multiple app instances. When you call initializeApp
without providing a name for the instance, you initialize the default instance. In your code above, you call admin.initializeApp(config)
multiple times, and then try to store admin
into your dictionary as the initialized project. Calling initializeApp
for the same instance will throw an exception, and admin
is just the SDK’s namespace, it does not represent the application you just initialized (the value returned from initializeApp
is, as you realized in your second attempt).
Note: Only one instance of the Admin SDK is needed to communicate with all resources on that project, initializing multiple instances for the same project is redundant. An “app” here is not talking about iOS/Android/Web applications, but an initialized admin SDK instance.
Taking this into consideration, you can update your code to:
// ./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');
To get Firestore for a given app, you can use:
const { getApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
const db = getFirestore(getApp('project-1'));
or
const { project1App } = require('./firebase');
const { getFirestore } = require('firebase-admin/firestore');
const db = getFirestore(project1App);
Consider switching out require/exports for the modern import/export equivalents.