-
Notifications
You must be signed in to change notification settings - Fork 213
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
Changes from 7 commits
5d6e827
3b43484
eb8e4fb
b3f9365
d2c4a56
2102689
ec42a87
8f92e1f
01de80b
c633bd9
b51ec90
4598383
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,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); | ||
}); | ||
}); | ||
}); |
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 | ||
colerogers marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
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 | ||
colerogers marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
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, | ||
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. 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) 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. Replaced this hard coded value with |
||
}, | ||
}; | ||
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] { | ||
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. 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]; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './alerts'; |
Uh oh!
There was an error while loading. Please reload this page.