UNPKG

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
"use strict"; 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;