Skip to content

Fix Firebase Functions v4 compatibility issues. #175

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

Merged
merged 7 commits into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 13 additions & 18 deletions spec/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
Expand All @@ -57,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('service');
expect(context.resource.service).to.equal(
'unknown-service.googleapis.com'
);
expect(
/ref\/wildcard[1-9]\/nested\/anotherWildcard[1-9]/.test(
context.resource.name
Expand Down Expand Up @@ -153,7 +157,7 @@ describe('main', () => {
const cf = constructBackgroundCF(
'google.firebase.database.ref.create'
);
cf.__trigger.eventTrigger.resource =
cf.__endpoint.eventTrigger.eventFilters.resource =
'companies/{company}/users/{user}';
const wrapped = wrap(cf);
const context = wrapped(
Expand All @@ -173,7 +177,7 @@ describe('main', () => {

it('should extract the appropriate params for Firestore function trigger', () => {
const cf = constructBackgroundCF('google.firestore.document.create');
cf.__trigger.eventTrigger.resource =
cf.__endpoint.eventTrigger.eventFilters.resource =
'databases/(default)/documents/companies/{company}/users/{user}';
const wrapped = wrap(cf);
const context = wrapped(
Expand All @@ -195,7 +199,7 @@ describe('main', () => {
const cf = constructBackgroundCF(
'google.firebase.database.ref.create'
);
cf.__trigger.eventTrigger.resource =
cf.__endpoint.eventTrigger.eventFilters.resource =
'companies/{company}/users/{user}';
const wrapped = wrap(cf);
const context = wrapped(
Expand Down Expand Up @@ -243,11 +247,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<any>);
});
Expand Down Expand Up @@ -340,11 +341,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);
});
});
});
2 changes: 1 addition & 1 deletion spec/v2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
1 change: 0 additions & 1 deletion src/cloudevent/mocks/database/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CloudFunction, database } from 'firebase-functions/v2';
import { Expression } from 'firebase-functions/v2/params';
import { DeepPartial } from '../../types';
import {
exampleDataSnapshot,
Expand Down
15 changes: 9 additions & 6 deletions src/cloudevent/mocks/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<any>): string {
type CloudFunction = v1.CloudFunction<any> | v2.CloudFunction<any>;

export function getEventType(cloudFunction: CloudFunction): string {
return cloudFunction?.__endpoint?.eventTrigger?.eventType || '';
}

export function getEventFilters(
cloudFunction: CloudFunction<any>
cloudFunction: CloudFunction
): Record<string, string | Expression<string>> {
return cloudFunction?.__endpoint?.eventTrigger?.eventFilters || {};
}

export function getBaseCloudEvent<EventType extends CloudEvent<unknown>>(
cloudFunction: CloudFunction<EventType>
export function getBaseCloudEvent<EventType extends v2.CloudEvent<unknown>>(
cloudFunction: v2.CloudFunction<EventType>
): EventType {
return {
specversion: '1.0',
Expand Down
69 changes: 49 additions & 20 deletions src/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ import {
firestore,
HttpsFunction,
Runnable,
// @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 = {
Expand Down Expand Up @@ -116,13 +124,13 @@ export function wrapV1<T>(
export function wrapV1<T>(
cloudFunction: CloudFunction<T>
): WrappedScheduledFunction | WrappedFunction<T, CloudFunction<T>> {
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
) => {
Expand All @@ -139,10 +147,7 @@ export function wrapV1<T>(
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`.'
);
Expand All @@ -154,8 +159,7 @@ export function wrapV1<T>(
);
}

const isCallableFunction =
get(cloudFunction, '__trigger.labels.deployment-callable') === 'true';
const isCallableFunction = has(cloudFunction, '__endpoint.callableTrigger');

let wrapped: WrappedFunction<T, typeof cloudFunction> = (data, options) => {
// Although in Typescript we require `options` some of our JS samples do not pass it.
Expand Down Expand Up @@ -197,11 +201,12 @@ export function wrapV1<T>(

/** @internal */
export function _makeResourceName(
triggerResource: string,
triggerResource: string | Expression<string>,
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);
Expand Down Expand Up @@ -244,8 +249,8 @@ function _makeDefaultContext<T>(
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 = {};
Expand Down Expand Up @@ -281,7 +286,7 @@ function _makeDefaultContext<T>(
const defaultContext: EventContext = {
eventId: _makeEventId(),
resource: eventResource && {
service: cloudFunction.__trigger.eventTrigger?.service,
service: serviceFromEventType(eventType),
name: _makeResourceName(eventResource, params),
},
eventType,
Expand All @@ -292,20 +297,22 @@ function _makeDefaultContext<T>(
}

function _extractDatabaseParams(
triggerResource: string,
triggerResource: string | Expression<string>,
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<string>,
data: firestore.DocumentSnapshot
): EventContext['params'] {
const resource = resolveStringExpression(triggerResource);
// Resource format: databases/(default)/documents/<path>
return _extractParams(
triggerResource.replace(/^databases\/[^\/]+\/documents\//, ''),
resource.replace(/^databases\/[^\/]+\/documents\//, ''),
data.ref.path
);
}
Expand Down Expand Up @@ -338,16 +345,38 @@ export function _extractParams(
return params;
}

function serviceFromEventType(eventType?: string): string {
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) {
return match[1];
}
}
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. */
export function makeChange<T>(before: T, after: T): Change<T> {
return Change.fromObjects(before, after);
}

/** Mock values returned by `functions.config()`. */
export function mockConfig(conf: { [key: string]: { [key: string]: any } }) {
if (config.singleton) {
delete config.singleton;
if (resetCache) {
resetCache();
}

process.env.CLOUD_RUNTIME_CONFIG = JSON.stringify(conf);
}