From 514b53b7d783da2e63546f314774349b4c9221da Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Thu, 13 Oct 2022 10:32:45 -0700 Subject: [PATCH 1/7] Fix broken mockConfig implementation for v4 SDK. In v4 SDK, we don't expose config singleton directly. --- src/v1.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/v1.ts b/src/v1.ts index e159e20..7f2338d 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -345,9 +345,14 @@ export function makeChange(before: T, after: T): Change { /** Mock values returned by `functions.config()`. */ export function mockConfig(conf: { [key: string]: { [key: string]: any } }) { - if (config.singleton) { - delete config.singleton; + const resetCache = require('firebase-functions').resetCache; + if (resetCache) { + resetCache(); + } else { + // Older versions of firebase-functions directly manipulated the config singleton + if ((config as any).singleton) { + delete (config as any).singleton; + } } - process.env.CLOUD_RUNTIME_CONFIG = JSON.stringify(conf); } From 0814b2c179bcb545d75792d320a39b2102b20ea3 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Thu, 13 Oct 2022 10:46:56 -0700 Subject: [PATCH 2/7] Fix broken mockConfig implementation for v4 SDK. In v4 SDK, we don't expose config singleton directly. --- src/v1.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/v1.ts b/src/v1.ts index 7f2338d..a4abc6c 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -32,6 +32,8 @@ import { firestore, HttpsFunction, Runnable, + // @ts-ignore + resetCache, } from 'firebase-functions'; /** Fields of the event context that can be overridden/customized. */ @@ -345,14 +347,8 @@ export function makeChange(before: T, after: T): Change { /** Mock values returned by `functions.config()`. */ export function mockConfig(conf: { [key: string]: { [key: string]: any } }) { - const resetCache = require('firebase-functions').resetCache; if (resetCache) { resetCache(); - } else { - // Older versions of firebase-functions directly manipulated the config singleton - if ((config as any).singleton) { - delete (config as any).singleton; - } } process.env.CLOUD_RUNTIME_CONFIG = JSON.stringify(conf); } From cac12def3146c521f065e9504f5bd577bbd43bf2 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Thu, 13 Oct 2022 11:14:26 -0700 Subject: [PATCH 3/7] Remove no longer relevant test. --- spec/main.spec.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spec/main.spec.ts b/spec/main.spec.ts index d34d79c..97f0aa9 100644 --- a/spec/main.spec.ts +++ b/spec/main.spec.ts @@ -340,11 +340,5 @@ describe('main', () => { expect(functions.config()).to.deep.equal(config); }); - - it('should not throw an error when functions.config.singleton is missing', () => { - delete functions.config.singleton; - - expect(() => mockConfig(config)).to.not.throw(Error); - }); }); }); From 3209550a2b403fc5b6f43494d516af21d688efee Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Thu, 13 Oct 2022 14:39:39 -0700 Subject: [PATCH 4/7] Fix Firebase Functions v4 compatibility issues. 1. Replace all use of __trigger annotation to __endpoint 2. Correct import path for Expression class/type. --- spec/main.spec.ts | 29 ++++++----- spec/v2.spec.ts | 2 +- src/cloudevent/mocks/database/helpers.ts | 1 - src/cloudevent/mocks/helpers.ts | 15 +++--- src/v1.ts | 64 +++++++++++++++++------- 5 files changed, 71 insertions(+), 40 deletions(-) diff --git a/spec/main.spec.ts b/spec/main.spec.ts index 97f0aa9..eab0917 100644 --- a/spec/main.spec.ts +++ b/spec/main.spec.ts @@ -39,11 +39,13 @@ describe('main', () => { set(cloudFunction, 'run', (data, context) => { return { data, context }; }); - set(cloudFunction, '__trigger', { + set(cloudFunction, '__endpoint', { eventTrigger: { - resource: 'ref/{wildcard}/nested/{anotherWildcard}', + eventFilters: { + resource: 'ref/{wildcard}/nested/{anotherWildcard}', + }, eventType: eventType || 'event', - service: 'service', + retry: false, }, }); return cloudFunction as functions.CloudFunction; @@ -57,7 +59,7 @@ describe('main', () => { it('should generate the appropriate context if no fields specified', () => { const context = wrap(constructBackgroundCF())('data').context; expect(typeof context.eventId).to.equal('string'); - expect(context.resource.service).to.equal('service'); + expect(context.resource.service).to.equal('unknown-service.googleapis.com'); expect( /ref\/wildcard[1-9]\/nested\/anotherWildcard[1-9]/.test( context.resource.name @@ -153,8 +155,8 @@ describe('main', () => { const cf = constructBackgroundCF( 'google.firebase.database.ref.create' ); - cf.__trigger.eventTrigger.resource = - 'companies/{company}/users/{user}'; + cf.__endpoint.eventTrigger.eventFilters.resource = + 'companies/{company}/users/{user}'; const wrapped = wrap(cf); const context = wrapped( features.database.makeDataSnapshot( @@ -173,8 +175,8 @@ describe('main', () => { it('should extract the appropriate params for Firestore function trigger', () => { const cf = constructBackgroundCF('google.firestore.document.create'); - cf.__trigger.eventTrigger.resource = - 'databases/(default)/documents/companies/{company}/users/{user}'; + cf.__endpoint.eventTrigger.eventFilters.resource = + 'databases/(default)/documents/companies/{company}/users/{user}'; const wrapped = wrap(cf); const context = wrapped( features.firestore.makeDocumentSnapshot( @@ -195,8 +197,8 @@ describe('main', () => { const cf = constructBackgroundCF( 'google.firebase.database.ref.create' ); - cf.__trigger.eventTrigger.resource = - 'companies/{company}/users/{user}'; + cf.__endpoint.eventTrigger.eventFilters.resource = + 'companies/{company}/users/{user}'; const wrapped = wrap(cf); const context = wrapped( features.database.makeDataSnapshot( @@ -243,11 +245,8 @@ describe('main', () => { set(cloudFunction, 'run', (data, context) => { return { data, context }; }); - set(cloudFunction, '__trigger', { - labels: { - 'deployment-callable': 'true', - }, - httpsTrigger: {}, + set(cloudFunction, '__endpoint', { + callableTrigger: {}, }); wrappedCF = wrap(cloudFunction as functions.CloudFunction); }); diff --git a/spec/v2.spec.ts b/spec/v2.spec.ts index f66bd4c..e10fa0d 100644 --- a/spec/v2.spec.ts +++ b/spec/v2.spec.ts @@ -33,7 +33,7 @@ import { eventarc, https, } from 'firebase-functions/v2'; -import { defineString } from 'firebase-functions/v2/params'; +import { defineString } from 'firebase-functions/params'; import { makeDataSnapshot } from '../src/providers/database'; describe('v2', () => { diff --git a/src/cloudevent/mocks/database/helpers.ts b/src/cloudevent/mocks/database/helpers.ts index 5fd76ae..f107176 100644 --- a/src/cloudevent/mocks/database/helpers.ts +++ b/src/cloudevent/mocks/database/helpers.ts @@ -1,5 +1,4 @@ import { CloudFunction, database } from 'firebase-functions/v2'; -import { Expression } from 'firebase-functions/v2/params'; import { DeepPartial } from '../../types'; import { exampleDataSnapshot, diff --git a/src/cloudevent/mocks/helpers.ts b/src/cloudevent/mocks/helpers.ts index 3c8563b..5b3cc85 100644 --- a/src/cloudevent/mocks/helpers.ts +++ b/src/cloudevent/mocks/helpers.ts @@ -1,22 +1,25 @@ -import { CloudEvent, CloudFunction } from 'firebase-functions/v2'; -import { Expression } from 'firebase-functions/v2/params'; +import * as v1 from 'firebase-functions'; +import * as v2 from 'firebase-functions/v2'; +import { Expression } from 'firebase-functions/params'; export const APP_ID = '__APP_ID__'; export const PROJECT_ID = '42'; export const FILENAME = 'file_name'; -export function getEventType(cloudFunction: CloudFunction): string { +type CloudFunction = v1.CloudFunction | v2.CloudFunction; + +export function getEventType(cloudFunction: CloudFunction): string { return cloudFunction?.__endpoint?.eventTrigger?.eventType || ''; } export function getEventFilters( - cloudFunction: CloudFunction + cloudFunction: CloudFunction ): Record> { return cloudFunction?.__endpoint?.eventTrigger?.eventFilters || {}; } -export function getBaseCloudEvent>( - cloudFunction: CloudFunction +export function getBaseCloudEvent>( + cloudFunction: v2.CloudFunction ): EventType { return { specversion: '1.0', diff --git a/src/v1.ts b/src/v1.ts index a4abc6c..1d9dd09 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -35,6 +35,12 @@ import { // @ts-ignore resetCache, } from 'firebase-functions'; +import { Expression } from 'firebase-functions/params'; +import { + getEventFilters, + getEventType, + resolveStringExpression, +} from './cloudevent/mocks/helpers'; /** Fields of the event context that can be overridden/customized. */ export type EventContextOptions = { @@ -118,13 +124,13 @@ export function wrapV1( export function wrapV1( cloudFunction: CloudFunction ): WrappedScheduledFunction | WrappedFunction> { - if (!has(cloudFunction, '__trigger')) { + if (!has(cloudFunction, '__endpoint')) { throw new Error( 'Wrap can only be called on functions written with the firebase-functions SDK.' ); } - if (get(cloudFunction, '__trigger.labels.deployment-scheduled') === 'true') { + if (has(cloudFunction, '__endpoint.scheduleTrigger')) { const scheduledWrapped: WrappedScheduledFunction = ( options: ContextOptions ) => { @@ -141,10 +147,7 @@ export function wrapV1( return scheduledWrapped; } - if ( - has(cloudFunction, '__trigger.httpsTrigger') && - get(cloudFunction, '__trigger.labels.deployment-callable') !== 'true' - ) { + if (has(cloudFunction, '__endpoint.httpsTrigger')) { throw new Error( 'Wrap function is only available for `onCall` HTTP functions, not `onRequest`.' ); @@ -156,8 +159,7 @@ export function wrapV1( ); } - const isCallableFunction = - get(cloudFunction, '__trigger.labels.deployment-callable') === 'true'; + const isCallableFunction = has(cloudFunction, '__endpoint.callableTrigger'); let wrapped: WrappedFunction = (data, options) => { // Although in Typescript we require `options` some of our JS samples do not pass it. @@ -199,11 +201,12 @@ export function wrapV1( /** @internal */ export function _makeResourceName( - triggerResource: string, + triggerResource: string | Expression, params = {} ): string { + const resource = resolveStringExpression(triggerResource); const wildcardRegex = new RegExp('{[^/{}]*}', 'g'); - let resourceName = triggerResource.replace(wildcardRegex, (wildcard) => { + let resourceName = resource.replace(wildcardRegex, (wildcard) => { let wildcardNoBraces = wildcard.slice(1, -1); // .slice removes '{' and '}' from wildcard let sub = get(params, wildcardNoBraces); return sub || wildcardNoBraces + random(1, 9); @@ -246,8 +249,8 @@ function _makeDefaultContext( triggerData?: T ): EventContext { let eventContextOptions = options as EventContextOptions; - const eventResource = cloudFunction.__trigger.eventTrigger?.resource; - const eventType = cloudFunction.__trigger.eventTrigger?.eventType; + const eventType = getEventType(cloudFunction); + const eventResource = getEventFilters(cloudFunction).resource; const optionsParams = eventContextOptions.params ?? {}; let triggerParams = {}; @@ -283,7 +286,7 @@ function _makeDefaultContext( const defaultContext: EventContext = { eventId: _makeEventId(), resource: eventResource && { - service: cloudFunction.__trigger.eventTrigger?.service, + service: serviceFromEventType(eventType), name: _makeResourceName(eventResource, params), }, eventType, @@ -294,20 +297,22 @@ function _makeDefaultContext( } function _extractDatabaseParams( - triggerResource: string, + triggerResource: string | Expression, data: database.DataSnapshot ): EventContext['params'] { + const resource = resolveStringExpression(triggerResource); const path = data.ref.toString().replace(data.ref.root.toString(), ''); - return _extractParams(triggerResource, path); + return _extractParams(resource, path); } function _extractFirestoreDocumentParams( - triggerResource: string, + triggerResource: string | Expression, data: firestore.DocumentSnapshot ): EventContext['params'] { + const resource = resolveStringExpression(triggerResource); // Resource format: databases/(default)/documents/ return _extractParams( - triggerResource.replace(/^databases\/[^\/]+\/documents\//, ''), + resource.replace(/^databases\/[^\/]+\/documents\//, ''), data.ref.path ); } @@ -340,6 +345,31 @@ export function _extractParams( return params; } +function serviceFromEventType(eventType?: string): string { + let service = 'unknown-service.googleapis.com'; + + if (eventType) { + const providerToService: Array<[string, string]> = [ + ['google.analytics', 'app-measurement.com'], + ['google.firebase.auth', 'firebaseauth.googleapis.com'], + ['google.firebase.database', 'firebaseio.com'], + ['google.firestore', 'firestore.googleapis.com'], + ['google.pubsub', 'pubsub.googleapis.com'], + ['google.firebase.remoteconfig', 'firebaseremoteconfig.googleapis.com'], + ['google.storage', 'storage.googleapis.com'], + ['google.testing', 'testing.googleapis.com'], + ]; + + const match = providerToService.find(([provider]) => { + eventType.includes(provider); + }); + if (match) { + service = match[1]; + } + } + return service; +} + /** Make a Change object to be used as test data for Firestore and real time database onWrite and onUpdate functions. */ export function makeChange(before: T, after: T): Change { return Change.fromObjects(before, after); From 5b091e53709b72237f17eeb6618fa9cce5150a7f Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Fri, 14 Oct 2022 10:16:18 -0700 Subject: [PATCH 5/7] Cleanup code per feedback. --- src/v1.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/v1.ts b/src/v1.ts index 1d9dd09..abd8d03 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -346,8 +346,6 @@ export function _extractParams( } function serviceFromEventType(eventType?: string): string { - let service = 'unknown-service.googleapis.com'; - if (eventType) { const providerToService: Array<[string, string]> = [ ['google.analytics', 'app-measurement.com'], @@ -364,10 +362,10 @@ function serviceFromEventType(eventType?: string): string { eventType.includes(provider); }); if (match) { - service = match[1]; + return match[1]; } } - return service; + return 'unknown-service.googleapis.com'; } /** Make a Change object to be used as test data for Firestore and real time database onWrite and onUpdate functions. */ From dc29fefd42e96fbe803c8f474fd76a463c572e80 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Fri, 14 Oct 2022 10:17:46 -0700 Subject: [PATCH 6/7] Fix lint issues. --- src/cloudevent/mocks/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cloudevent/mocks/helpers.ts b/src/cloudevent/mocks/helpers.ts index 5b3cc85..aed664b 100644 --- a/src/cloudevent/mocks/helpers.ts +++ b/src/cloudevent/mocks/helpers.ts @@ -1,5 +1,5 @@ import * as v1 from 'firebase-functions'; -import * as v2 from 'firebase-functions/v2'; +import * as v2 from 'firebase-functions/v2'; import { Expression } from 'firebase-functions/params'; export const APP_ID = '__APP_ID__'; From 3aca64368e590ba640a28b92e5f7571a876fdf0c Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Fri, 14 Oct 2022 10:20:13 -0700 Subject: [PATCH 7/7] One more lint issue. --- spec/main.spec.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/main.spec.ts b/spec/main.spec.ts index eab0917..867cc50 100644 --- a/spec/main.spec.ts +++ b/spec/main.spec.ts @@ -59,7 +59,9 @@ describe('main', () => { it('should generate the appropriate context if no fields specified', () => { const context = wrap(constructBackgroundCF())('data').context; expect(typeof context.eventId).to.equal('string'); - expect(context.resource.service).to.equal('unknown-service.googleapis.com'); + expect(context.resource.service).to.equal( + 'unknown-service.googleapis.com' + ); expect( /ref\/wildcard[1-9]\/nested\/anotherWildcard[1-9]/.test( context.resource.name @@ -156,7 +158,7 @@ describe('main', () => { 'google.firebase.database.ref.create' ); cf.__endpoint.eventTrigger.eventFilters.resource = - 'companies/{company}/users/{user}'; + 'companies/{company}/users/{user}'; const wrapped = wrap(cf); const context = wrapped( features.database.makeDataSnapshot( @@ -176,7 +178,7 @@ describe('main', () => { it('should extract the appropriate params for Firestore function trigger', () => { const cf = constructBackgroundCF('google.firestore.document.create'); cf.__endpoint.eventTrigger.eventFilters.resource = - 'databases/(default)/documents/companies/{company}/users/{user}'; + 'databases/(default)/documents/companies/{company}/users/{user}'; const wrapped = wrap(cf); const context = wrapped( features.firestore.makeDocumentSnapshot( @@ -198,7 +200,7 @@ describe('main', () => { 'google.firebase.database.ref.create' ); cf.__endpoint.eventTrigger.eventFilters.resource = - 'companies/{company}/users/{user}'; + 'companies/{company}/users/{user}'; const wrapped = wrap(cf); const context = wrapped( features.database.makeDataSnapshot(