-
Notifications
You must be signed in to change notification settings - Fork 431
fix(kits): restore the extensions' event payload shapes #3098
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: kits
Are you sure you want to change the base?
Changes from all commits
8145b50
6396a75
0b5f830
f69b065
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| /** | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| import type { FirestoreEvent } from "firebase-functions/v2/firestore"; | ||
|
|
||
| /** | ||
| * The 1st gen `EventContext` shape that the extension published inside its | ||
| * `onStart` and `onCompletion` payloads. | ||
| */ | ||
| export interface EventContext { | ||
| eventId: string; | ||
| timestamp: string; | ||
| eventType: string; | ||
| resource: { | ||
| service: string; | ||
| name: string; | ||
| }; | ||
| params: Record<string, string>; | ||
| } | ||
|
|
||
| /** | ||
| * Every 1st gen Firestore `onWrite` trigger reported this event type, so | ||
| * subscribers matching on `context.eventType` keep matching it. | ||
| */ | ||
| const FIRESTORE_WRITE_EVENT_TYPE = "google.firestore.document.write"; | ||
|
|
||
| const FIRESTORE_SERVICE = "firestore.googleapis.com"; | ||
|
|
||
| /** | ||
| * Rebuilds the 1st gen `EventContext` from a 2nd gen `FirestoreEvent`. | ||
| * | ||
| * The extension handed the 1st gen handler's `context` straight to Eventarc, so | ||
| * subscribers read `eventId`, `timestamp`, `eventType`, `resource` and `params` | ||
| * off it. `FirestoreEvent` carries the same information under different names, | ||
| * so the published payload keeps its original shape instead of following the | ||
| * 2nd gen handler signature. | ||
| */ | ||
| export function toEventContext( | ||
|
cabljac marked this conversation as resolved.
|
||
| event: FirestoreEvent<unknown, Record<string, string>> | ||
| ): EventContext { | ||
| return { | ||
| eventId: event.id, | ||
| timestamp: event.time, | ||
| eventType: FIRESTORE_WRITE_EVENT_TYPE, | ||
| resource: { | ||
| service: FIRESTORE_SERVICE, | ||
| name: `projects/${event.project}/databases/${event.database}/documents/${event.document}`, | ||
| }, | ||
| params: event.params, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think the extension published any params here, so "still ... under In v1, Worth verifying yourself before you change anything, since you already have the deploy set up. Harmless either way, a superset breaks nobody, but the README and the description both claim a parity I don't think holds. The more general point: the deploy test ran the kit's own
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One precision after reading |
||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /** | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| import { describe, expect, test } from "vitest"; | ||
| import { toEventContext } from "../src/event-context"; | ||
|
|
||
| /** | ||
| * The extension published its 1st gen handler's `context` verbatim, so these | ||
| * assertions pin the fields subscribers read off it. | ||
| */ | ||
| describe("toEventContext", () => { | ||
| const event = { | ||
| id: "event-1", | ||
| time: "2026-01-01T00:00:00.000Z", | ||
| project: "demo-project", | ||
| database: "(default)", | ||
| document: "pages/home/_counter_shards_/0000", | ||
| params: { collection: "pages", counter: "home", shardId: "0000" }, | ||
| } as any; | ||
|
|
||
| test("rebuilds the 1st gen event context from a 2nd gen event", () => { | ||
| expect(toEventContext(event)).toEqual({ | ||
| eventId: "event-1", | ||
| timestamp: "2026-01-01T00:00:00.000Z", | ||
| eventType: "google.firestore.document.write", | ||
| resource: { | ||
| service: "firestore.googleapis.com", | ||
| name: "projects/demo-project/databases/(default)/documents/pages/home/_counter_shards_/0000", | ||
| }, | ||
| params: { collection: "pages", counter: "home", shardId: "0000" }, | ||
| }); | ||
| }); | ||
|
|
||
| test("names the resource under the event's own database", () => { | ||
| const context = toEventContext({ ...event, database: "counters" }); | ||
|
|
||
| expect(context.resource.name).toBe( | ||
| "projects/demo-project/databases/counters/documents/pages/home/_counter_shards_/0000" | ||
| ); | ||
| }); | ||
|
|
||
| test("passes the trigger wildcards through unchanged", () => { | ||
| const params = { collection: "docs", counter: "a/b/c", shardId: "0001" }; | ||
|
|
||
| expect(toEventContext({ ...event, params }).params).toEqual(params); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,17 @@ | |
| */ | ||
|
|
||
| import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; | ||
| import { toEventContext } from "../src/event-context"; | ||
|
|
||
| /** A shard write on the `{collection}/{counter=**}/_counter_shards_/{shardId}` trigger. */ | ||
| const SHARD_WRITE = { | ||
| id: "event-1", | ||
| time: "2026-01-01T00:00:00.000Z", | ||
| project: "demo-project", | ||
| database: "(default)", | ||
| document: "pages/home/_counter_shards_/0000", | ||
| params: { collection: "pages", counter: "home", shardId: "0000" }, | ||
| } as any; | ||
|
|
||
| const publish = vi.fn(); | ||
| const channel = vi.fn(() => ({ publish })); | ||
|
|
@@ -81,11 +92,16 @@ describe("event publishing", () => { | |
| test("publishes start events", async () => { | ||
| const events = await setupEnabledEvents(); | ||
|
|
||
| await events.recordStartEvent({ params: { shardId: "0000" } }); | ||
| const context = toEventContext(SHARD_WRITE); | ||
|
|
||
| await events.recordStartEvent({ | ||
| change: { before: {}, after: {} }, | ||
| context, | ||
| }); | ||
|
|
||
| expect(publish).toHaveBeenCalledWith({ | ||
| type: "firebase.extensions.firestore-counter.v1.onStart", | ||
| data: { params: { shardId: "0000" } }, | ||
| data: { change: { before: {}, after: {} }, context }, | ||
| }); | ||
| }); | ||
|
|
||
|
|
@@ -119,12 +135,41 @@ describe("event publishing", () => { | |
| test("publishes completion events", async () => { | ||
| const events = await setupEnabledEvents(); | ||
|
|
||
| await events.recordCompletionEvent({ params: { shardId: "0000" } }); | ||
| const context = toEventContext(SHARD_WRITE); | ||
|
|
||
| await events.recordCompletionEvent({ context }); | ||
|
|
||
| expect(publish).toHaveBeenCalledWith({ | ||
| type: "firebase.extensions.firestore-counter.v1.onCompletion", | ||
| data: { params: { shardId: "0000" } }, | ||
| data: { context }, | ||
| }); | ||
| }); | ||
|
|
||
| test("puts the whole 1st gen context on the wire", async () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few small ones, none blocking:
|
||
| const events = await setupEnabledEvents(); | ||
| const context = toEventContext(SHARD_WRITE); | ||
|
|
||
| await events.recordStartEvent({ | ||
| change: { before: {}, after: {} }, | ||
| context, | ||
| }); | ||
| await events.recordCompletionEvent({ context }); | ||
|
|
||
| // `firebase-admin` sends the payload as `JSON.stringify(data)`, so this is | ||
| // what a subscriber of the extension's events actually reads. | ||
| expect(publish.mock.calls.length).toBe(2); | ||
| for (const [event] of publish.mock.calls) { | ||
| expect(JSON.parse(JSON.stringify(event.data)).context).toEqual({ | ||
| eventId: "event-1", | ||
| timestamp: "2026-01-01T00:00:00.000Z", | ||
| eventType: "google.firestore.document.write", | ||
| resource: { | ||
| service: "firestore.googleapis.com", | ||
| name: "projects/demo-project/databases/(default)/documents/pages/home/_counter_shards_/0000", | ||
| }, | ||
| params: { collection: "pages", counter: "home", shardId: "0000" }, | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| test("does nothing before the channel is set up", async () => { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.