Skip to content

Generic interface for provider enhancement #1020

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 12 commits into from
Jan 28, 2022
Merged
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"./v2/https": "./lib/v2/providers/https.js",
"./v2/params": "./lib/v2/params/index.js",
"./v2/pubsub": "./lib/v2/providers/pubsub.js",
"./v2/storage": "./lib/v2/providers/storage.js"
"./v2/storage": "./lib/v2/providers/storage.js",
"./v2/alerts": "./lib/v2/providers/alerts/index.js"
},
"typesVersions": {
"*": {
Expand Down Expand Up @@ -114,6 +115,9 @@
],
"v2/storage": [
"lib/v2/providers/storage"
],
"v2/alerts": [
"lib/v2/providers/alerts"
]
}
},
Expand Down
211 changes: 211 additions & 0 deletions spec/v2/providers/alerts/alerts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { expect } from 'chai';
import { CloudEvent, CloudFunction } from '../../../../src/v2/core';
import * as options from '../../../../src/v2/options';
import * as alerts from '../../../../src/v2/providers/alerts';
import { FULL_ENDPOINT, FULL_OPTIONS } from '../helpers';

const ALERT_TYPE = 'new-alert-type';
const APPID = '123456789';

function getMockFunction(): CloudFunction<alerts.FirebaseAlertData<String>> {
const func = (raw: CloudEvent<unknown>) => 42;
func.run = (event: CloudEvent<alerts.FirebaseAlertData<String>>) => 42;
func.__endpoint = {};
return func;
}

describe('alerts', () => {
describe('onAlertPublished', () => {
it('should create the function without opts', () => {
const result = alerts.onAlertPublished(ALERT_TYPE, () => 42);

expect(result.__endpoint).to.deep.equal({
platform: 'gcfv2',
labels: {},
eventTrigger: {
eventType: alerts.eventType,
eventFilters: {
alertType: ALERT_TYPE,
},
retry: false,
},
});
});

it('should create the function with opts', () => {
const result = alerts.onAlertPublished(
{
...FULL_OPTIONS,
alertType: ALERT_TYPE,
appId: APPID,
},
() => 42
);

expect(result.__endpoint).to.deep.equal({
...FULL_ENDPOINT,
eventTrigger: {
eventType: alerts.eventType,
eventFilters: {
alertType: ALERT_TYPE,
appId: APPID,
},
retry: false,
},
});
});

it('should have a .run method', () => {
const func = alerts.onAlertPublished(ALERT_TYPE, (event) => event);

const res = func.run('input' as any);

expect(res).to.equal('input');
});
});

describe('getEndpointAnnotation', () => {
beforeEach(() => {
process.env.GCLOUD_PROJECT = 'aProject';
});

afterEach(() => {
options.setGlobalOptions({});
delete process.env.GCLOUD_PROJECT;
});

it('should define the endpoint without appId and opts', () => {
const func = getMockFunction();

func.__endpoint = alerts.getEndpointAnnotation({}, ALERT_TYPE);

expect(func.__endpoint).to.deep.equal({
platform: 'gcfv2',
labels: {},
eventTrigger: {
eventType: alerts.eventType,
eventFilters: {
alertType: ALERT_TYPE,
},
retry: false,
},
});
});

it('should define a complex endpoint without appId', () => {
const func = getMockFunction();

func.__endpoint = alerts.getEndpointAnnotation(
{ ...FULL_OPTIONS },
ALERT_TYPE
);

expect(func.__endpoint).to.deep.equal({
...FULL_ENDPOINT,
eventTrigger: {
eventType: alerts.eventType,
eventFilters: {
alertType: ALERT_TYPE,
},
retry: false,
},
});
});

it('should define a complex endpoint', () => {
const func = getMockFunction();

func.__endpoint = alerts.getEndpointAnnotation(
{ ...FULL_OPTIONS },
ALERT_TYPE,
APPID
);

expect(func.__endpoint).to.deep.equal({
...FULL_ENDPOINT,
eventTrigger: {
eventType: alerts.eventType,
eventFilters: {
alertType: ALERT_TYPE,
appId: APPID,
},
retry: false,
},
});
});

it('should merge global & specific opts', () => {
options.setGlobalOptions({
concurrency: 20,
region: 'europe-west1',
minInstances: 1,
});
const specificOpts = {
region: 'us-west1',
minInstances: 3,
};
const func = getMockFunction();

func.__endpoint = alerts.getEndpointAnnotation(
specificOpts,
ALERT_TYPE,
APPID
);

expect(func.__endpoint).to.deep.equal({
platform: 'gcfv2',
labels: {},
concurrency: 20,
region: ['us-west1'],
minInstances: 3,
eventTrigger: {
eventType: alerts.eventType,
eventFilters: {
alertType: ALERT_TYPE,
appId: APPID,
},
retry: false,
},
});
});
});

describe('getOptsAndAlertTypeAndApp', () => {
it('should parse a string', () => {
const [opts, alertType, appId] = alerts.getOptsAndAlertTypeAndApp(
ALERT_TYPE
);

expect(opts).to.deep.equal({});
expect(alertType).to.equal(ALERT_TYPE);
expect(appId).to.be.undefined;
});

it('should parse an options object without appId', () => {
const myOpts: alerts.FirebaseAlertOptions = {
alertType: ALERT_TYPE,
region: 'us-west1',
};

const [opts, alertType, appId] = alerts.getOptsAndAlertTypeAndApp(myOpts);

expect(opts).to.deep.equal({ region: 'us-west1' });
expect(alertType).to.equal(myOpts.alertType);
expect(appId).to.be.undefined;
});

it('should parse an options object with appId', () => {
const myOpts: alerts.FirebaseAlertOptions = {
alertType: ALERT_TYPE,
appId: APPID,
region: 'us-west1',
};

const [opts, alertType, appId] = alerts.getOptsAndAlertTypeAndApp(myOpts);

expect(opts).to.deep.equal({ region: 'us-west1' });
expect(alertType).to.equal(myOpts.alertType);
expect(appId).to.be.equal(myOpts.appId);
});
});
});
8 changes: 6 additions & 2 deletions src/v2/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export interface TriggerAnnotation {
}

