diff --git a/src/emulator/constants.ts b/src/emulator/constants.ts index 459e5712469..78ec9ec85ac 100644 --- a/src/emulator/constants.ts +++ b/src/emulator/constants.ts @@ -13,6 +13,7 @@ const DEFAULT_HOST = "localhost"; export class Constants { static SERVICE_FIRESTORE = "firestore.googleapis.com"; + static SERVICE_REALTIME_DATABASE = "firebaseio.com"; static getDefaultHost(emulator: Emulators): string { return DEFAULT_HOST; diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index e1bb2500f9c..cec97b9ccf4 100644 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -154,17 +154,22 @@ export async function startAll(options: any): Promise { if (targets.indexOf(Emulators.DATABASE) > -1) { const databaseAddr = Constants.getAddress(Emulators.DATABASE, options); - const databaseEmulator = new DatabaseEmulator({ - host: databaseAddr.host, - port: databaseAddr.port, - }); + let databaseEmulator; + if (targets.indexOf(Emulators.FUNCTIONS) > -1) { + const functionsAddr = Constants.getAddress(Emulators.FUNCTIONS, options); + databaseEmulator = new DatabaseEmulator({ + host: databaseAddr.host, + port: databaseAddr.port, + functions_emulator_host: functionsAddr.host, + functions_emulator_port: functionsAddr.port, + }); + } else { + databaseEmulator = new DatabaseEmulator({ + host: databaseAddr.host, + port: databaseAddr.port, + }); + } await startEmulator(databaseEmulator); - - // TODO: When the database emulator is integrated with the Functions - // emulator, we will need to pass the port in and remove this warning - utils.logWarning( - `Note: the database emulator is not currently integrated with the functions emulator.` - ); } if (targets.indexOf(Emulators.HOSTING) > -1) { diff --git a/src/emulator/databaseEmulator.ts b/src/emulator/databaseEmulator.ts index b9b9b8b4f43..1449191e938 100644 --- a/src/emulator/databaseEmulator.ts +++ b/src/emulator/databaseEmulator.ts @@ -5,6 +5,8 @@ import { Constants } from "./constants"; interface DatabaseEmulatorArgs { port?: number; host?: string; + functions_emulator_port?: number; + functions_emulator_host?: string; } export class DatabaseEmulator implements EmulatorInstance { diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 42c6f9296ab..3de608b1072 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -32,6 +32,14 @@ import { EmulatorLogger, Verbosity } from "./emulatorLogger"; const EVENT_INVOKE = "functions:invoke"; +/* + * The Realtime Database emulator expects the `path` field in its trigger + * definition to be relative to the database root. This regex is used to extract + * that path from the `resource` member in the trigger definition used by the + * functions emulator. + */ +const DATABASE_PATH_PATTERN = new RegExp("^projects/[^/]+/instances/[^/]+/refs(/.*)$"); + export interface FunctionsEmulatorArgs { port?: number; host?: string; @@ -490,6 +498,9 @@ You can probably fix this by running "npm install ${ case Constants.SERVICE_FIRESTORE: await this.addFirestoreTrigger(this.projectId, definition); break; + case Constants.SERVICE_REALTIME_DATABASE: + await this.addRealtimeDatabaseTrigger(this.projectId, definition); + break; default: EmulatorLogger.log("DEBUG", `Unsupported trigger: ${JSON.stringify(definition)}`); EmulatorLogger.log( @@ -514,6 +525,74 @@ You can probably fix this by running "npm install ${ return loadTriggers(); } + addRealtimeDatabaseTrigger( + projectId: string, + definition: EmulatedTriggerDefinition + ): Promise { + const databasePort = EmulatorRegistry.getPort(Emulators.DATABASE); + if (!databasePort) { + EmulatorLogger.log( + "INFO", + `Ignoring trigger "${ + definition.name + }" because the Realtime Database emulator is not running.` + ); + return Promise.resolve(); + } + if (definition.eventTrigger === undefined) { + EmulatorLogger.log( + "WARN", + `Event trigger "${definition.name}" has undefined "eventTrigger" member` + ); + return Promise.reject(); + } + + const result: string[] | null = DATABASE_PATH_PATTERN.exec(definition.eventTrigger.resource); + if (result === null || result.length !== 2) { + EmulatorLogger.log( + "WARN", + `Event trigger "${definition.name}" has malformed "resource" member. ` + + `${definition.eventTrigger.resource}` + ); + return Promise.reject(); + } + + const bundle = JSON.stringify([ + { + name: `projects/${projectId}/locations/_/functions/${definition.name}`, + path: result[1], // path stored in the first capture group + event: definition.eventTrigger.eventType, + topic: `projects/${projectId}/topics/${definition.name}`, + }, + ]); + + EmulatorLogger.logLabeled( + "BULLET", + "functions", + `Setting up Realtime Database trigger "${definition.name}"` + ); + logger.debug(`addDatabaseTrigger`, JSON.stringify(bundle)); + return new Promise((resolve, reject) => { + request.put( + `http://localhost:${databasePort}/.settings/functionTriggers.json`, + { + auth: { + bearer: "owner", + }, + body: bundle, + }, + (err, res, body) => { + if (err) { + EmulatorLogger.log("WARN", "Error adding trigger: " + err); + reject(); + return; + } + resolve(); + } + ); + }); + } + addFirestoreTrigger(projectId: string, definition: EmulatedTriggerDefinition): Promise { const firestorePort = EmulatorRegistry.getPort(Emulators.FIRESTORE); if (!firestorePort) {