From 9eb6e90f2efdb1236312261513823b56a9d29a36 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Mon, 10 May 2021 19:27:25 +0000 Subject: [PATCH 1/4] Convert legacy GCF events to CloudEvents --- .github/workflows/conformance.yml | 2 +- src/cloudevents.ts | 13 + src/functions.ts | 4 +- src/middelware/ce_to_legacy_event.ts | 30 +-- src/middelware/legacy_event_to_cloudevent.ts | 218 ++++++++++++++++ src/server.ts | 12 +- test/integration/cloudevent.ts | 173 ++++++++++-- test/middleware/ce_to_legacy_event.ts | 2 +- test/middleware/legacy_event_to_cloudevent.ts | 246 ++++++++++++++++++ 9 files changed, 659 insertions(+), 41 deletions(-) create mode 100644 src/middelware/legacy_event_to_cloudevent.ts create mode 100644 test/middleware/legacy_event_to_cloudevent.ts diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 78129542..bf4bcb5e 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -54,6 +54,6 @@ jobs: with: functionType: 'cloudevent' useBuildpacks: false - validateMapping: false + validateMapping: true workingDirectory: 'test/conformance' cmd: "'npm start -- --target=writeCloudEvent --signature-type=cloudevent'" diff --git a/src/cloudevents.ts b/src/cloudevents.ts index 1d28fbe3..ee468d5d 100644 --- a/src/cloudevents.ts +++ b/src/cloudevents.ts @@ -15,6 +15,19 @@ import * as express from 'express'; import {CloudEventsContext} from './functions'; +/** + * Custom exception class to represent errors durring event conversions. + */ +export class EventConversionError extends Error {} + +// CloudEvent service names. +export const FIREBASE_AUTH_CE_SERVICE = 'firebaseauth.googleapis.com'; +export const FIREBASE_CE_SERVICE = 'firebase.googleapis.com'; +export const FIREBASE_DB_CE_SERVICE = 'firebasedatabase.googleapis.com'; +export const FIRESTORE_CE_SERVICE = 'firestore.googleapis.com'; +export const PUBSUB_CE_SERVICE = 'pubsub.googleapis.com'; +export const STORAGE_CE_SERVICE = 'storage.googleapis.com'; + /** * Checks whether the incoming request is a CloudEvents event in binary content * mode. This is verified by checking the presence of required headers. diff --git a/src/functions.ts b/src/functions.ts index bedf5505..467f1a77 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -44,7 +44,7 @@ export type HandlerFunction = * A legacy event. */ export interface LegacyEvent { - data: object; + data: {[key: string]: any}; context: CloudFunctionsContext; } @@ -75,7 +75,7 @@ export interface CloudFunctionsContext { /** * The resource that emitted the event. */ - resource?: string | object; + resource?: string | {[key: string]: string}; } /** diff --git a/src/middelware/ce_to_legacy_event.ts b/src/middelware/ce_to_legacy_event.ts index d3c9b365..6c3f1aa3 100644 --- a/src/middelware/ce_to_legacy_event.ts +++ b/src/middelware/ce_to_legacy_event.ts @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. import {Request, Response, NextFunction} from 'express'; -import {isBinaryCloudEvent, getBinaryCloudEventContext} from '../cloudevents'; +import * as cloudevents from '../cloudevents'; -const CE_TO_BACKGROUND_TYPE = new Map( +export const CE_TO_BACKGROUND_TYPE = new Map( Object.entries({ 'google.cloud.pubsub.topic.v1.messagePublished': 'google.pubsub.topic.publish', @@ -49,11 +49,6 @@ const CE_TO_BACKGROUND_TYPE = new Map( }) ); -// CloudEvent service names. -const FIREBASE_AUTH_CE_SERVICE = 'firebaseauth.googleapis.com'; -const PUBSUB_CE_SERVICE = 'pubsub.googleapis.com'; -const STORAGE_CE_SERVICE = 'storage.googleapis.com'; - const PUBSUB_MESSAGE_TYPE = 'type.googleapis.com/google.pubsub.v1.PubsubMessage'; @@ -62,18 +57,13 @@ const PUBSUB_MESSAGE_TYPE = */ const CE_SOURCE_REGEX = /\/\/([^/]+)\/(.+)/; -/** - * Costom exception class to represent errors durring event converion. - */ -export class EventConversionError extends Error {} - /** * Is the given request a known CloudEvent that can be converted to a legacy event. * @param request express request object * @returns true if the request can be converted */ const isConvertableCloudEvent = (request: Request): boolean => { - if (isBinaryCloudEvent(request)) { + if (cloudevents.isBinaryCloudEvent(request)) { const ceType = request.header('ce-type'); return CE_TO_BACKGROUND_TYPE.has(ceType!); } @@ -90,7 +80,7 @@ export const parseSource = ( ): {service: string; name: string} => { const match = source.match(CE_SOURCE_REGEX); if (!match) { - throw new EventConversionError( + throw new cloudevents.EventConversionError( `Failed to convert CloudEvent with invalid source: "${source}"` ); } @@ -108,7 +98,7 @@ export const parseSource = ( const marshallConvertableCloudEvent = ( req: Request ): {context: object; data: object} => { - const ceContext = getBinaryCloudEventContext(req); + const ceContext = cloudevents.getBinaryCloudEventContext(req); const {service, name} = parseSource(ceContext.source!); const subject = ceContext.subject!; let data = req.body; @@ -117,7 +107,7 @@ const marshallConvertableCloudEvent = ( let resource: string | {[key: string]: string} = `${name}/${subject}`; switch (service) { - case PUBSUB_CE_SERVICE: + case cloudevents.PUBSUB_CE_SERVICE: // PubSub resource format resource = { service: service, @@ -129,7 +119,7 @@ const marshallConvertableCloudEvent = ( data = data.message; } break; - case FIREBASE_AUTH_CE_SERVICE: + case cloudevents.FIREBASE_AUTH_CE_SERVICE: // FirebaseAuth resource format resource = name; if ('metadata' in data) { @@ -144,7 +134,7 @@ const marshallConvertableCloudEvent = ( } } break; - case STORAGE_CE_SERVICE: + case cloudevents.STORAGE_CE_SERVICE: // CloudStorage resource format resource = { name: `${name}/${subject}`, @@ -180,11 +170,11 @@ export const ceToLegacyEventMiddleware = ( if (isConvertableCloudEvent(req)) { // This is a CloudEvent that can be converted a known legacy event. req.body = marshallConvertableCloudEvent(req); - } else if (isBinaryCloudEvent(req)) { + } else if (cloudevents.isBinaryCloudEvent(req)) { // Support CloudEvents in binary content mode, with data being the whole // request body and context attributes retrieved from request headers. req.body = { - context: getBinaryCloudEventContext(req), + context: cloudevents.getBinaryCloudEventContext(req), data: req.body, }; } diff --git a/src/middelware/legacy_event_to_cloudevent.ts b/src/middelware/legacy_event_to_cloudevent.ts new file mode 100644 index 00000000..2c5bc4c2 --- /dev/null +++ b/src/middelware/legacy_event_to_cloudevent.ts @@ -0,0 +1,218 @@ +// Copyright 2019 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 +// +// http://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 {Request, Response, NextFunction} from 'express'; +import * as cloudevents from '../cloudevents'; +import {CE_TO_BACKGROUND_TYPE} from './ce_to_legacy_event'; +import {CloudFunctionsContext, LegacyEvent} from '../functions'; + +const BACKGROUND_TO_CE_TYPE = new Map( + [...CE_TO_BACKGROUND_TYPE].map(x => [x[1], x[0]]) +); +BACKGROUND_TO_CE_TYPE.set( + 'providers/cloud.storage/eventTypes/object.change', + 'google.cloud.storage.object.v1.finalized' +); +BACKGROUND_TO_CE_TYPE.set( + 'providers/cloud.pubsub/eventTypes/topic.publish', + 'google.cloud.pubsub.topic.v1.messagePublished' +); + +// Maps background event services to their equivalent CloudEvent services. +const SERVICE_BACKGROUND_TO_CE = new Map( + Object.entries({ + 'providers/cloud.firestore/': cloudevents.FIRESTORE_CE_SERVICE, + 'providers/google.firebase.analytics/': cloudevents.FIREBASE_CE_SERVICE, + 'providers/firebase.auth/': cloudevents.FIREBASE_AUTH_CE_SERVICE, + 'providers/google.firebase.database/': cloudevents.FIREBASE_DB_CE_SERVICE, + 'providers/cloud.pubsub/': cloudevents.PUBSUB_CE_SERVICE, + 'providers/cloud.storage/': cloudevents.STORAGE_CE_SERVICE, + 'google.pubsub': cloudevents.PUBSUB_CE_SERVICE, + 'google.storage': cloudevents.STORAGE_CE_SERVICE, + }) +); + +/** + * Maps CloudEvent service strings to regular expressions used to split a background + * event resource string into CloudEvent resource and subject strings. Each regex + * must have exactly two capture groups: the first for the resource and the second + * for the subject. + */ +const CE_SERVICE_TO_RESOURCE_RE = new Map([ + [cloudevents.FIREBASE_CE_SERVICE, /^(projects\/[^/]+)\/(events\/[^/]+)$/], + [ + cloudevents.FIREBASE_DB_CE_SERVICE, + /^(projects\/[^/]\/instances\/[^/]+)\/(refs\/.+)$/, + ], + [ + cloudevents.FIRESTORE_CE_SERVICE, + /^(projects\/[^/]+\/databases\/\(default\))\/(documents\/.+)$/, + ], + [ + cloudevents.STORAGE_CE_SERVICE, + /^(projects\/[^/]\/buckets\/[^/]+)\/(objects\/.+)$/, + ], +]); + +/** + * Is this request a known GCF event that can be converted to a cloud event. + * @param req the express request object + * @returns true if this request can be converted to a CloudEvent + */ +const isConvertableLegacyEvent = (req: Request): boolean => { + const {body} = req; + const context = 'context' in body ? body.context : body; + return ( + !cloudevents.isBinaryCloudEvent(req) && + 'data' in body && + 'eventType' in context && + 'resource' in context && + BACKGROUND_TO_CE_TYPE.has(context.eventType) + ); +}; + +/** + * Convert the given HTTP request into the GCF Legacy Event data / context format. + * @param body the express request object + * @returns a marshalled legacy event + */ +const getLegacyEvent = (request: Request): LegacyEvent => { + let {context} = request.body; + const {data} = request.body; + if (!context) { + context = request.body; + context.data = undefined; + delete context.data; + } + return {context, data}; +}; + +interface ParsedResource { + service: string; + resource: string; + subject: string; +} + +/** + * Splits a background event's resource into a CloudEvent service, resource, and subject. + */ +export const splitResource = ( + context: CloudFunctionsContext +): ParsedResource => { + let service = ''; + let resource = ''; + let subject = ''; + if (typeof context.resource === 'string') { + resource = context.resource; + service = ''; + } else if (context.resource !== undefined) { + resource = context.resource.name ?? ''; + service = context.resource.service; + } + + if (!service) { + for (const [backgroundService, ceService] of SERVICE_BACKGROUND_TO_CE) { + if (context.eventType?.startsWith(backgroundService)) { + service = ceService; + } + } + } + + if (!service) { + throw new cloudevents.EventConversionError( + `Unable to find equivalent CloudEvent service for ${context.eventType}.` + ); + } + + const regex = CE_SERVICE_TO_RESOURCE_RE.get(service); + if (regex) { + const match = resource.match(regex); + if (match) { + resource = match[1]; + subject = match[2]; + } else { + throw new cloudevents.EventConversionError( + `Resource string did not match expected format: ${resource}.` + ); + } + } + return { + service, + resource, + subject, + }; +}; + +/** + * Express middleware to convert legacy GCF requests to cloud events. This enables functions + * using the "CLOUD_EVENT" signature type to accept requests from a legacy event producer. + * @param req express request object + * @param res express response object + * @param next function used to pass control to the next middleware function in the stack + */ +export const legacyEventToCloudEventMiddleware = ( + req: Request, + res: Response, + next: NextFunction +) => { + if (isConvertableLegacyEvent(req)) { + // eslint-disable-next-line prefer-const + let {context, data} = getLegacyEvent(req); + const newType = BACKGROUND_TO_CE_TYPE.get(context.eventType ?? ''); + if (!newType) { + throw new cloudevents.EventConversionError( + `Unable to find equivalent CloudEvent type for ${context.eventType}` + ); + } + // eslint-disable-next-line prefer-const + let {service, resource, subject} = splitResource(context); + + if (service === cloudevents.PUBSUB_CE_SERVICE) { + // PubSub data is nested under the "message" key. + data = {message: data}; + } + + if (service === cloudevents.FIREBASE_AUTH_CE_SERVICE) { + if ('metadata' in data) { + // Some metadata are not consistent between cloudevents and legacy events + const metadata: object = data.metadata; + data.metadata = {}; + // eslint-disable-next-line prefer-const + for (let [k, v] of Object.entries(metadata)) { + k = k === 'createdAt' ? 'createTime' : k; + k = k === 'lastSignedInAt' ? 'lastSignInTime' : k; + data.metadata[k] = v; + } + // Subject comes from the 'uid' field in the data payload. + if ('uid' in data) { + subject = `users/${data.uid}`; + } + } + } + + const cloudEvent: {[k: string]: string | object | undefined} = { + id: context.eventId, + time: context.timestamp, + specversion: '1.0', + datacontenttype: 'application/json', + type: newType, + source: `//${service}/${resource}`, + data, + }; + if (subject) { + cloudEvent.subject = subject; + } + req.body = cloudEvent; + } + next(); +}; diff --git a/src/server.ts b/src/server.ts index 0c94dd37..b716fa9d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -21,6 +21,7 @@ import {setLatestRes} from './invoker'; import {registerFunctionRoutes} from './router'; import {legacyPubSubEventMiddleware} from './pubsub_middleware'; import {ceToLegacyEventMiddleware} from './middelware/ce_to_legacy_event'; +import {legacyEventToCloudEventMiddleware} from './middelware/legacy_event_to_cloudevent'; /** * Creates and configures an Express application and returns an HTTP server @@ -99,13 +100,22 @@ export function getServer( // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header app.disable('x-powered-by'); - if (functionSignatureType === SignatureType.EVENT) { + if ( + functionSignatureType === SignatureType.EVENT || + functionSignatureType === SignatureType.CLOUDEVENT + ) { // If a Pub/Sub subscription is configured to invoke a user's function directly, the request body // needs to be marshalled into the structure that wrapEventFunction expects. This unblocks local // development with the Pub/Sub emulator app.use(legacyPubSubEventMiddleware); + } + + if (functionSignatureType === SignatureType.EVENT) { app.use(ceToLegacyEventMiddleware); } + if (functionSignatureType === SignatureType.CLOUDEVENT) { + app.use(legacyEventToCloudEventMiddleware); + } registerFunctionRoutes(app, userFunction, functionSignatureType); return http.createServer(app); diff --git a/test/integration/cloudevent.ts b/test/integration/cloudevent.ts index 42c793c7..6c706017 100644 --- a/test/integration/cloudevent.ts +++ b/test/integration/cloudevent.ts @@ -14,6 +14,7 @@ import * as assert from 'assert'; import * as functions from '../../src/functions'; +import * as sinon from 'sinon'; import {getServer} from '../../src/server'; import {SignatureType} from '../../src/types'; import * as supertest from 'supertest'; @@ -32,11 +33,25 @@ const TEST_CLOUD_EVENT = { }; describe('CloudEvent Function', () => { + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + // Prevent log spew from the PubSub emulator request. + sinon.stub(console, 'warn'); + }); + + afterEach(() => { + clock.restore(); + (console.warn as sinon.SinonSpy).restore(); + }); + const testData = [ { name: 'CloudEvents v1.0 structured content request', headers: {'Content-Type': 'application/cloudevents+json'}, body: TEST_CLOUD_EVENT, + expectedCloudEvent: TEST_CLOUD_EVENT, }, { name: 'CloudEvents v1.0 binary content request', @@ -51,10 +66,150 @@ describe('CloudEvent Function', () => { 'ce-datacontenttype': TEST_CLOUD_EVENT.datacontenttype, }, body: TEST_CLOUD_EVENT.data, + expectedCloudEvent: TEST_CLOUD_EVENT, + }, + { + name: 'PubSub GCF event request', + headers: {}, + body: { + context: { + eventId: 'aaaaaa-1111-bbbb-2222-cccccccccccc', + timestamp: '2020-09-29T11:32:00.000Z', + eventType: 'google.pubsub.topic.publish', + resource: { + service: 'pubsub.googleapis.com', + name: 'projects/sample-project/topics/gcf-test', + type: 'type.googleapis.com/google.pubsub.v1.PubsubMessage', + }, + }, + data: { + '@type': 'type.googleapis.com/google.pubsub.v1.PubsubMessage', + data: 'AQIDBA==', + }, + }, + expectedCloudEvent: { + specversion: '1.0', + type: 'google.cloud.pubsub.topic.v1.messagePublished', + source: + '//pubsub.googleapis.com/projects/sample-project/topics/gcf-test', + id: 'aaaaaa-1111-bbbb-2222-cccccccccccc', + time: '2020-09-29T11:32:00.000Z', + datacontenttype: 'application/json', + data: { + message: { + '@type': 'type.googleapis.com/google.pubsub.v1.PubsubMessage', + data: 'AQIDBA==', + }, + }, + }, + }, + { + name: 'Legacy PubSub GCF event request', + headers: {}, + body: { + eventId: 'aaaaaa-1111-bbbb-2222-cccccccccccc', + timestamp: '2020-09-29T11:32:00.000Z', + eventType: 'providers/cloud.pubsub/eventTypes/topic.publish', + resource: 'projects/sample-project/topics/gcf-test', + data: { + '@type': 'type.googleapis.com/google.pubsub.v1.PubsubMessage', + attributes: { + attribute1: 'value1', + }, + data: 'VGhpcyBpcyBhIHNhbXBsZSBtZXNzYWdl', + }, + }, + expectedCloudEvent: { + specversion: '1.0', + type: 'google.cloud.pubsub.topic.v1.messagePublished', + source: + '//pubsub.googleapis.com/projects/sample-project/topics/gcf-test', + id: 'aaaaaa-1111-bbbb-2222-cccccccccccc', + time: '2020-09-29T11:32:00.000Z', + datacontenttype: 'application/json', + data: { + message: { + '@type': 'type.googleapis.com/google.pubsub.v1.PubsubMessage', + attributes: { + attribute1: 'value1', + }, + data: 'VGhpcyBpcyBhIHNhbXBsZSBtZXNzYWdl', + }, + }, + }, + }, + { + name: 'PubSub emulator request', + headers: {}, + body: { + subscription: 'projects/FOO/subscriptions/BAR_SUB', + message: { + data: 'VGhpcyBpcyBhIHNhbXBsZSBtZXNzYWdl', + messageId: 'aaaaaa-1111-bbbb-2222-cccccccccccc', + attributes: { + attribute1: 'value1', + }, + }, + }, + expectedCloudEvent: { + specversion: '1.0', + type: 'google.cloud.pubsub.topic.v1.messagePublished', + source: '//pubsub.googleapis.com/', + id: 'aaaaaa-1111-bbbb-2222-cccccccccccc', + time: '1970-01-01T00:00:00.000Z', + datacontenttype: 'application/json', + data: { + message: { + '@type': 'type.googleapis.com/google.pubsub.v1.PubsubMessage', + attributes: { + attribute1: 'value1', + }, + data: 'VGhpcyBpcyBhIHNhbXBsZSBtZXNzYWdl', + }, + }, + }, + }, + { + name: 'Firebase Database GCF event request', + headers: {}, + body: { + eventType: 'providers/google.firebase.database/eventTypes/ref.write', + params: { + child: 'xyz', + }, + auth: { + admin: true, + }, + data: { + data: null, + delta: { + grandchild: 'other', + }, + }, + resource: 'projects/_/instances/my-project-id/refs/gcf-test/xyz', + timestamp: '2020-09-29T11:32:00.000Z', + eventId: 'aaaaaa-1111-bbbb-2222-cccccccccccc', + }, + expectedCloudEvent: { + specversion: '1.0', + type: 'google.firebase.database.document.v1.written', + source: + '//firebasedatabase.googleapis.com/projects/_/instances/my-project-id', + subject: 'refs/gcf-test/xyz', + id: 'aaaaaa-1111-bbbb-2222-cccccccccccc', + time: '2020-09-29T11:32:00.000Z', + datacontenttype: 'application/json', + data: { + data: null, + delta: { + grandchild: 'other', + }, + }, + }, }, ]; testData.forEach(test => { - it(`should receive data and context from ${test.name}`, async () => { + it(`${test.name}`, async () => { let receivedCloudEvent: functions.CloudEventsContext | null = null; const server = getServer((cloudevent: functions.CloudEventsContext) => { receivedCloudEvent = cloudevent as functions.CloudEventsContext; @@ -64,21 +219,7 @@ describe('CloudEvent Function', () => { .set(test.headers) .send(test.body) .expect(204); - assert.notStrictEqual(receivedCloudEvent, null); - assert.strictEqual( - receivedCloudEvent!.specversion, - TEST_CLOUD_EVENT.specversion - ); - assert.strictEqual(receivedCloudEvent!.type, TEST_CLOUD_EVENT.type); - assert.strictEqual(receivedCloudEvent!.source, TEST_CLOUD_EVENT.source); - assert.strictEqual(receivedCloudEvent!.subject, TEST_CLOUD_EVENT.subject); - assert.strictEqual(receivedCloudEvent!.id, TEST_CLOUD_EVENT.id); - assert.strictEqual(receivedCloudEvent!.time, TEST_CLOUD_EVENT.time); - assert.strictEqual( - receivedCloudEvent!.datacontenttype, - TEST_CLOUD_EVENT.datacontenttype - ); - assert.deepStrictEqual(receivedCloudEvent!.data, TEST_CLOUD_EVENT.data); + assert.deepStrictEqual(receivedCloudEvent, test.expectedCloudEvent); }); }); }); diff --git a/test/middleware/ce_to_legacy_event.ts b/test/middleware/ce_to_legacy_event.ts index 7eb363bc..dbc313b5 100644 --- a/test/middleware/ce_to_legacy_event.ts +++ b/test/middleware/ce_to_legacy_event.ts @@ -5,8 +5,8 @@ import {Response, Request} from 'express'; import { ceToLegacyEventMiddleware, parseSource, - EventConversionError, } from '../../src/middelware/ce_to_legacy_event'; +import {EventConversionError} from '../../src/cloudevents'; const ceHeaders = (eventType: string, source: string) => ({ 'ce-id': 'my-id', diff --git a/test/middleware/legacy_event_to_cloudevent.ts b/test/middleware/legacy_event_to_cloudevent.ts new file mode 100644 index 00000000..7b0ce6be --- /dev/null +++ b/test/middleware/legacy_event_to_cloudevent.ts @@ -0,0 +1,246 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import {Response, Request} from 'express'; + +import { + splitResource, + legacyEventToCloudEventMiddleware, +} from '../../src/middelware/legacy_event_to_cloudevent'; +import {CloudFunctionsContext} from '../../src/functions'; +import {EventConversionError} from '../../src/cloudevents'; + +describe('splitResource', () => { + const testData = [ + { + name: 'background resource', + context: { + eventType: 'google.storage.object.finalize', + resource: { + service: 'storage.googleapis.com', + name: 'projects/_/buckets/some-bucket/objects/folder/Test.cs', + type: 'storage#object', + }, + }, + expectedResult: { + service: 'storage.googleapis.com', + resource: 'projects/_/buckets/some-bucket', + subject: 'objects/folder/Test.cs', + }, + }, + + { + name: 'background resource without service', + context: { + eventType: 'google.storage.object.finalize', + resource: { + name: 'projects/_/buckets/some-bucket/objects/folder/Test.cs', + type: 'storage#object', + }, + }, + expectedResult: { + service: 'storage.googleapis.com', + resource: 'projects/_/buckets/some-bucket', + subject: 'objects/folder/Test.cs', + }, + }, + + { + name: 'background resource string', + context: { + eventType: 'google.storage.object.finalize', + resource: 'projects/_/buckets/some-bucket/objects/folder/Test.cs', + }, + expectedResult: { + service: 'storage.googleapis.com', + resource: 'projects/_/buckets/some-bucket', + subject: 'objects/folder/Test.cs', + }, + }, + { + name: 'unknown service and event type', + context: { + eventType: 'unknown_event_type', + resource: { + service: 'not_a_known_service', + name: 'projects/_/my/stuff/at/test.txt', + type: 'storage#object', + }, + }, + expectedResult: { + service: 'not_a_known_service', + resource: 'projects/_/my/stuff/at/test.txt', + subject: '', + }, + }, + ]; + + testData.forEach(test => { + it(test.name, () => { + const result = splitResource(test.context as CloudFunctionsContext); + assert.deepStrictEqual(result, test.expectedResult); + }); + }); + + it('throws an exception on unknown event type', () => { + const context = { + eventType: 'not_a_known_event_type', + resource: { + name: 'projects/_/buckets/some-bucket/objects/folder/Test.cs', + type: 'storage#object', + }, + }; + assert.throws(() => splitResource(context), EventConversionError); + }); + + it('throws an exception on unknown resource type', () => { + const context = { + eventType: 'google.storage.object.finalize', + resource: { + // This name will not match the regex associated with the service. + name: 'foo/bar/baz', + service: 'storage.googleapis.com', + type: 'storage#object', + }, + }; + assert.throws(() => splitResource(context), EventConversionError); + }); +}); + +describe('legacyEventToCloudEventMiddleware', () => { + const createLegacyEventBody = ( + eventType: string, + resource: {[k: string]: string} | string, + data: object = {data: '10'} + ) => ({ + context: { + eventId: '1215011316659232', + timestamp: '2020-05-18T12:13:19Z', + eventType, + resource, + }, + data, + }); + + const createCloudEventBody = ( + type: string, + source: string, + data: object, + subject?: string + ) => + Object.assign(subject ? {subject} : {}, { + specversion: '1.0', + id: '1215011316659232', + time: '2020-05-18T12:13:19Z', + datacontenttype: 'application/json', + type, + source, + data, + }); + + const testData = [ + { + name: 'CloudEvent', + body: { + specversion: '1.0', + type: 'com.google.cloud.storage', + source: + 'https://github.com/GoogleCloudPlatform/functions-framework-nodejs', + subject: 'test-subject', + id: 'test-1234-1234', + time: '2020-05-13T01:23:45Z', + datacontenttype: 'application/json', + data: { + some: 'payload', + }, + }, + expectedCloudEvent: { + specversion: '1.0', + type: 'com.google.cloud.storage', + source: + 'https://github.com/GoogleCloudPlatform/functions-framework-nodejs', + subject: 'test-subject', + id: 'test-1234-1234', + time: '2020-05-13T01:23:45Z', + datacontenttype: 'application/json', + data: { + some: 'payload', + }, + }, + }, + { + name: 'PubSub request', + body: createLegacyEventBody('google.pubsub.topic.publish', { + service: 'pubsub.googleapis.com', + name: 'projects/sample-project/topics/gcf-test', + type: 'type.googleapis.com/google.pubsub.v1.PubsubMessage', + }), + expectedCloudEvent: createCloudEventBody( + 'google.cloud.pubsub.topic.v1.messagePublished', + '//pubsub.googleapis.com/projects/sample-project/topics/gcf-test', + { + message: { + data: '10', + }, + } + ), + }, + { + name: 'Legacy PubSub request', + body: createLegacyEventBody( + 'providers/cloud.pubsub/eventTypes/topic.publish', + 'projects/sample-project/topics/gcf-test' + ), + expectedCloudEvent: createCloudEventBody( + 'google.cloud.pubsub.topic.v1.messagePublished', + '//pubsub.googleapis.com/projects/sample-project/topics/gcf-test', + { + message: { + data: '10', + }, + } + ), + }, + { + name: 'Firebase auth event', + body: createLegacyEventBody( + 'providers/firebase.auth/eventTypes/user.create', + 'projects/my-project-id', + { + email: 'test@nowhere.com', + metadata: { + createdAt: '2020-05-26T10:42:27Z', + lastSignedInAt: '2020-10-24T11:00:00Z', + }, + uid: 'UUpby3s4spZre6kHsgVSPetzQ8l2', + } + ), + expectedCloudEvent: createCloudEventBody( + 'google.firebase.auth.user.v1.created', + '//firebaseauth.googleapis.com/projects/my-project-id', + { + email: 'test@nowhere.com', + metadata: { + createTime: '2020-05-26T10:42:27Z', + lastSignInTime: '2020-10-24T11:00:00Z', + }, + uid: 'UUpby3s4spZre6kHsgVSPetzQ8l2', + }, + 'users/UUpby3s4spZre6kHsgVSPetzQ8l2' + ), + }, + ]; + + testData.forEach(test => { + it(test.name, () => { + const next = sinon.spy(); + const req = { + body: test.body, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + header: (_: string) => '', + }; + legacyEventToCloudEventMiddleware(req as Request, {} as Response, next); + assert.deepStrictEqual(req.body, test.expectedCloudEvent); + assert.strictEqual(next.called, true); + }); + }); +}); From 4d67df2009f296d18700aafad7f7ff224ee94b73 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Thu, 13 May 2021 17:38:07 +0000 Subject: [PATCH 2/4] clean up cloudevents service exports --- src/cloudevents.ts | 14 +++--- src/middelware/ce_to_legacy_event.ts | 23 ++++++---- src/middelware/legacy_event_to_cloudevent.ts | 48 ++++++++++---------- 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/cloudevents.ts b/src/cloudevents.ts index ee468d5d..8eaeffb9 100644 --- a/src/cloudevents.ts +++ b/src/cloudevents.ts @@ -21,12 +21,14 @@ import {CloudEventsContext} from './functions'; export class EventConversionError extends Error {} // CloudEvent service names. -export const FIREBASE_AUTH_CE_SERVICE = 'firebaseauth.googleapis.com'; -export const FIREBASE_CE_SERVICE = 'firebase.googleapis.com'; -export const FIREBASE_DB_CE_SERVICE = 'firebasedatabase.googleapis.com'; -export const FIRESTORE_CE_SERVICE = 'firestore.googleapis.com'; -export const PUBSUB_CE_SERVICE = 'pubsub.googleapis.com'; -export const STORAGE_CE_SERVICE = 'storage.googleapis.com'; +export const CE_SERVICE = { + FIREBASE_AUTH: 'firebaseauth.googleapis.com', + FIREBASE_DB: 'firebasedatabase.googleapis.com', + FIREBASE: 'firebase.googleapis.com', + FIRESTORE: 'firestore.googleapis.com', + PUBSUB: 'pubsub.googleapis.com', + STORAGE: 'storage.googleapis.com', +}; /** * Checks whether the incoming request is a CloudEvents event in binary content diff --git a/src/middelware/ce_to_legacy_event.ts b/src/middelware/ce_to_legacy_event.ts index 6c3f1aa3..6e8bd202 100644 --- a/src/middelware/ce_to_legacy_event.ts +++ b/src/middelware/ce_to_legacy_event.ts @@ -12,7 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. import {Request, Response, NextFunction} from 'express'; -import * as cloudevents from '../cloudevents'; +import { + CE_SERVICE, + isBinaryCloudEvent, + getBinaryCloudEventContext, + EventConversionError, +} from '../cloudevents'; export const CE_TO_BACKGROUND_TYPE = new Map( Object.entries({ @@ -63,7 +68,7 @@ const CE_SOURCE_REGEX = /\/\/([^/]+)\/(.+)/; * @returns true if the request can be converted */ const isConvertableCloudEvent = (request: Request): boolean => { - if (cloudevents.isBinaryCloudEvent(request)) { + if (isBinaryCloudEvent(request)) { const ceType = request.header('ce-type'); return CE_TO_BACKGROUND_TYPE.has(ceType!); } @@ -80,7 +85,7 @@ export const parseSource = ( ): {service: string; name: string} => { const match = source.match(CE_SOURCE_REGEX); if (!match) { - throw new cloudevents.EventConversionError( + throw new EventConversionError( `Failed to convert CloudEvent with invalid source: "${source}"` ); } @@ -98,7 +103,7 @@ export const parseSource = ( const marshallConvertableCloudEvent = ( req: Request ): {context: object; data: object} => { - const ceContext = cloudevents.getBinaryCloudEventContext(req); + const ceContext = getBinaryCloudEventContext(req); const {service, name} = parseSource(ceContext.source!); const subject = ceContext.subject!; let data = req.body; @@ -107,7 +112,7 @@ const marshallConvertableCloudEvent = ( let resource: string | {[key: string]: string} = `${name}/${subject}`; switch (service) { - case cloudevents.PUBSUB_CE_SERVICE: + case CE_SERVICE.PUBSUB: // PubSub resource format resource = { service: service, @@ -119,7 +124,7 @@ const marshallConvertableCloudEvent = ( data = data.message; } break; - case cloudevents.FIREBASE_AUTH_CE_SERVICE: + case CE_SERVICE.FIREBASE_AUTH: // FirebaseAuth resource format resource = name; if ('metadata' in data) { @@ -134,7 +139,7 @@ const marshallConvertableCloudEvent = ( } } break; - case cloudevents.STORAGE_CE_SERVICE: + case CE_SERVICE.STORAGE: // CloudStorage resource format resource = { name: `${name}/${subject}`, @@ -170,11 +175,11 @@ export const ceToLegacyEventMiddleware = ( if (isConvertableCloudEvent(req)) { // This is a CloudEvent that can be converted a known legacy event. req.body = marshallConvertableCloudEvent(req); - } else if (cloudevents.isBinaryCloudEvent(req)) { + } else if (isBinaryCloudEvent(req)) { // Support CloudEvents in binary content mode, with data being the whole // request body and context attributes retrieved from request headers. req.body = { - context: cloudevents.getBinaryCloudEventContext(req), + context: getBinaryCloudEventContext(req), data: req.body, }; } diff --git a/src/middelware/legacy_event_to_cloudevent.ts b/src/middelware/legacy_event_to_cloudevent.ts index 2c5bc4c2..c1925c22 100644 --- a/src/middelware/legacy_event_to_cloudevent.ts +++ b/src/middelware/legacy_event_to_cloudevent.ts @@ -12,7 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. import {Request, Response, NextFunction} from 'express'; -import * as cloudevents from '../cloudevents'; +import { + CE_SERVICE, + EventConversionError, + isBinaryCloudEvent, +} from '../cloudevents'; import {CE_TO_BACKGROUND_TYPE} from './ce_to_legacy_event'; import {CloudFunctionsContext, LegacyEvent} from '../functions'; @@ -31,14 +35,14 @@ BACKGROUND_TO_CE_TYPE.set( // Maps background event services to their equivalent CloudEvent services. const SERVICE_BACKGROUND_TO_CE = new Map( Object.entries({ - 'providers/cloud.firestore/': cloudevents.FIRESTORE_CE_SERVICE, - 'providers/google.firebase.analytics/': cloudevents.FIREBASE_CE_SERVICE, - 'providers/firebase.auth/': cloudevents.FIREBASE_AUTH_CE_SERVICE, - 'providers/google.firebase.database/': cloudevents.FIREBASE_DB_CE_SERVICE, - 'providers/cloud.pubsub/': cloudevents.PUBSUB_CE_SERVICE, - 'providers/cloud.storage/': cloudevents.STORAGE_CE_SERVICE, - 'google.pubsub': cloudevents.PUBSUB_CE_SERVICE, - 'google.storage': cloudevents.STORAGE_CE_SERVICE, + 'providers/cloud.firestore/': CE_SERVICE.FIRESTORE, + 'providers/google.firebase.analytics/': CE_SERVICE.FIREBASE, + 'providers/firebase.auth/': CE_SERVICE.FIREBASE_AUTH, + 'providers/google.firebase.database/': CE_SERVICE.FIREBASE_DB, + 'providers/cloud.pubsub/': CE_SERVICE.PUBSUB, + 'providers/cloud.storage/': CE_SERVICE.STORAGE, + 'google.pubsub': CE_SERVICE.PUBSUB, + 'google.storage': CE_SERVICE.STORAGE, }) ); @@ -49,19 +53,13 @@ const SERVICE_BACKGROUND_TO_CE = new Map( * for the subject. */ const CE_SERVICE_TO_RESOURCE_RE = new Map([ - [cloudevents.FIREBASE_CE_SERVICE, /^(projects\/[^/]+)\/(events\/[^/]+)$/], + [CE_SERVICE.FIREBASE, /^(projects\/[^/]+)\/(events\/[^/]+)$/], + [CE_SERVICE.FIREBASE_DB, /^(projects\/[^/]\/instances\/[^/]+)\/(refs\/.+)$/], [ - cloudevents.FIREBASE_DB_CE_SERVICE, - /^(projects\/[^/]\/instances\/[^/]+)\/(refs\/.+)$/, - ], - [ - cloudevents.FIRESTORE_CE_SERVICE, + CE_SERVICE.FIRESTORE, /^(projects\/[^/]+\/databases\/\(default\))\/(documents\/.+)$/, ], - [ - cloudevents.STORAGE_CE_SERVICE, - /^(projects\/[^/]\/buckets\/[^/]+)\/(objects\/.+)$/, - ], + [CE_SERVICE.STORAGE, /^(projects\/[^/]\/buckets\/[^/]+)\/(objects\/.+)$/], ]); /** @@ -73,7 +71,7 @@ const isConvertableLegacyEvent = (req: Request): boolean => { const {body} = req; const context = 'context' in body ? body.context : body; return ( - !cloudevents.isBinaryCloudEvent(req) && + !isBinaryCloudEvent(req) && 'data' in body && 'eventType' in context && 'resource' in context && @@ -129,7 +127,7 @@ export const splitResource = ( } if (!service) { - throw new cloudevents.EventConversionError( + throw new EventConversionError( `Unable to find equivalent CloudEvent service for ${context.eventType}.` ); } @@ -141,7 +139,7 @@ export const splitResource = ( resource = match[1]; subject = match[2]; } else { - throw new cloudevents.EventConversionError( + throw new EventConversionError( `Resource string did not match expected format: ${resource}.` ); } @@ -170,19 +168,19 @@ export const legacyEventToCloudEventMiddleware = ( let {context, data} = getLegacyEvent(req); const newType = BACKGROUND_TO_CE_TYPE.get(context.eventType ?? ''); if (!newType) { - throw new cloudevents.EventConversionError( + throw new EventConversionError( `Unable to find equivalent CloudEvent type for ${context.eventType}` ); } // eslint-disable-next-line prefer-const let {service, resource, subject} = splitResource(context); - if (service === cloudevents.PUBSUB_CE_SERVICE) { + if (service === CE_SERVICE.PUBSUB) { // PubSub data is nested under the "message" key. data = {message: data}; } - if (service === cloudevents.FIREBASE_AUTH_CE_SERVICE) { + if (service === CE_SERVICE.FIREBASE_AUTH) { if ('metadata' in data) { // Some metadata are not consistent between cloudevents and legacy events const metadata: object = data.metadata; From 7bf9b88549a552ad9068bd878139a7f934c6c750 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Thu, 13 May 2021 17:44:01 +0000 Subject: [PATCH 3/4] tidy up comments --- src/middelware/legacy_event_to_cloudevent.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/middelware/legacy_event_to_cloudevent.ts b/src/middelware/legacy_event_to_cloudevent.ts index c1925c22..0a6ab1c6 100644 --- a/src/middelware/legacy_event_to_cloudevent.ts +++ b/src/middelware/legacy_event_to_cloudevent.ts @@ -95,6 +95,9 @@ const getLegacyEvent = (request: Request): LegacyEvent => { return {context, data}; }; +/** + * The CloudEvent service, resource and subject fields parsed from a GCF event context. + */ interface ParsedResource { service: string; resource: string; @@ -103,6 +106,8 @@ interface ParsedResource { /** * Splits a background event's resource into a CloudEvent service, resource, and subject. + * @param context the GCF event context to parse. + * @returns the CloudEvent service, resource and subject fields for the given GCF event context. */ export const splitResource = ( context: CloudFunctionsContext @@ -152,8 +157,8 @@ export const splitResource = ( }; /** - * Express middleware to convert legacy GCF requests to cloud events. This enables functions - * using the "CLOUD_EVENT" signature type to accept requests from a legacy event producer. + * Express middleware to convert legacy GCF requests to CloudEvents. This enables functions + * using the "cloudevent" signature type to accept requests from a legacy event producer. * @param req express request object * @param res express response object * @param next function used to pass control to the next middleware function in the stack From 2373578959d1a1630c1e3be712e83f9fe796ab84 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Thu, 13 May 2021 20:20:21 +0000 Subject: [PATCH 4/4] prefer javascript objects over maps --- src/middelware/ce_to_legacy_event.ts | 70 ++++++++++---------- src/middelware/legacy_event_to_cloudevent.ts | 50 +++++++------- 2 files changed, 59 insertions(+), 61 deletions(-) diff --git a/src/middelware/ce_to_legacy_event.ts b/src/middelware/ce_to_legacy_event.ts index 6e8bd202..0db7cc4f 100644 --- a/src/middelware/ce_to_legacy_event.ts +++ b/src/middelware/ce_to_legacy_event.ts @@ -19,40 +19,38 @@ import { EventConversionError, } from '../cloudevents'; -export const CE_TO_BACKGROUND_TYPE = new Map( - Object.entries({ - 'google.cloud.pubsub.topic.v1.messagePublished': - 'google.pubsub.topic.publish', - 'google.cloud.storage.object.v1.finalized': - 'google.storage.object.finalize', - 'google.cloud.storage.object.v1.deleted': 'google.storage.object.delete', - 'google.cloud.storage.object.v1.archived': 'google.storage.object.archive', - 'google.cloud.storage.object.v1.metadataUpdated': - 'google.storage.object.metadataUpdate', - 'google.cloud.firestore.document.v1.written': - 'providers/cloud.firestore/eventTypes/document.write', - 'google.cloud.firestore.document.v1.created': - 'providers/cloud.firestore/eventTypes/document.create', - 'google.cloud.firestore.document.v1.updated': - 'providers/cloud.firestore/eventTypes/document.update', - 'google.cloud.firestore.document.v1.deleted': - 'providers/cloud.firestore/eventTypes/document.delete', - 'google.firebase.auth.user.v1.created': - 'providers/firebase.auth/eventTypes/user.create', - 'google.firebase.auth.user.v1.deleted': - 'providers/firebase.auth/eventTypes/user.delete', - 'google.firebase.analytics.log.v1.written': - 'providers/google.firebase.analytics/eventTypes/event.log', - 'google.firebase.database.document.v1.created': - 'providers/google.firebase.database/eventTypes/ref.create', - 'google.firebase.database.document.v1.written': - 'providers/google.firebase.database/eventTypes/ref.write', - 'google.firebase.database.document.v1.updated': - 'providers/google.firebase.database/eventTypes/ref.update', - 'google.firebase.database.document.v1.deleted': - 'providers/google.firebase.database/eventTypes/ref.delete', - }) -); +// Maps CloudEvent types to the equivalent GCF Event type +export const CE_TO_BACKGROUND_TYPE: {[k: string]: string} = { + 'google.cloud.pubsub.topic.v1.messagePublished': + 'google.pubsub.topic.publish', + 'google.cloud.storage.object.v1.finalized': 'google.storage.object.finalize', + 'google.cloud.storage.object.v1.deleted': 'google.storage.object.delete', + 'google.cloud.storage.object.v1.archived': 'google.storage.object.archive', + 'google.cloud.storage.object.v1.metadataUpdated': + 'google.storage.object.metadataUpdate', + 'google.cloud.firestore.document.v1.written': + 'providers/cloud.firestore/eventTypes/document.write', + 'google.cloud.firestore.document.v1.created': + 'providers/cloud.firestore/eventTypes/document.create', + 'google.cloud.firestore.document.v1.updated': + 'providers/cloud.firestore/eventTypes/document.update', + 'google.cloud.firestore.document.v1.deleted': + 'providers/cloud.firestore/eventTypes/document.delete', + 'google.firebase.auth.user.v1.created': + 'providers/firebase.auth/eventTypes/user.create', + 'google.firebase.auth.user.v1.deleted': + 'providers/firebase.auth/eventTypes/user.delete', + 'google.firebase.analytics.log.v1.written': + 'providers/google.firebase.analytics/eventTypes/event.log', + 'google.firebase.database.document.v1.created': + 'providers/google.firebase.database/eventTypes/ref.create', + 'google.firebase.database.document.v1.written': + 'providers/google.firebase.database/eventTypes/ref.write', + 'google.firebase.database.document.v1.updated': + 'providers/google.firebase.database/eventTypes/ref.update', + 'google.firebase.database.document.v1.deleted': + 'providers/google.firebase.database/eventTypes/ref.delete', +}; const PUBSUB_MESSAGE_TYPE = 'type.googleapis.com/google.pubsub.v1.PubsubMessage'; @@ -70,7 +68,7 @@ const CE_SOURCE_REGEX = /\/\/([^/]+)\/(.+)/; const isConvertableCloudEvent = (request: Request): boolean => { if (isBinaryCloudEvent(request)) { const ceType = request.header('ce-type'); - return CE_TO_BACKGROUND_TYPE.has(ceType!); + return !!ceType && ceType in CE_TO_BACKGROUND_TYPE; } return false; }; @@ -153,7 +151,7 @@ const marshallConvertableCloudEvent = ( context: { eventId: ceContext.id!, timestamp: ceContext.time!, - eventType: CE_TO_BACKGROUND_TYPE.get(ceContext.type!), + eventType: CE_TO_BACKGROUND_TYPE[ceContext.type!], resource, }, data, diff --git a/src/middelware/legacy_event_to_cloudevent.ts b/src/middelware/legacy_event_to_cloudevent.ts index 0a6ab1c6..aef91a90 100644 --- a/src/middelware/legacy_event_to_cloudevent.ts +++ b/src/middelware/legacy_event_to_cloudevent.ts @@ -20,31 +20,29 @@ import { import {CE_TO_BACKGROUND_TYPE} from './ce_to_legacy_event'; import {CloudFunctionsContext, LegacyEvent} from '../functions'; -const BACKGROUND_TO_CE_TYPE = new Map( - [...CE_TO_BACKGROUND_TYPE].map(x => [x[1], x[0]]) -); -BACKGROUND_TO_CE_TYPE.set( - 'providers/cloud.storage/eventTypes/object.change', - 'google.cloud.storage.object.v1.finalized' -); -BACKGROUND_TO_CE_TYPE.set( - 'providers/cloud.pubsub/eventTypes/topic.publish', - 'google.cloud.pubsub.topic.v1.messagePublished' +// Maps GCF Event types to the equivalent CloudEventType +const BACKGROUND_TO_CE_TYPE: {[key: string]: string} = Object.assign( + { + 'providers/cloud.storage/eventTypes/object.change': + 'google.cloud.storage.object.v1.finalized', + 'providers/cloud.pubsub/eventTypes/topic.publish': + 'google.cloud.pubsub.topic.v1.messagePublished', + }, + // include the inverse of CE_TO_BACKGROUND_TYPE + ...Object.entries(CE_TO_BACKGROUND_TYPE).map(([a, b]) => ({[b]: a})) ); // Maps background event services to their equivalent CloudEvent services. -const SERVICE_BACKGROUND_TO_CE = new Map( - Object.entries({ - 'providers/cloud.firestore/': CE_SERVICE.FIRESTORE, - 'providers/google.firebase.analytics/': CE_SERVICE.FIREBASE, - 'providers/firebase.auth/': CE_SERVICE.FIREBASE_AUTH, - 'providers/google.firebase.database/': CE_SERVICE.FIREBASE_DB, - 'providers/cloud.pubsub/': CE_SERVICE.PUBSUB, - 'providers/cloud.storage/': CE_SERVICE.STORAGE, - 'google.pubsub': CE_SERVICE.PUBSUB, - 'google.storage': CE_SERVICE.STORAGE, - }) -); +const SERVICE_BACKGROUND_TO_CE = { + 'providers/cloud.firestore/': CE_SERVICE.FIRESTORE, + 'providers/google.firebase.analytics/': CE_SERVICE.FIREBASE, + 'providers/firebase.auth/': CE_SERVICE.FIREBASE_AUTH, + 'providers/google.firebase.database/': CE_SERVICE.FIREBASE_DB, + 'providers/cloud.pubsub/': CE_SERVICE.PUBSUB, + 'providers/cloud.storage/': CE_SERVICE.STORAGE, + 'google.pubsub': CE_SERVICE.PUBSUB, + 'google.storage': CE_SERVICE.STORAGE, +}; /** * Maps CloudEvent service strings to regular expressions used to split a background @@ -75,7 +73,7 @@ const isConvertableLegacyEvent = (req: Request): boolean => { 'data' in body && 'eventType' in context && 'resource' in context && - BACKGROUND_TO_CE_TYPE.has(context.eventType) + context.eventType in BACKGROUND_TO_CE_TYPE ); }; @@ -124,7 +122,9 @@ export const splitResource = ( } if (!service) { - for (const [backgroundService, ceService] of SERVICE_BACKGROUND_TO_CE) { + for (const [backgroundService, ceService] of Object.entries( + SERVICE_BACKGROUND_TO_CE + )) { if (context.eventType?.startsWith(backgroundService)) { service = ceService; } @@ -171,7 +171,7 @@ export const legacyEventToCloudEventMiddleware = ( if (isConvertableLegacyEvent(req)) { // eslint-disable-next-line prefer-const let {context, data} = getLegacyEvent(req); - const newType = BACKGROUND_TO_CE_TYPE.get(context.eventType ?? ''); + const newType = BACKGROUND_TO_CE_TYPE[context.eventType ?? '']; if (!newType) { throw new EventConversionError( `Unable to find equivalent CloudEvent type for ${context.eventType}`