/**
* A CloudEvent is a cross-platform format for encoding a serverless event.
* A CloudEventBase is the base of a cross-platform format for encoding a serverless event.
* More information can be found in https://github.com/cloudevents/spec
*/
export interface CloudEvent<T> {
interface CloudEventBase<T> {
/** Version of the CloudEvents spec for this event. */
readonly specversion: '1.0';

Expand Down Expand Up @@ -89,6 +89,10 @@ export interface CloudEvent<T> {
params?: Record<string, string>;
}

/**
* A CloudEvent with custom extension attributes
*/
export type CloudEvent<T = any, Ext = {}> = CloudEventBase<T> & Ext;
/** A handler for CloudEvents. */
export interface CloudFunction<T> {
(raw: CloudEvent<unknown>): any | Promise<any>;
Expand Down
3 changes: 2 additions & 1 deletion src/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@

import * as logger from '../logger';
import * as params from './params';
import * as alerts from './providers/alerts';
import * as https from './providers/https';
import * as pubsub from './providers/pubsub';
import * as storage from './providers/storage';

export { https, pubsub, storage, logger, params };
export { https, pubsub, storage, logger, params, alerts };

export { setGlobalOptions, GlobalOptions } from './options';

Expand Down
127 changes: 127 additions & 0 deletions src/v2/providers/alerts/alerts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { ManifestEndpoint } from '../../../common/manifest';
import { CloudEvent, CloudFunction } from '../../core';
import * as options from '../../options';

/**
* The data object that is emitted from Firebase Alerts inside the CloudEvent
*/
export interface FirebaseAlertData<T = any> {
createTime: string;
endTime: string;
payload: T;
}

interface WithAlertTypeAndApp {
alertType: string;
appId?: string;
}
/**
* A custom CloudEvent for Firebase Alerts with custom extension attributes defined
*/
export type AlertEvent<T> = CloudEvent<
FirebaseAlertData<T>,
WithAlertTypeAndApp
>;

/** @internal */
export const eventType = 'firebase.firebasealerts.alerts.v1.published';

/** The underlying alert type of the Firebase Alerts provider */
export type AlertType =
| 'crashlytics.newFatalIssue'
| 'crashlytics.newNonfatalIssue'
| 'crashlytics.regression'
| 'crashlytics.stabilityDigest'
| 'crashlytics.velocity'
| 'crashlytics.newAnrIssue'
| 'billing.planUpdate'
| 'billing.automatedPlanUpdate'
| 'appDistribution.newTesterIosDevice'
| string;

/**
* Configuration for Firebase Alert functions
*/
export interface FirebaseAlertOptions extends options.EventHandlerOptions {
alertType: AlertType;
appId?: string;
}

/**
* Declares a function that can handle Firebase Alerts from CloudEvents
* @param alertTypeOrOpts the alert type or Firebase Alert function configuration
* @param handler a function that can handle the Firebase Alert inside a CloudEvent
*/
export function onAlertPublished<T extends { ['@type']: string } = any>(
alertTypeOrOpts: AlertType | FirebaseAlertOptions,
handler: (event: AlertEvent<T>) => any | Promise<any>
): CloudFunction<FirebaseAlertData<T>> {
const [opts, alertType, appId] = getOptsAndAlertTypeAndApp(alertTypeOrOpts);

const func = (raw: CloudEvent<unknown>) => {
return handler(
raw as CloudEvent<FirebaseAlertData<T>, WithAlertTypeAndApp>
);
};

func.run = handler;
func.__endpoint = getEndpointAnnotation(opts, alertType, appId);

return func;
}

/**
* @internal
* Helper function for getting the endpoint annotation used in alert handling functions
*/
export function getEndpointAnnotation(
opts: options.EventHandlerOptions,
alertType: string,
appId?: string
): ManifestEndpoint {
const baseOpts = options.optionsToEndpoint(options.getGlobalOptions());
const specificOpts = options.optionsToEndpoint(opts);
const endpoint: ManifestEndpoint = {
platform: 'gcfv2',
...baseOpts,
...specificOpts,
labels: {
...baseOpts?.labels,
...specificOpts?.labels,
},
eventTrigger: {
eventType,
eventFilters: {
alertType,
},
retry: false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we always hard code retry: false? We should be supporting booleans here now that GCF gen 2 supports retry (though I actually have a thread with them about changing this from a boolean to a more featured struct)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced this hard coded value with opts.retry in the latest commit

},
};
if (appId) {
endpoint.eventTrigger.eventFilters.appId = appId;
}
return endpoint;
}

/**
* @internal
* Helper function to parse the function opts, alert type, and appId
*/
export function getOptsAndAlertTypeAndApp(
alertTypeOrOpts: AlertType | FirebaseAlertOptions
): [options.EventHandlerOptions, string, string | undefined] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning a tuple is pythonic. Returning an object would have probably been more idiomatic JS. Regardless, this is probably easier to read so LGTM.

let opts: options.EventHandlerOptions;
let alertType: AlertType;
let appId: string | undefined;
if (typeof alertTypeOrOpts === 'string') {
alertType = alertTypeOrOpts;
opts = {};
} else {
alertType = alertTypeOrOpts.alertType;
appId = alertTypeOrOpts.appId;
opts = { ...alertTypeOrOpts };
delete (opts as any).alertType;
delete (opts as any).appId;
}
return [opts, alertType, appId];
}
1 change: 1 addition & 0 deletions src/v2/providers/alerts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './alerts';
Loading