UNPKG

firebase-tools

Version:
195 lines (194 loc) 8.87 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EventarcEmulator = void 0; const express = require("express"); const constants_1 = require("./constants"); const types_1 = require("./types"); const utils_1 = require("../utils"); const emulatorLogger_1 = require("./emulatorLogger"); const registry_1 = require("./registry"); const error_1 = require("../error"); const eventarcEmulatorUtils_1 = require("./eventarcEmulatorUtils"); const cors = require("cors"); const GOOGLE_CHANNEL = "google"; class EventarcEmulator { constructor(args) { this.args = args; this.logger = emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.EVENTARC); this.events = {}; } createHubServer() { const registerTriggerRoute = `/emulator/v1/projects/:project_id/triggers/:trigger_name(*)`; const registerTriggerHandler = (req, res) => { try { const { projectId, triggerName, eventTrigger, key } = getTriggerIdentifiers(req); this.logger.logLabeled("BULLET", "eventarc", `Registering Eventarc event trigger for ${key} with trigger name ${triggerName}.`); const eventTriggers = this.events[key] || []; eventTriggers.push({ projectId, triggerName, eventTrigger }); this.events[key] = eventTriggers; res.status(200).send({ res: "OK" }); } catch (error) { res.status(400).send({ error }); } }; const getTriggerIdentifiers = (req) => { const projectId = req.params.project_id; const triggerName = req.params.trigger_name; if (!projectId || !triggerName) { const error = "Missing project ID or trigger name."; this.logger.log("ERROR", error); throw error; } const bodyString = req.rawBody.toString(); const substituted = bodyString.replaceAll("${PROJECT_ID}", projectId); const body = JSON.parse(substituted); const eventTrigger = body.eventTrigger; if (!eventTrigger) { const error = `Missing event trigger for ${triggerName}.`; this.logger.log("ERROR", error); throw error; } const channel = eventTrigger.channel || GOOGLE_CHANNEL; const key = `${eventTrigger.eventType}-${channel}`; return { projectId, triggerName, eventTrigger, key }; }; const removeTriggerRoute = `/emulator/v1/remove/projects/:project_id/triggers/:trigger_name(*)`; const removeTriggerHandler = (req, res) => { try { const { projectId, triggerName, eventTrigger, key } = getTriggerIdentifiers(req); this.logger.logLabeled("BULLET", "eventarc", `Removing Eventarc event trigger for ${key} with trigger name ${triggerName}.`); const eventTriggers = this.events[key] || []; const triggerIdentifier = { projectId, triggerName, eventTrigger }; const removeIdx = eventTriggers.findIndex((e) => JSON.stringify(triggerIdentifier) === JSON.stringify(e)); if (removeIdx === -1) { this.logger.logLabeled("ERROR", "eventarc", "Tried to remove nonexistent trigger"); throw new Error(`Unable to delete function trigger ${triggerName}`); } eventTriggers.splice(removeIdx, 1); if (eventTriggers.length === 0) { delete this.events[key]; } else { this.events[key] = eventTriggers; } res.status(200).send({ res: "OK" }); } catch (error) { res.status(400).send({ error }); } }; const getTriggersRoute = `/google/getTriggers`; const getTriggersHandler = (req, res) => { res.status(200).send(this.events); }; const publishEventsRoute = `/projects/:project_id/locations/:location/channels/:channel::publishEvents`; const publishNativeEventsRoute = `/google/publishEvents`; const publishEventsHandler = (req, res) => { const isCustom = req.params.project_id && req.params.channel; const channel = isCustom ? `projects/${req.params.project_id}/locations/${req.params.location}/channels/${req.params.channel}` : GOOGLE_CHANNEL; const body = JSON.parse(req.rawBody.toString()); for (const event of body.events) { if (!event.type) { res.sendStatus(400); return; } this.logger.log("INFO", `Received event at channel ${channel}: ${JSON.stringify(event, null, 2)}`); this.triggerEventFunction(channel, event); } res.sendStatus(200); }; const dataMiddleware = (req, _, next) => { const chunks = []; req.on("data", (chunk) => { chunks.push(chunk); }); req.on("end", () => { req.rawBody = Buffer.concat(chunks); next(); }); }; const hub = express(); hub.post([registerTriggerRoute], dataMiddleware, registerTriggerHandler); hub.post([publishEventsRoute], dataMiddleware, publishEventsHandler); hub.post([publishNativeEventsRoute], dataMiddleware, cors({ origin: true }), publishEventsHandler); hub.post([removeTriggerRoute], dataMiddleware, removeTriggerHandler); hub.get([getTriggersRoute], cors({ origin: true }), getTriggersHandler); hub.all("*", (req, res) => { this.logger.log("DEBUG", `Eventarc emulator received unknown request at path ${req.path}`); res.sendStatus(404); }); return hub; } async triggerEventFunction(channel, event) { if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.FUNCTIONS)) { this.logger.log("INFO", "Functions emulator not found. This should not happen."); return Promise.reject(); } const key = `${event.type}-${channel}`; const triggers = this.events[key] || []; const eventPayload = channel === GOOGLE_CHANNEL ? event : (0, eventarcEmulatorUtils_1.cloudEventFromProtoToJson)(event); return await Promise.all(triggers .filter((trigger) => !trigger.eventTrigger.eventFilters || this.matchesAll(event, trigger.eventTrigger.eventFilters)) .map((trigger) => this.callFunctionTrigger(trigger, eventPayload))); } callFunctionTrigger(trigger, event) { return registry_1.EmulatorRegistry.client(types_1.Emulators.FUNCTIONS) .request({ method: "POST", path: `/functions/projects/${trigger.projectId}/triggers/${trigger.triggerName}`, headers: { "Content-Type": "application/json" }, body: JSON.stringify(event), responseType: "stream", resolveOnHTTPError: true, }) .then((res) => { if (res.status >= 400) { throw new error_1.FirebaseError(`Received non-200 status code: ${res.status}`); } }) .catch((err) => { this.logger.log("ERROR", `Failed to trigger Functions emulator for ${trigger.triggerName}: ${err}`); }); } matchesAll(event, eventFilters) { return Object.entries(eventFilters).every(([key, value]) => { var _a, _b; let attr = (_a = event[key]) !== null && _a !== void 0 ? _a : event.attributes[key]; if (typeof attr === "object" && !Array.isArray(attr)) { attr = (_b = attr.ceTimestamp) !== null && _b !== void 0 ? _b : attr.ceString; } return attr === value; }); } async start() { const { host, port } = this.getInfo(); const server = this.createHubServer().listen(port, host); this.destroyServer = (0, utils_1.createDestroyer)(server); return Promise.resolve(); } async connect() { return Promise.resolve(); } async stop() { if (this.destroyServer) { await this.destroyServer(); } } getInfo() { const host = this.args.host || constants_1.Constants.getDefaultHost(); const port = this.args.port || constants_1.Constants.getDefaultPort(types_1.Emulators.EVENTARC); return { name: this.getName(), host, port, }; } getName() { return types_1.Emulators.EVENTARC; } } exports.EventarcEmulator = EventarcEmulator;