firebase-tools-extra
Version:
Extra functionality for firebase-tools with support for emulators and auth through service account.
292 lines (291 loc) • 13.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var admin = tslib_1.__importStar(require("firebase-admin"));
var fs_1 = require("fs");
var util_1 = require("util");
var logger = tslib_1.__importStar(require("./logger"));
exports.writeFilePromise = util_1.promisify(fs_1.writeFile);
/**
* Check whether a value is a string or not
* @param valToCheck - Value to check
* @returns Whether or not value is a string
*/
function isString(valToCheck) {
return typeof valToCheck === 'string' || valToCheck instanceof String;
}
/**
* Get settings from firebaserc file
* @param filePath - Path for file
* @returns Firebase settings object
*/
function readJsonFile(filePath) {
if (!fs_1.existsSync(filePath)) {
var errMsg = "File does not exist at path \"" + filePath + "\"";
/* eslint-disable no-console */
logger.error(errMsg);
/* eslint-enable no-console */
throw new Error(errMsg);
}
try {
var fileBuffer = fs_1.readFileSync(filePath, 'utf8');
return JSON.parse(fileBuffer.toString());
}
catch (err) {
logger.error("Unable to parse " + filePath.replace(process.cwd(), '') + " - JSON is most likely not valid");
return {};
}
}
exports.readJsonFile = readJsonFile;
/**
* Parse fixture path string into JSON with error handling
* @param valueToParse - valueToParse string to be parsed into JSON
* @returns Parsed fixture value or path
*/
function tryToJsonParse(valueToParse) {
if (isString(valueToParse)) {
try {
return JSON.parse(valueToParse);
}
catch (err) {
return valueToParse;
}
}
return valueToParse;
}
exports.tryToJsonParse = tryToJsonParse;
/**
* Get service account from either local file or environment variables
* @returns Service account object
*/
function getServiceAccount() {
var serviceAccountPath = process.cwd() + "/serviceAccount.json";
// Check for local service account file (Local dev)
if (fs_1.existsSync(serviceAccountPath)) {
return readJsonFile(serviceAccountPath); // eslint-disable-line global-require, import/no-dynamic-require
}
// Use environment variables (CI)
var SERVICE_ACCOUNT = process.env.SERVICE_ACCOUNT;
if (SERVICE_ACCOUNT) {
try {
return JSON.parse(SERVICE_ACCOUNT);
}
catch (err) {
logger.warn("Issue parsing \"SERVICE_ACCOUNT\" environment variable from string to object: ", err.message);
}
}
return null;
}
exports.getServiceAccount = getServiceAccount;
var fbInstance;
/**
* Load settings from firebase.json if it exists in
* cwd of command
* @returns firebase.json contents
*/
function loadFirebaseJsonSettings() {
var firebaseJsonPath = process.cwd() + "/firebase.json";
if (fs_1.existsSync(firebaseJsonPath)) {
return readJsonFile(firebaseJsonPath);
}
}
/**
* Initialize Firebase instance from service account (from either local
* serviceAccount.json or environment variables)
*
* @returns Initialized Firebase instance
* @param options - Options object
*/
function initializeFirebase(options) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
// Return existing firebase-admin app instance
if (fbInstance) {
return fbInstance;
}
// Return if init has already occurred in firebase-admin
if (admin.apps.length !== 0) {
fbInstance = admin.apps[0]; // eslint-disable-line prefer-destructuring
return fbInstance;
}
// Use emulator if settings exists in environment or if emulator option is true
var _k = process.env, FIRESTORE_EMULATOR_HOST = _k.FIRESTORE_EMULATOR_HOST, FIREBASE_DATABASE_EMULATOR_HOST = _k.FIREBASE_DATABASE_EMULATOR_HOST;
if (FIRESTORE_EMULATOR_HOST ||
FIREBASE_DATABASE_EMULATOR_HOST || (options === null || options === void 0 ? void 0 : options.emulator)) {
try {
// TODO: Look into using @firebase/testing in place of admin here to allow for
// usage of clearFirestoreData (see https://github.com/prescottprue/cypress-firebase/issues/73 for more info)
// Get settings for emulators and service account to add as credential if it exists
var _l = process.env, GCLOUD_PROJECT = _l.GCLOUD_PROJECT, FIREBASE_PROJECT = _l.FIREBASE_PROJECT;
var serviceAccount = getServiceAccount();
var firebaseJson = loadFirebaseJsonSettings();
var projectId = GCLOUD_PROJECT ||
FIREBASE_PROJECT || (serviceAccount === null || serviceAccount === void 0 ? void 0 : serviceAccount.project_id) || ((_a = firebaseJson === null || firebaseJson === void 0 ? void 0 : firebaseJson.projects) === null || _a === void 0 ? void 0 : _a.default) ||
'test';
var fbConfig = { projectId: projectId };
// Initialize RTDB with databaseURL from FIREBASE_DATABASE_EMULATOR_HOST to allow for RTDB actions
// within Emulator
if (FIREBASE_DATABASE_EMULATOR_HOST || (options === null || options === void 0 ? void 0 : options.emulator)) {
var databaseEmulatorHost = FIREBASE_DATABASE_EMULATOR_HOST ||
(((_c = (_b = firebaseJson === null || firebaseJson === void 0 ? void 0 : firebaseJson.emulators) === null || _b === void 0 ? void 0 : _b.database) === null || _c === void 0 ? void 0 : _c.port) ? "localhost:" + ((_e = (_d = firebaseJson === null || firebaseJson === void 0 ? void 0 : firebaseJson.emulators) === null || _d === void 0 ? void 0 : _d.database) === null || _e === void 0 ? void 0 : _e.port)
: 'localhost:9000');
// Set default database emulator host if none is set (so it is picked up by firebase-admin)
// TODO: attempt to load settings from firebase.json for port numbers
if (!FIREBASE_DATABASE_EMULATOR_HOST) {
process.env.FIREBASE_DATABASE_EMULATOR_HOST = databaseEmulatorHost;
}
// TODO: Check into if namespace is required or if it should be optional
// TODO: Support passing a database url
fbConfig.databaseURL = "http://" + databaseEmulatorHost + "?ns=" + projectId;
// Log setting if debug option enabled
if (options === null || options === void 0 ? void 0 : options.debug) {
logger.info("Using RTDB emulator with DB URL: " + fbConfig.databaseURL);
}
}
// Add service account credential if it exists so that custom auth tokens can be generated
if (serviceAccount) {
fbConfig.credential = admin.credential.cert(serviceAccount);
}
fbInstance = admin.initializeApp(fbConfig);
// Enable Firestore Emulator is env variable is set or emulator option is enabled
if (FIRESTORE_EMULATOR_HOST || (options === null || options === void 0 ? void 0 : options.emulator)) {
// Get host from env variable, falling back to firebase.json, then to localhost:8080
var firestoreEmulatorHost = FIRESTORE_EMULATOR_HOST ||
(((_g = (_f = firebaseJson === null || firebaseJson === void 0 ? void 0 : firebaseJson.emulators) === null || _f === void 0 ? void 0 : _f.firestore) === null || _g === void 0 ? void 0 : _g.port) ? "localhost:" + ((_j = (_h = firebaseJson === null || firebaseJson === void 0 ? void 0 : firebaseJson.emulators) === null || _h === void 0 ? void 0 : _h.firestore) === null || _j === void 0 ? void 0 : _j.port)
: 'localhost:8080');
var _m = tslib_1.__read(firestoreEmulatorHost.split(':'), 2), servicePath = _m[0], portStr = _m[1];
var firestoreSettings = {
servicePath: servicePath,
port: parseInt(portStr, 10),
};
// Set default Firestore emulator host if none is set (so it is picked up by firebase-admin)
if (!FIRESTORE_EMULATOR_HOST) {
process.env.FIRESTORE_EMULATOR_HOST = firestoreEmulatorHost;
}
// Log setting if debug option enabled
if (options === null || options === void 0 ? void 0 : options.debug) {
logger.info("Using Firestore emulator with host: " + firestoreEmulatorHost);
}
admin.firestore().settings(firestoreSettings);
}
}
catch (err) {
logger.error('Error initializing firebase-admin instance with emulator settings.', err.message);
throw err;
}
}
else {
try {
// Get service account from local file falling back to environment variables
var serviceAccount = getServiceAccount();
var projectId = serviceAccount === null || serviceAccount === void 0 ? void 0 : serviceAccount.project_id;
if (!projectId || !isString(projectId)) {
var missingProjectIdErr = 'Error project_id from service account to initialize Firebase.';
logger.error(missingProjectIdErr);
throw new Error(missingProjectIdErr);
}
var cleanProjectId = projectId.replace('firebase-top-agent-int', 'top-agent-int');
fbInstance = admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://" + cleanProjectId + ".firebaseio.com",
});
}
catch (err) {
logger.error('Error initializing firebase-admin instance from service account.', err.message);
throw err;
}
}
return fbInstance;
}
exports.initializeFirebase = initializeFirebase;
/**
* Check with or not a slash path is the path of a document
* @param slashPath - Path to check for whether or not it is a doc
* @returns Whether or not slash path is a document path
*/
function isDocPath(slashPath) {
return !(slashPath.replace(/^\/|\/$/g, '').split('/').length % 2);
}
/**
* Convert slash path to Firestore reference
* @param firestoreInstance - Instance on which to
* create ref
* @param slashPath - Path to convert into firestore reference
* @param options - Options object
* @returns Ref at slash path
*/
function slashPathToFirestoreRef(firestoreInstance, slashPath, options) {
if (!slashPath) {
throw new Error('Path is required to make Firestore Reference');
}
var ref = isDocPath(slashPath)
? firestoreInstance.doc(slashPath)
: firestoreInstance.collection(slashPath);
// Apply orderBy to query if it exists
if ((options === null || options === void 0 ? void 0 : options.orderBy) && typeof ref.orderBy === 'function') {
ref = ref.orderBy(options.orderBy);
}
// Apply where to query if it exists
if ((options === null || options === void 0 ? void 0 : options.where) && typeof ref.where === 'function') {
ref = ref.where.apply(ref, tslib_1.__spread(options.where));
}
// Apply limit to query if it exists
if ((options === null || options === void 0 ? void 0 : options.limit) && typeof ref.limit === 'function') {
ref = ref.limit(options.limit);
}
// Apply limitToLast to query if it exists
if ((options === null || options === void 0 ? void 0 : options.limitToLast) && typeof ref.limitToLast === 'function') {
ref = ref.limitToLast(options.limitToLast);
}
return ref;
}
exports.slashPathToFirestoreRef = slashPathToFirestoreRef;
/**
* @param firestoreInstance - Instance of firestore from which to delete collection
* @param query - Parent collection query
* @param resolve - Function to call to resolve
* @param reject - Function to call to reject
*/
function deleteQueryBatch(firestoreInstance, query, resolve, reject) {
query
.get()
.then(function (snapshot) {
// When there are no documents left, we are done
if (snapshot.size === 0) {
return 0;
}
// Delete documents in a batch
var batch = firestoreInstance.batch();
snapshot.docs.forEach(function (doc) {
batch.delete(doc.ref);
});
return batch.commit().then(function () {
return snapshot.size;
});
})
.then(function (numDeleted) {
if (numDeleted === 0) {
resolve();
return;
}
// Recurse on the next process tick, to avoid
// exploding the stack.
process.nextTick(function () {
deleteQueryBatch(firestoreInstance, query, resolve, reject);
});
})
.catch(reject);
}
/**
* @param firestoreInstance - Instance of firestore from which to delete collection
* @param collectionPath - Path of collection to delete
* @param batchSize - Size of batch
* @returns Promise which resolves when collection has been deleted
*/
function deleteFirestoreCollection(firestoreInstance, collectionPath, batchSize) {
var collectionRef = firestoreInstance.collection(collectionPath);
var query = collectionRef.orderBy('__name__').limit(batchSize || 200);
return new Promise(function (resolve, reject) {
deleteQueryBatch(firestoreInstance, query, resolve, reject);
});
}
exports.deleteFirestoreCollection = deleteFirestoreCollection;