Skip to content

Commit

Permalink
Adds emulator support for v2 rtdb triggers (#5045)
Browse files Browse the repository at this point in the history
* changing how we detect non implemented events and splitting up the trigger register by platform

* fixing pubsub string

* adding int tests

* formatter

* increasing timeout of after func

* fixing timeout

* add comments & pr updates

* add changelog entry
  • Loading branch information
colerogers committed Oct 4, 2022
1 parent 443d13b commit cd737c9
Show file tree
Hide file tree
Showing 14 changed files with 134 additions and 58 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
@@ -0,0 +1 @@
- Add functions emulator support for RTDB v2 triggers (#5045).
6 changes: 6 additions & 0 deletions scripts/integration-helpers/framework.ts
Expand Up @@ -23,6 +23,7 @@ const STORAGE_BUCKET_FUNCTION_V2_FINALIZED_LOG =
"========== STORAGE BUCKET V2 FUNCTION FINALIZED ==========";
const STORAGE_BUCKET_FUNCTION_V2_METADATA_LOG =
"========== STORAGE BUCKET V2 FUNCTION METADATA ==========";
const RTDB_V2_FUNCTION_LOG = "========== RTDB V2 FUNCTION ==========";
/* Functions V1 */
const RTDB_FUNCTION_LOG = "========== RTDB FUNCTION ==========";
const FIRESTORE_FUNCTION_LOG = "========== FIRESTORE FUNCTION ==========";
Expand Down Expand Up @@ -141,6 +142,7 @@ export class TriggerEndToEndTest extends EmulatorEndToEndTest {
storageBucketV2MetadataTriggerCount = 0;
authBlockingCreateV2TriggerCount = 0;
authBlockingSignInV2TriggerCount = 0;
rtdbV2TriggerCount = 0;

rtdbFromFirestore = false;
firestoreFromRtdb = false;
Expand Down Expand Up @@ -176,6 +178,7 @@ export class TriggerEndToEndTest extends EmulatorEndToEndTest {
this.storageBucketV2MetadataTriggerCount = 0;
this.authBlockingCreateV2TriggerCount = 0;
this.authBlockingSignInV2TriggerCount = 0;
this.rtdbV2TriggerCount = 0;
}

/*
Expand Down Expand Up @@ -268,6 +271,9 @@ export class TriggerEndToEndTest extends EmulatorEndToEndTest {
if (data.includes(AUTH_BLOCKING_SIGN_IN_V2_LOG)) {
this.authBlockingSignInV2TriggerCount++;
}
if (data.includes(RTDB_V2_FUNCTION_LOG)) {
this.rtdbV2TriggerCount++;
}
});

return startEmulators;
Expand Down
1 change: 1 addition & 0 deletions scripts/triggers-end-to-end-tests/tests.ts
Expand Up @@ -148,6 +148,7 @@ describe("function triggers", () => {

it("should have have triggered cloud functions", () => {
expect(test.rtdbTriggerCount).to.equal(1);
expect(test.rtdbV2TriggerCount).to.eq(1);
expect(test.firestoreTriggerCount).to.equal(1);
/*
* Check for the presence of all expected documents in the firestore
Expand Down
14 changes: 7 additions & 7 deletions scripts/triggers-end-to-end-tests/triggers/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion scripts/triggers-end-to-end-tests/triggers/package.json
Expand Up @@ -10,7 +10,7 @@
"@google-cloud/pubsub": "^3.0.1",
"firebase": "^9.9.0",
"firebase-admin": "^11.0.0",
"firebase-functions": "^3.22.0"
"firebase-functions": "^3.24.1"
},
"devDependencies": {
"firebase-functions-test": "^0.2.0"
Expand Down
14 changes: 7 additions & 7 deletions scripts/triggers-end-to-end-tests/v1/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion scripts/triggers-end-to-end-tests/v1/package.json
Expand Up @@ -8,7 +8,7 @@
"dependencies": {
"@firebase/database-compat": "0.1.2",
"firebase-admin": "^11.0.0",
"firebase-functions": "^3.22.0"
"firebase-functions": "^3.24.1"
},
"devDependencies": {
"firebase-functions-test": "^0.2.0"
Expand Down
7 changes: 7 additions & 0 deletions scripts/triggers-end-to-end-tests/v2/index.js
Expand Up @@ -22,8 +22,10 @@ const AUTH_BLOCKING_CREATE_V2_LOG =
"========== AUTH BLOCKING CREATE V2 FUNCTION METADATA ==========";
const AUTH_BLOCKING_SIGN_IN_V2_LOG =
"========== AUTH BLOCKING SIGN IN V2 FUNCTION METADATA ==========";
const RTDB_LOG = "========== RTDB V2 FUNCTION ==========";

const PUBSUB_TOPIC = "test-topic";
const START_DOCUMENT_NAME = "test/start";

admin.initializeApp();

Expand Down Expand Up @@ -125,3 +127,8 @@ exports.onreqv2timeout = functionsV2.https.onRequest({ timeoutSeconds: 1 }, asyn
}, 3_000);
});
});

exports.rtdbv2reaction = functionsV2.database.onValueWritten(START_DOCUMENT_NAME, (event) => {
console.log(RTDB_LOG);
return;
});
14 changes: 7 additions & 7 deletions scripts/triggers-end-to-end-tests/v2/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion scripts/triggers-end-to-end-tests/v2/package.json
Expand Up @@ -7,7 +7,7 @@
},
"dependencies": {
"firebase-admin": "^11.0.0",
"firebase-functions": "^3.22.0"
"firebase-functions": "^3.24.1"
},
"devDependencies": {
"firebase-functions-test": "^0.2.0"
Expand Down
87 changes: 66 additions & 21 deletions src/emulator/functionsEmulator.ts
Expand Up @@ -589,7 +589,9 @@ export class FunctionsEmulator implements EmulatorInstance {
added = await this.addRealtimeDatabaseTrigger(
this.args.projectId,
key,
definition.eventTrigger
definition.eventTrigger,
signature,
definition.region
);
break;
case Constants.SERVICE_PUBSUB:
Expand Down Expand Up @@ -721,21 +723,12 @@ export class FunctionsEmulator implements EmulatorInstance {
}
}

async addRealtimeDatabaseTrigger(
projectId: string,
key: string,
eventTrigger: EventTrigger
): Promise<boolean> {
const databaseEmu = EmulatorRegistry.get(Emulators.DATABASE);
if (!databaseEmu) {
return false;
}

const result: string[] | null = DATABASE_PATH_PATTERN.exec(eventTrigger.resource);
private getV1DatabaseApiAttributes(projectId: string, key: string, eventTrigger: EventTrigger) {
const result: string[] | null = DATABASE_PATH_PATTERN.exec(eventTrigger.resource!);
if (result === null || result.length !== 3) {
this.logger.log(
"WARN",
`Event function "${key}" has malformed "resource" member. ` + `${eventTrigger.resource}`
`Event function "${key}" has malformed "resource" member. ` + `${eventTrigger.resource!}`
);
throw new FirebaseError(`Event function ${key} has malformed resource member`);
}
Expand All @@ -748,24 +741,76 @@ export class FunctionsEmulator implements EmulatorInstance {
topic: `projects/${projectId}/topics/${key}`,
});

logger.debug(`addRealtimeDatabaseTrigger[${instance}]`, JSON.stringify(bundle));

let setTriggersPath = "/.settings/functionTriggers.json";
let apiPath = "/.settings/functionTriggers.json";
if (instance !== "") {
setTriggersPath += `?ns=${instance}`;
apiPath += `?ns=${instance}`;
} else {
this.logger.log(
"WARN",
`No project in use. Registering function for sentinel namespace '${Constants.DEFAULT_DATABASE_EMULATOR_NAMESPACE}'`
);
}

return { bundle, apiPath, instance };
}

private getV2DatabaseApiAttributes(
projectId: string,
key: string,
eventTrigger: EventTrigger,
region: string
) {
const instance =
eventTrigger.eventFilters?.instance || eventTrigger.eventFilterPathPatterns?.instance;
if (!instance) {
throw new FirebaseError("A database instance must be supplied.");
}

const ref = eventTrigger.eventFilterPathPatterns?.ref;
if (!ref) {
throw new FirebaseError("A database reference must be supplied.");
}

// The 'namespacePattern' determines that we are using the v2 interface
const bundle = JSON.stringify({
name: `projects/${projectId}/locations/${region}/triggers/${key}`,
path: ref,
event: eventTrigger.eventType,
topic: `projects/${projectId}/topics/${key}`,
namespacePattern: instance,
});

// The query parameter '?ns=${instance}' is ignored in v2
const apiPath = "/.settings/functionTriggers.json";

return { bundle, apiPath, instance };
}

async addRealtimeDatabaseTrigger(
projectId: string,
key: string,
eventTrigger: EventTrigger,
signature: SignatureType,
region: string
): Promise<boolean> {
const databaseEmu = EmulatorRegistry.get(Emulators.DATABASE);
if (!databaseEmu) {
return false;
}

const { bundle, apiPath, instance } =
signature === "cloudevent"
? this.getV2DatabaseApiAttributes(projectId, key, eventTrigger, region)
: this.getV1DatabaseApiAttributes(projectId, key, eventTrigger);

logger.debug(`addRealtimeDatabaseTrigger[${instance}]`, JSON.stringify(bundle));

const client = new Client({
urlPrefix: `http://${EmulatorRegistry.getInfoHostString(databaseEmu.getInfo())}`,
auth: false,
});
try {
await client.post(setTriggersPath, bundle, { headers: { Authorization: "Bearer owner" } });
await client.post(apiPath, bundle, { headers: { Authorization: "Bearer owner" } });
} catch (err: any) {
this.logger.log("WARN", "Error adding Realtime Database function: " + err);
throw err;
Expand Down Expand Up @@ -819,7 +864,7 @@ export class FunctionsEmulator implements EmulatorInstance {
logger.debug(`addPubsubTrigger`, JSON.stringify({ eventTrigger }));

// "resource":\"projects/{PROJECT_ID}/topics/{TOPIC_ID}";
const resource = eventTrigger.resource;
const resource = eventTrigger.resource!;
let topic;
if (schedule) {
// In production this topic looks like
Expand Down Expand Up @@ -852,8 +897,8 @@ export class FunctionsEmulator implements EmulatorInstance {
addStorageTrigger(projectId: string, key: string, eventTrigger: EventTrigger): boolean {
logger.debug(`addStorageTrigger`, JSON.stringify({ eventTrigger }));

const bucket = eventTrigger.resource.startsWith("projects/_/buckets/")
? eventTrigger.resource.split("/")[3]
const bucket = eventTrigger.resource!.startsWith("projects/_/buckets/")
? eventTrigger.resource!.split("/")[3]
: eventTrigger.resource;
const eventTriggerId = `${projectId}:${eventTrigger.eventType}:${bucket}`;
const triggers = this.multicastTriggers[eventTriggerId] || [];
Expand Down
8 changes: 3 additions & 5 deletions src/emulator/functionsEmulatorRuntime.ts
Expand Up @@ -1048,11 +1048,9 @@ async function main(): Promise<void> {
case "cloudevent":
const rawBody = (req as RequestWithRawBody).rawBody;
let reqBody = JSON.parse(rawBody.toString());
if (req.headers["content-type"]?.includes("cloudevent")) {
if (EventUtils.isBinaryCloudEvent(req)) {
reqBody = EventUtils.extractBinaryCloudEventContext(req);
reqBody.data = req.body;
}
if (EventUtils.isBinaryCloudEvent(req)) {
reqBody = EventUtils.extractBinaryCloudEventContext(req);
reqBody.data = req.body;
}
await processBackground(trigger, reqBody, FUNCTION_SIGNATURE);
res.send({ status: "acknowledged" });
Expand Down

0 comments on commit cd737c9

Please sign in to comment.