From 5d6e827dda00e65919920e2e059ea630183bb48b Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Wed, 19 Jan 2022 14:41:49 -0800 Subject: [PATCH 01/12] breaking out general interface --- package.json | 18 +- spec/v2/providers/alerts/alerts.spec.ts | 235 ++++++++++++++++++++++++ spec/v2/providers/helpers.ts | 15 ++ src/v2/core.ts | 6 +- src/v2/index.ts | 3 +- src/v2/options.ts | 2 +- src/v2/providers/alerts/alerts.ts | 121 ++++++++++++ src/v2/providers/alerts/index.ts | 1 + src/v2/providers/https.ts | 4 +- src/v2/providers/pubsub.ts | 4 +- src/v2/providers/storage.ts | 8 +- v2/alerts/index.js | 26 +++ 12 files changed, 431 insertions(+), 12 deletions(-) create mode 100644 spec/v2/providers/alerts/alerts.spec.ts create mode 100644 src/v2/providers/alerts/alerts.ts create mode 100644 src/v2/providers/alerts/index.ts create mode 100644 v2/alerts/index.js diff --git a/package.json b/package.json index 50637bb74..0be8d9ef9 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,11 @@ "./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", + "./v2/alerts/appDistribution": "./lib/v2/providers/alerts/appDistribution.js", + "./v2/alerts/billing": "./lib/v2/providers/alerts/billing.js", + "./v2/alerts/crashlytics": "./lib/v2/providers/alerts/crashlytics.js" }, "typesVersions": { "*": { @@ -114,6 +118,18 @@ ], "v2/storage": [ "lib/v2/providers/storage" + ], + "v2/alerts": [ + "lib/v2/providers/alerts" + ], + "v2/alerts/appDistribution": [ + "lib/v2/providers/alerts/appDistribution" + ], + "v2/alerts/billing": [ + "lib/v2/providers/alerts/billing" + ], + "v2/alerts/crashlytics": [ + "lib/v2/providers/alerts/crashlytics" ] } }, diff --git a/spec/v2/providers/alerts/alerts.spec.ts b/spec/v2/providers/alerts/alerts.spec.ts new file mode 100644 index 000000000..1f3ebeaf6 --- /dev/null +++ b/spec/v2/providers/alerts/alerts.spec.ts @@ -0,0 +1,235 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as config from '../../../../src/config'; +import { CloudEvent, CloudFunction } from '../../../../src/v2/core'; +import * as options from '../../../../src/v2/options'; +import * as alerts from '../../../../src/v2/providers/alerts'; +import { + BASIC_ENDPOINT, + BASIC_OPTIONS, + FULL_ENDPOINT, + FULL_OPTIONS, +} from '../helpers'; + +const ALERT_TYPE = 'new-alert-type'; +const APPID = '123456789'; + +function getMockFunction(): CloudFunction> { + const func = (raw: CloudEvent) => 42; + func.run = (event: CloudEvent>) => 42; + func.__trigger = 'silence the transpiler'; + 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( + { + ...BASIC_OPTIONS, + alertType: ALERT_TYPE, + appId: APPID, + }, + () => 42 + ); + + expect(result.__endpoint).to.deep.equal({ + ...BASIC_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('defineEndpoint', () => { + let configStub: sinon.SinonStub; + + beforeEach(() => { + process.env.GCLOUD_PROJECT = 'aProject'; + configStub = sinon.stub(config, 'firebaseConfig'); + }); + + afterEach(() => { + options.setGlobalOptions({}); + delete process.env.GCLOUD_PROJECT; + configStub.restore(); + }); + + it('should define the endpoint without appId and opts', () => { + const func = getMockFunction(); + + alerts.defineEndpoint(func, {}, ALERT_TYPE); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: ALERT_TYPE, + }, + retry: false, + }, + }); + }); + + it('should define the endpoint without appId, with opts', () => { + const func = getMockFunction(); + + alerts.defineEndpoint(func, { ...BASIC_OPTIONS }, ALERT_TYPE); + + expect(func.__endpoint).to.deep.equal({ + ...BASIC_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: ALERT_TYPE, + }, + retry: false, + }, + }); + }); + + it('should define the endpoint with appId', () => { + const func = getMockFunction(); + + alerts.defineEndpoint(func, { ...BASIC_OPTIONS }, ALERT_TYPE, APPID); + + expect(func.__endpoint).to.deep.equal({ + ...BASIC_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: ALERT_TYPE, + appId: APPID, + }, + retry: false, + }, + }); + }); + + it('should define a complex endpoint', () => { + const func = getMockFunction(); + + alerts.defineEndpoint( + func, + { ...FULL_OPTIONS }, + 'new-alert-type', + '123456789' + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: 'new-alert-type', + appId: '123456789', + }, + 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(); + + alerts.defineEndpoint(func, 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); + }); + }); +}); diff --git a/spec/v2/providers/helpers.ts b/spec/v2/providers/helpers.ts index a499be69a..24f1327fe 100644 --- a/spec/v2/providers/helpers.ts +++ b/spec/v2/providers/helpers.ts @@ -1,5 +1,20 @@ import * as options from '../../../src/v2/options'; +export const BASIC_OPTIONS: options.EventHandlerOptions = { + labels: { + someKey: 'someValue', + }, + region: 'us-east1', +}; + +export const BASIC_ENDPOINT = { + platform: 'gcfv2', + region: ['us-east1'], + labels: { + someKey: 'someValue', + }, +}; + export const FULL_OPTIONS: options.GlobalOptions = { region: 'us-west1', memory: '512MB', diff --git a/src/v2/core.ts b/src/v2/core.ts index c790be7a8..823708542 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -52,7 +52,7 @@ export interface TriggerAnnotation { * A CloudEvent is a cross-platform format for encoding a serverless event. * More information can be found in https://github.com/cloudevents/spec */ -export interface CloudEvent { +export interface CloudEventBase { /** Version of the CloudEvents spec for this event. */ readonly specversion: '1.0'; @@ -89,6 +89,10 @@ export interface CloudEvent { params?: Record; } +/** + * + */ +export type CloudEvent = CloudEventBase & Ext; /** A handler for CloudEvents. */ export interface CloudFunction { (raw: CloudEvent): any | Promise; diff --git a/src/v2/index.ts b/src/v2/index.ts index 5c99a4678..3de9b749f 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -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'; diff --git a/src/v2/options.ts b/src/v2/options.ts index 7db01167e..eeeed0c16 100644 --- a/src/v2/options.ts +++ b/src/v2/options.ts @@ -26,11 +26,11 @@ import { durationFromSeconds, serviceAccountFromShorthand, } from '../common/encoding'; +import { ManifestEndpoint } from '../common/manifest'; import * as logger from '../logger'; import { TriggerAnnotation } from './core'; import { declaredParams } from './params'; import { ParamSpec } from './params/types'; -import { ManifestEndpoint } from '../common/manifest'; /** * List of all regions supported by Cloud Functions v2 diff --git a/src/v2/providers/alerts/alerts.ts b/src/v2/providers/alerts/alerts.ts new file mode 100644 index 000000000..e45a965d9 --- /dev/null +++ b/src/v2/providers/alerts/alerts.ts @@ -0,0 +1,121 @@ +import { ManifestEndpoint } from '../../../common/manifest'; +import { CloudEvent, CloudFunction } from '../../core'; +import * as options from '../../options'; + +// data of the CloudEvent +export interface FirebaseAlertData { + createTime: string; + endTime: string; + payload: T; +} + +export type AlertType = + | 'crashlytics.newFatalIssue' + | 'crashlytics.newNonfatalIssue' + | 'crashlytics.regression' + | 'crashlytics.stabilityDigest' + | 'crashlytics.velocity' + | 'crashlytics.newAnrIssue' + | 'billing.planUpdate' + | 'billing.automatedPlanUpdate' + | 'appDistribution.newTesterIosDevice' + | string; // for forward and backward compatibility + +/** Options */ +export interface FirebaseAlertOptions extends options.EventHandlerOptions { + alertType: AlertType; // required + appId?: string; // optional +} + +interface WithAlertTypeAndApp { + alertType: string; + appId?: string; // optional in the payload +} +export type AlertEvent = CloudEvent< + FirebaseAlertData, + WithAlertTypeAndApp +>; + +/** @internal */ +export const eventType = 'firebase.firebasealerts.alerts.v1.published'; + +/** Handlers */ +export function onAlertPublished( + alertTypeOrOpts: AlertType | FirebaseAlertOptions, + handler: (event: AlertEvent) => any | Promise +): CloudFunction> { + const [opts, alertType, appId] = getOptsAndAlertTypeAndApp(alertTypeOrOpts); + + const func = (raw: CloudEvent) => { + return handler( + raw as CloudEvent, WithAlertTypeAndApp> + ); + }; + + func.run = handler; + + // TypeScript doesn't recognize defineProperty as adding a property and complains + // that __endpoint doesn't exist. We can either cast to any and lose all type safety + // or we can just assign a meaningless value before calling defineProperty. + func.__trigger = 'silence the transpiler'; + func.__endpoint = {} as ManifestEndpoint; + defineEndpoint(func, opts, alertType, appId); + + return func; +} + +/** @internal */ +export function defineEndpoint( + func: CloudFunction>, + opts: options.EventHandlerOptions, + alertType: string, + appId?: string +): void { + Object.defineProperty(func, '__endpoint', { + get: () => { + 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, + }, + }; + if (appId) { + endpoint.eventTrigger.eventFilters.appId = appId; + } + return endpoint; + }, + }); +} + +/** @internal */ +export function getOptsAndAlertTypeAndApp( + alertTypeOrOpts: AlertType | FirebaseAlertOptions +): [options.EventHandlerOptions, string, string | undefined] { + 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]; +} diff --git a/src/v2/providers/alerts/index.ts b/src/v2/providers/alerts/index.ts new file mode 100644 index 000000000..c40670cd1 --- /dev/null +++ b/src/v2/providers/alerts/index.ts @@ -0,0 +1 @@ +export * from './alerts'; diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index 292c7cf0e..64ebb0d19 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -28,7 +28,7 @@ import { copyIfPresent, } from '../../common/encoding'; -import * as options from '../options'; +import { ManifestEndpoint } from '../../common/manifest'; import { CallableRequest, FunctionsErrorCode, @@ -40,7 +40,7 @@ import { TaskRequest, TaskRetryConfig, } from '../../common/providers/https'; -import { ManifestEndpoint } from '../../common/manifest'; +import * as options from '../options'; export { Request, diff --git a/src/v2/providers/pubsub.ts b/src/v2/providers/pubsub.ts index a751f0cd2..4cab7256c 100644 --- a/src/v2/providers/pubsub.ts +++ b/src/v2/providers/pubsub.ts @@ -1,7 +1,7 @@ -import * as options from '../options'; -import { CloudEvent, CloudFunction } from '../core'; import { copyIfPresent } from '../../common/encoding'; import { ManifestEndpoint } from '../../common/manifest'; +import { CloudEvent, CloudFunction } from '../core'; +import * as options from '../options'; /** * Interface representing a Google Cloud Pub/Sub message. diff --git a/src/v2/providers/storage.ts b/src/v2/providers/storage.ts index e03115df0..4ccb273c7 100644 --- a/src/v2/providers/storage.ts +++ b/src/v2/providers/storage.ts @@ -20,11 +20,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import * as options from '../options'; -import { firebaseConfig } from '../../config'; -import { CloudEvent, CloudFunction } from '../core'; import { copyIfPresent } from '../../common/encoding'; import { ManifestEndpoint } from '../../common/manifest'; +import { firebaseConfig } from '../../config'; +import { CloudEvent, CloudFunction } from '../core'; +import * as options from '../options'; /** * An object within Google Cloud Storage. @@ -359,7 +359,7 @@ export function onOperation( ...specificOpts?.labels, }, eventTrigger: { - eventType: eventType, + eventType, eventFilters: { bucket, }, diff --git a/v2/alerts/index.js b/v2/alerts/index.js new file mode 100644 index 000000000..7d725acc3 --- /dev/null +++ b/v2/alerts/index.js @@ -0,0 +1,26 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// This file is not part of the firebase-functions SDK. It is used to silence the +// imports eslint plugin until it can understand import paths defined by node +// package exports. +// For more information, see github.com/import-js/eslint-plugin-import/issues/1810 From 3b434840e66409538106cdecdc3017d38451cd6e Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Wed, 19 Jan 2022 14:50:23 -0800 Subject: [PATCH 02/12] cleaning up exports --- src/v2/core.ts | 4 ++-- src/v2/options.ts | 2 +- src/v2/providers/https.ts | 4 ++-- src/v2/providers/pubsub.ts | 4 ++-- src/v2/providers/storage.ts | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/v2/core.ts b/src/v2/core.ts index 823708542..2d9bbd8ba 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -49,7 +49,7 @@ 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 CloudEventBase { @@ -90,7 +90,7 @@ export interface CloudEventBase { } /** - * + * A CloudEvent with custom extension attributes */ export type CloudEvent = CloudEventBase & Ext; /** A handler for CloudEvents. */ diff --git a/src/v2/options.ts b/src/v2/options.ts index eeeed0c16..7db01167e 100644 --- a/src/v2/options.ts +++ b/src/v2/options.ts @@ -26,11 +26,11 @@ import { durationFromSeconds, serviceAccountFromShorthand, } from '../common/encoding'; -import { ManifestEndpoint } from '../common/manifest'; import * as logger from '../logger'; import { TriggerAnnotation } from './core'; import { declaredParams } from './params'; import { ParamSpec } from './params/types'; +import { ManifestEndpoint } from '../common/manifest'; /** * List of all regions supported by Cloud Functions v2 diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index 64ebb0d19..292c7cf0e 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -28,7 +28,7 @@ import { copyIfPresent, } from '../../common/encoding'; -import { ManifestEndpoint } from '../../common/manifest'; +import * as options from '../options'; import { CallableRequest, FunctionsErrorCode, @@ -40,7 +40,7 @@ import { TaskRequest, TaskRetryConfig, } from '../../common/providers/https'; -import * as options from '../options'; +import { ManifestEndpoint } from '../../common/manifest'; export { Request, diff --git a/src/v2/providers/pubsub.ts b/src/v2/providers/pubsub.ts index 4cab7256c..a751f0cd2 100644 --- a/src/v2/providers/pubsub.ts +++ b/src/v2/providers/pubsub.ts @@ -1,7 +1,7 @@ +import * as options from '../options'; +import { CloudEvent, CloudFunction } from '../core'; import { copyIfPresent } from '../../common/encoding'; import { ManifestEndpoint } from '../../common/manifest'; -import { CloudEvent, CloudFunction } from '../core'; -import * as options from '../options'; /** * Interface representing a Google Cloud Pub/Sub message. diff --git a/src/v2/providers/storage.ts b/src/v2/providers/storage.ts index 4ccb273c7..d07e1955a 100644 --- a/src/v2/providers/storage.ts +++ b/src/v2/providers/storage.ts @@ -20,11 +20,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { copyIfPresent } from '../../common/encoding'; -import { ManifestEndpoint } from '../../common/manifest'; +import * as options from '../options'; import { firebaseConfig } from '../../config'; import { CloudEvent, CloudFunction } from '../core'; -import * as options from '../options'; +import { copyIfPresent } from '../../common/encoding'; +import { ManifestEndpoint } from '../../common/manifest'; /** * An object within Google Cloud Storage. From eb8e4fb4cc63082d3002cca4b4f90032e4a78868 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Thu, 20 Jan 2022 07:27:45 -0800 Subject: [PATCH 03/12] removing comments & references to specific interfaces --- package.json | 14 +------------- src/v2/providers/alerts/alerts.ts | 15 +++++++++------ 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 0be8d9ef9..d19a8c5cf 100644 --- a/package.json +++ b/package.json @@ -55,10 +55,7 @@ "./v2/params": "./lib/v2/params/index.js", "./v2/pubsub": "./lib/v2/providers/pubsub.js", "./v2/storage": "./lib/v2/providers/storage.js", - "./v2/alerts": "./lib/v2/providers/alerts/index.js", - "./v2/alerts/appDistribution": "./lib/v2/providers/alerts/appDistribution.js", - "./v2/alerts/billing": "./lib/v2/providers/alerts/billing.js", - "./v2/alerts/crashlytics": "./lib/v2/providers/alerts/crashlytics.js" + "./v2/alerts": "./lib/v2/providers/alerts/index.js" }, "typesVersions": { "*": { @@ -121,15 +118,6 @@ ], "v2/alerts": [ "lib/v2/providers/alerts" - ], - "v2/alerts/appDistribution": [ - "lib/v2/providers/alerts/appDistribution" - ], - "v2/alerts/billing": [ - "lib/v2/providers/alerts/billing" - ], - "v2/alerts/crashlytics": [ - "lib/v2/providers/alerts/crashlytics" ] } }, diff --git a/src/v2/providers/alerts/alerts.ts b/src/v2/providers/alerts/alerts.ts index e45a965d9..d35409941 100644 --- a/src/v2/providers/alerts/alerts.ts +++ b/src/v2/providers/alerts/alerts.ts @@ -2,7 +2,7 @@ import { ManifestEndpoint } from '../../../common/manifest'; import { CloudEvent, CloudFunction } from '../../core'; import * as options from '../../options'; -// data of the CloudEvent +/** Data */ export interface FirebaseAlertData { createTime: string; endTime: string; @@ -19,17 +19,17 @@ export type AlertType = | 'billing.planUpdate' | 'billing.automatedPlanUpdate' | 'appDistribution.newTesterIosDevice' - | string; // for forward and backward compatibility + | string; /** Options */ export interface FirebaseAlertOptions extends options.EventHandlerOptions { - alertType: AlertType; // required - appId?: string; // optional + alertType: AlertType; + appId?: string; } interface WithAlertTypeAndApp { alertType: string; - appId?: string; // optional in the payload + appId?: string; } export type AlertEvent = CloudEvent< FirebaseAlertData, @@ -64,7 +64,10 @@ export function onAlertPublished( return func; } -/** @internal */ +/** + * @internal + * Helper function for defining alerting endpoints + */ export function defineEndpoint( func: CloudFunction>, opts: options.EventHandlerOptions, From b3f9365a1956307f635381969023063ec6cfb66e Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Thu, 20 Jan 2022 07:30:41 -0800 Subject: [PATCH 04/12] format --- src/v2/providers/alerts/alerts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2/providers/alerts/alerts.ts b/src/v2/providers/alerts/alerts.ts index d35409941..ac126e8a5 100644 --- a/src/v2/providers/alerts/alerts.ts +++ b/src/v2/providers/alerts/alerts.ts @@ -64,7 +64,7 @@ export function onAlertPublished( return func; } -/** +/** * @internal * Helper function for defining alerting endpoints */ From d2c4a567142c6c535ebd97914ca4f84b0bbd9398 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Thu, 20 Jan 2022 08:04:38 -0800 Subject: [PATCH 05/12] jsdoc comments --- src/v2/providers/alerts/alerts.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/v2/providers/alerts/alerts.ts b/src/v2/providers/alerts/alerts.ts index ac126e8a5..d3a52eaf1 100644 --- a/src/v2/providers/alerts/alerts.ts +++ b/src/v2/providers/alerts/alerts.ts @@ -9,6 +9,10 @@ export interface FirebaseAlertData { payload: T; } +/** @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' @@ -27,6 +31,7 @@ export interface FirebaseAlertOptions extends options.EventHandlerOptions { appId?: string; } +/** Cloud Event Type */ interface WithAlertTypeAndApp { alertType: string; appId?: string; @@ -36,10 +41,8 @@ export type AlertEvent = CloudEvent< WithAlertTypeAndApp >; -/** @internal */ -export const eventType = 'firebase.firebasealerts.alerts.v1.published'; - /** Handlers */ +/** Handle an alert published */ export function onAlertPublished( alertTypeOrOpts: AlertType | FirebaseAlertOptions, handler: (event: AlertEvent) => any | Promise @@ -66,7 +69,7 @@ export function onAlertPublished( /** * @internal - * Helper function for defining alerting endpoints + * Helper function for defining an endpoint for alert handling functions */ export function defineEndpoint( func: CloudFunction>, From 210268925ce9cb00602b233966d07c42760669e5 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Mon, 24 Jan 2022 13:33:31 -0800 Subject: [PATCH 06/12] address pr comments --- spec/v2/providers/alerts/alerts.spec.ts | 66 +++++---------- spec/v2/providers/helpers.ts | 15 ---- src/v2/core.ts | 2 +- src/v2/providers/alerts/alerts.ts | 103 ++++++++++++------------ 4 files changed, 73 insertions(+), 113 deletions(-) diff --git a/spec/v2/providers/alerts/alerts.spec.ts b/spec/v2/providers/alerts/alerts.spec.ts index 1f3ebeaf6..a9c87a85f 100644 --- a/spec/v2/providers/alerts/alerts.spec.ts +++ b/spec/v2/providers/alerts/alerts.spec.ts @@ -1,15 +1,8 @@ import { expect } from 'chai'; -import * as sinon from 'sinon'; -import * as config from '../../../../src/config'; import { CloudEvent, CloudFunction } from '../../../../src/v2/core'; import * as options from '../../../../src/v2/options'; import * as alerts from '../../../../src/v2/providers/alerts'; -import { - BASIC_ENDPOINT, - BASIC_OPTIONS, - FULL_ENDPOINT, - FULL_OPTIONS, -} from '../helpers'; +import { FULL_ENDPOINT, FULL_OPTIONS } from '../helpers'; const ALERT_TYPE = 'new-alert-type'; const APPID = '123456789'; @@ -17,7 +10,6 @@ const APPID = '123456789'; function getMockFunction(): CloudFunction> { const func = (raw: CloudEvent) => 42; func.run = (event: CloudEvent>) => 42; - func.__trigger = 'silence the transpiler'; func.__endpoint = {}; return func; } @@ -43,7 +35,7 @@ describe('alerts', () => { it('should create the function with opts', () => { const result = alerts.onAlertPublished( { - ...BASIC_OPTIONS, + ...FULL_OPTIONS, alertType: ALERT_TYPE, appId: APPID, }, @@ -51,7 +43,7 @@ describe('alerts', () => { ); expect(result.__endpoint).to.deep.equal({ - ...BASIC_ENDPOINT, + ...FULL_ENDPOINT, eventTrigger: { eventType: alerts.eventType, eventFilters: { @@ -72,24 +64,20 @@ describe('alerts', () => { }); }); - describe('defineEndpoint', () => { - let configStub: sinon.SinonStub; - + describe('getEndpointAnnotation', () => { beforeEach(() => { process.env.GCLOUD_PROJECT = 'aProject'; - configStub = sinon.stub(config, 'firebaseConfig'); }); afterEach(() => { options.setGlobalOptions({}); delete process.env.GCLOUD_PROJECT; - configStub.restore(); }); it('should define the endpoint without appId and opts', () => { const func = getMockFunction(); - alerts.defineEndpoint(func, {}, ALERT_TYPE); + func.__endpoint = alerts.getEndpointAnnotation({}, ALERT_TYPE); expect(func.__endpoint).to.deep.equal({ platform: 'gcfv2', @@ -104,35 +92,20 @@ describe('alerts', () => { }); }); - it('should define the endpoint without appId, with opts', () => { - const func = getMockFunction(); - - alerts.defineEndpoint(func, { ...BASIC_OPTIONS }, ALERT_TYPE); - - expect(func.__endpoint).to.deep.equal({ - ...BASIC_ENDPOINT, - eventTrigger: { - eventType: alerts.eventType, - eventFilters: { - alertType: ALERT_TYPE, - }, - retry: false, - }, - }); - }); - - it('should define the endpoint with appId', () => { + it('should define a complex endpoint without appId', () => { const func = getMockFunction(); - alerts.defineEndpoint(func, { ...BASIC_OPTIONS }, ALERT_TYPE, APPID); + func.__endpoint = alerts.getEndpointAnnotation( + { ...FULL_OPTIONS }, + ALERT_TYPE + ); expect(func.__endpoint).to.deep.equal({ - ...BASIC_ENDPOINT, + ...FULL_ENDPOINT, eventTrigger: { eventType: alerts.eventType, eventFilters: { alertType: ALERT_TYPE, - appId: APPID, }, retry: false, }, @@ -142,11 +115,10 @@ describe('alerts', () => { it('should define a complex endpoint', () => { const func = getMockFunction(); - alerts.defineEndpoint( - func, + func.__endpoint = alerts.getEndpointAnnotation( { ...FULL_OPTIONS }, - 'new-alert-type', - '123456789' + ALERT_TYPE, + APPID ); expect(func.__endpoint).to.deep.equal({ @@ -154,8 +126,8 @@ describe('alerts', () => { eventTrigger: { eventType: alerts.eventType, eventFilters: { - alertType: 'new-alert-type', - appId: '123456789', + alertType: ALERT_TYPE, + appId: APPID, }, retry: false, }, @@ -174,7 +146,11 @@ describe('alerts', () => { }; const func = getMockFunction(); - alerts.defineEndpoint(func, specificOpts, ALERT_TYPE, APPID); + func.__endpoint = alerts.getEndpointAnnotation( + specificOpts, + ALERT_TYPE, + APPID + ); expect(func.__endpoint).to.deep.equal({ platform: 'gcfv2', diff --git a/spec/v2/providers/helpers.ts b/spec/v2/providers/helpers.ts index 24f1327fe..a499be69a 100644 --- a/spec/v2/providers/helpers.ts +++ b/spec/v2/providers/helpers.ts @@ -1,20 +1,5 @@ import * as options from '../../../src/v2/options'; -export const BASIC_OPTIONS: options.EventHandlerOptions = { - labels: { - someKey: 'someValue', - }, - region: 'us-east1', -}; - -export const BASIC_ENDPOINT = { - platform: 'gcfv2', - region: ['us-east1'], - labels: { - someKey: 'someValue', - }, -}; - export const FULL_OPTIONS: options.GlobalOptions = { region: 'us-west1', memory: '512MB', diff --git a/src/v2/core.ts b/src/v2/core.ts index 2d9bbd8ba..845cf2990 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -52,7 +52,7 @@ export interface TriggerAnnotation { * 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 CloudEventBase { +interface CloudEventBase { /** Version of the CloudEvents spec for this event. */ readonly specversion: '1.0'; diff --git a/src/v2/providers/alerts/alerts.ts b/src/v2/providers/alerts/alerts.ts index d3a52eaf1..8a8654f0e 100644 --- a/src/v2/providers/alerts/alerts.ts +++ b/src/v2/providers/alerts/alerts.ts @@ -2,13 +2,27 @@ import { ManifestEndpoint } from '../../../common/manifest'; import { CloudEvent, CloudFunction } from '../../core'; import * as options from '../../options'; -/** Data */ +/** + * The data object that is emitted from Firebase Alerts inside the CloudEvent + */ export interface FirebaseAlertData { 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 = CloudEvent< + FirebaseAlertData, + WithAlertTypeAndApp +>; + /** @internal */ export const eventType = 'firebase.firebasealerts.alerts.v1.published'; @@ -25,24 +39,18 @@ export type AlertType = | 'appDistribution.newTesterIosDevice' | string; -/** Options */ +/** + * Configuration for Firebase Alert functions + */ export interface FirebaseAlertOptions extends options.EventHandlerOptions { alertType: AlertType; appId?: string; } -/** Cloud Event Type */ -interface WithAlertTypeAndApp { - alertType: string; - appId?: string; -} -export type AlertEvent = CloudEvent< - FirebaseAlertData, - WithAlertTypeAndApp ->; - -/** Handlers */ -/** Handle an alert published */ +/** + * Declares a function that can handle Firebase Alerts from CloudEvents + * @param alertTypeOrOpts the alert type or Firebase Alert function configuration + */ export function onAlertPublished( alertTypeOrOpts: AlertType | FirebaseAlertOptions, handler: (event: AlertEvent) => any | Promise @@ -56,57 +64,48 @@ export function onAlertPublished( }; func.run = handler; - - // TypeScript doesn't recognize defineProperty as adding a property and complains - // that __endpoint doesn't exist. We can either cast to any and lose all type safety - // or we can just assign a meaningless value before calling defineProperty. - func.__trigger = 'silence the transpiler'; - func.__endpoint = {} as ManifestEndpoint; - defineEndpoint(func, opts, alertType, appId); + func.__endpoint = getEndpointAnnotation(opts, alertType, appId); return func; } /** * @internal - * Helper function for defining an endpoint for alert handling functions + * Helper function for getting the endpoint annotation used in alert handling functions */ -export function defineEndpoint( - func: CloudFunction>, +export function getEndpointAnnotation( opts: options.EventHandlerOptions, alertType: string, appId?: string -): void { - Object.defineProperty(func, '__endpoint', { - get: () => { - 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, - }, - }; - if (appId) { - endpoint.eventTrigger.eventFilters.appId = appId; - } - return endpoint; +): 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, }, - }); + }; + if (appId) { + endpoint.eventTrigger.eventFilters.appId = appId; + } + return endpoint; } -/** @internal */ +/** + * @internal + * Helper function to parse the function opts, alert type, and appId + */ export function getOptsAndAlertTypeAndApp( alertTypeOrOpts: AlertType | FirebaseAlertOptions ): [options.EventHandlerOptions, string, string | undefined] { From ec42a8764f1f56b29a43ae2a9c98743da0565435 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Mon, 24 Jan 2022 14:25:07 -0800 Subject: [PATCH 07/12] added param comment --- src/v2/providers/alerts/alerts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/v2/providers/alerts/alerts.ts b/src/v2/providers/alerts/alerts.ts index 8a8654f0e..deb5f03f1 100644 --- a/src/v2/providers/alerts/alerts.ts +++ b/src/v2/providers/alerts/alerts.ts @@ -50,6 +50,7 @@ export interface FirebaseAlertOptions extends options.EventHandlerOptions { /** * 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( alertTypeOrOpts: AlertType | FirebaseAlertOptions, From 8f92e1f2ed577745885d9a35a84ee66b317a4384 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Mon, 24 Jan 2022 18:34:17 -0800 Subject: [PATCH 08/12] addressing comments --- spec/v2/providers/alerts/alerts.spec.ts | 48 ++++++------------------- src/v2/providers/alerts/alerts.ts | 18 +++++----- 2 files changed, 19 insertions(+), 47 deletions(-) diff --git a/spec/v2/providers/alerts/alerts.spec.ts b/spec/v2/providers/alerts/alerts.spec.ts index a9c87a85f..b63f567aa 100644 --- a/spec/v2/providers/alerts/alerts.spec.ts +++ b/spec/v2/providers/alerts/alerts.spec.ts @@ -1,5 +1,4 @@ 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'; @@ -7,13 +6,6 @@ import { FULL_ENDPOINT, FULL_OPTIONS } from '../helpers'; const ALERT_TYPE = 'new-alert-type'; const APPID = '123456789'; -function getMockFunction(): CloudFunction> { - const func = (raw: CloudEvent) => 42; - func.run = (event: CloudEvent>) => 42; - func.__endpoint = {}; - return func; -} - describe('alerts', () => { describe('onAlertPublished', () => { it('should create the function without opts', () => { @@ -75,11 +67,7 @@ describe('alerts', () => { }); it('should define the endpoint without appId and opts', () => { - const func = getMockFunction(); - - func.__endpoint = alerts.getEndpointAnnotation({}, ALERT_TYPE); - - expect(func.__endpoint).to.deep.equal({ + expect(alerts.getEndpointAnnotation({}, ALERT_TYPE)).to.deep.equal({ platform: 'gcfv2', labels: {}, eventTrigger: { @@ -93,14 +81,9 @@ describe('alerts', () => { }); 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({ + expect( + alerts.getEndpointAnnotation({ ...FULL_OPTIONS }, ALERT_TYPE) + ).to.deep.equal({ ...FULL_ENDPOINT, eventTrigger: { eventType: alerts.eventType, @@ -113,15 +96,9 @@ describe('alerts', () => { }); it('should define a complex endpoint', () => { - const func = getMockFunction(); - - func.__endpoint = alerts.getEndpointAnnotation( - { ...FULL_OPTIONS }, - ALERT_TYPE, - APPID - ); - - expect(func.__endpoint).to.deep.equal({ + expect( + alerts.getEndpointAnnotation({ ...FULL_OPTIONS }, ALERT_TYPE, APPID) + ).to.deep.equal({ ...FULL_ENDPOINT, eventTrigger: { eventType: alerts.eventType, @@ -144,15 +121,10 @@ describe('alerts', () => { region: 'us-west1', minInstances: 3, }; - const func = getMockFunction(); - - func.__endpoint = alerts.getEndpointAnnotation( - specificOpts, - ALERT_TYPE, - APPID - ); - expect(func.__endpoint).to.deep.equal({ + expect( + alerts.getEndpointAnnotation(specificOpts, ALERT_TYPE, APPID) + ).to.deep.equal({ platform: 'gcfv2', labels: {}, concurrency: 20, diff --git a/src/v2/providers/alerts/alerts.ts b/src/v2/providers/alerts/alerts.ts index deb5f03f1..41af22dd7 100644 --- a/src/v2/providers/alerts/alerts.ts +++ b/src/v2/providers/alerts/alerts.ts @@ -3,7 +3,7 @@ import { CloudEvent, CloudFunction } from '../../core'; import * as options from '../../options'; /** - * The data object that is emitted from Firebase Alerts inside the CloudEvent + * The CloudEvent data emitted by Firebase Alerts. */ export interface FirebaseAlertData { createTime: string; @@ -16,7 +16,7 @@ interface WithAlertTypeAndApp { appId?: string; } /** - * A custom CloudEvent for Firebase Alerts with custom extension attributes defined + * A custom CloudEvent for Firebase Alerts (with custom extension attributes). */ export type AlertEvent = CloudEvent< FirebaseAlertData, @@ -26,7 +26,7 @@ export type AlertEvent = CloudEvent< /** @internal */ export const eventType = 'firebase.firebasealerts.alerts.v1.published'; -/** The underlying alert type of the Firebase Alerts provider */ +/** The underlying alert type of the Firebase Alerts provider. */ export type AlertType = | 'crashlytics.newFatalIssue' | 'crashlytics.newNonfatalIssue' @@ -40,7 +40,7 @@ export type AlertType = | string; /** - * Configuration for Firebase Alert functions + * Configuration for Firebase Alert functions. */ export interface FirebaseAlertOptions extends options.EventHandlerOptions { alertType: AlertType; @@ -48,9 +48,9 @@ export interface FirebaseAlertOptions extends options.EventHandlerOptions { } /** - * 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 + * 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( alertTypeOrOpts: AlertType | FirebaseAlertOptions, @@ -72,7 +72,7 @@ export function onAlertPublished( /** * @internal - * Helper function for getting the endpoint annotation used in alert handling functions + * Helper function for getting the endpoint annotation used in alert handling functions. */ export function getEndpointAnnotation( opts: options.EventHandlerOptions, @@ -105,7 +105,7 @@ export function getEndpointAnnotation( /** * @internal - * Helper function to parse the function opts, alert type, and appId + * Helper function to parse the function opts, alert type, and appId. */ export function getOptsAndAlertTypeAndApp( alertTypeOrOpts: AlertType | FirebaseAlertOptions From 01de80b68b1b9531fd32e8694ced7a105b47bee9 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Thu, 27 Jan 2022 09:29:53 -0800 Subject: [PATCH 09/12] Specific interface for provider enhancement (1/3) (#1021) * adding in app distro changes * removing comments & addings package refs * jsdoc comments * fix comments * adding periods to doc strings * fix wording --- CHANGELOG.md | 1 + package.json | 6 +- .../providers/alerts/appDistribution.spec.ts | 127 ++++++++++++++++++ src/v2/providers/alerts/appDistribution.ts | 107 +++++++++++++++ v2/alerts/appDistribution.js | 26 ++++ 5 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 spec/v2/providers/alerts/appDistribution.spec.ts create mode 100644 src/v2/providers/alerts/appDistribution.ts create mode 100644 v2/alerts/appDistribution.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d6230214..7a80dea4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Parallelizes network calls that occur when validating authorization for onCall handlers. - Adds new regions to V2 API +- Adds new provider for alerts diff --git a/package.json b/package.json index d19a8c5cf..2add3d3d2 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "./v2/params": "./lib/v2/params/index.js", "./v2/pubsub": "./lib/v2/providers/pubsub.js", "./v2/storage": "./lib/v2/providers/storage.js", - "./v2/alerts": "./lib/v2/providers/alerts/index.js" + "./v2/alerts": "./lib/v2/providers/alerts/index.js", + "./v2/alerts/appDistribution": "./lib/v2/providers/alerts/appDistribution.js" }, "typesVersions": { "*": { @@ -118,6 +119,9 @@ ], "v2/alerts": [ "lib/v2/providers/alerts" + ], + "v2/alerts/appDistribution": [ + "lib/v2/providers/alerts/appDistribution" ] } }, diff --git a/spec/v2/providers/alerts/appDistribution.spec.ts b/spec/v2/providers/alerts/appDistribution.spec.ts new file mode 100644 index 000000000..52fdfd3b9 --- /dev/null +++ b/spec/v2/providers/alerts/appDistribution.spec.ts @@ -0,0 +1,127 @@ +import { expect } from 'chai'; +import * as alerts from '../../../../src/v2/providers/alerts'; +import * as appDistribution from '../../../../src/v2/providers/alerts/appDistribution'; +import { FULL_ENDPOINT, FULL_OPTIONS } from '../helpers'; + +const APPID = '123456789'; +const myHandler = () => 42; + +describe('appDistribution', () => { + describe('onNewTesterIosDevicePublished', () => { + it('should create a function with alertType & appId', () => { + const func = appDistribution.onNewTesterIosDevicePublished( + APPID, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: appDistribution.newTesterIosDeviceAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + + it('should create a function with opts', () => { + const func = appDistribution.onNewTesterIosDevicePublished( + { ...FULL_OPTIONS }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: appDistribution.newTesterIosDeviceAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with appid in opts', () => { + const func = appDistribution.onNewTesterIosDevicePublished( + { ...FULL_OPTIONS, appId: APPID }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: appDistribution.newTesterIosDeviceAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + + it('should create a function without opts or appId', () => { + const func = appDistribution.onNewTesterIosDevicePublished(myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: appDistribution.newTesterIosDeviceAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with a run method', () => { + const func = appDistribution.onNewTesterIosDevicePublished( + APPID, + (event) => event + ); + + const res = func.run('input' as any); + + expect(res).to.equal('input'); + }); + }); + + describe('getOptsAndApp', () => { + it('should parse a string', () => { + const [opts, appId] = appDistribution.getOptsAndApp(APPID); + + expect(opts).to.deep.equal({}); + expect(appId).to.equal(APPID); + }); + + it('should parse an options object without appId', () => { + const myOpts: appDistribution.AppDistributionOptions = { + region: 'us-west1', + }; + + const [opts, appId] = appDistribution.getOptsAndApp(myOpts); + + expect(opts).to.deep.equal({ region: 'us-west1' }); + expect(appId).to.be.undefined; + }); + + it('should parse an options object with appId', () => { + const myOpts: appDistribution.AppDistributionOptions = { + appId: APPID, + region: 'us-west1', + }; + + const [opts, appId] = appDistribution.getOptsAndApp(myOpts); + + expect(opts).to.deep.equal({ region: 'us-west1' }); + expect(appId).to.equal(APPID); + }); + }); +}); diff --git a/src/v2/providers/alerts/appDistribution.ts b/src/v2/providers/alerts/appDistribution.ts new file mode 100644 index 000000000..58b54e9b6 --- /dev/null +++ b/src/v2/providers/alerts/appDistribution.ts @@ -0,0 +1,107 @@ +import { getEndpointAnnotation, FirebaseAlertData } from './alerts'; +import { CloudEvent, CloudFunction } from '../../core'; +import * as options from '../../options'; + +/** + * The internal payload object for adding a new tester device to app distribution. + * Payload is wrapped inside a FirebaseAlertData object. + */ +export interface NewTesterDevicePayload { + ['@type']: 'com.google.firebase.firebasealerts.NewTesterDevicePayload'; + testerName: string; + testerEmail: string; + testerDeviceModelName: string; + testerDeviceIdentifier: string; +} + +interface WithAlertTypeAndApp { + alertType: string; + appId: string; +} +/** + * A custom CloudEvent for Firebase Alerts (with custom extension attributes). + */ +export type AppDistributionEvent = CloudEvent< + FirebaseAlertData, + WithAlertTypeAndApp +>; + +/** @internal */ +export const newTesterIosDeviceAlert = 'appDistribution.newTesterIosDevice'; + +/** + * Configuration for app distribution functions. + */ +export interface AppDistributionOptions extends options.EventHandlerOptions { + appId?: string; +} + +/** + * Declares a function that can handle adding a new tester iOS device. + */ +export function onNewTesterIosDevicePublished( + handler: ( + event: AppDistributionEvent + ) => any | Promise +): CloudFunction>; +export function onNewTesterIosDevicePublished( + appId: string, + handler: ( + event: AppDistributionEvent + ) => any | Promise +): CloudFunction>; +export function onNewTesterIosDevicePublished( + opts: AppDistributionOptions, + handler: ( + event: AppDistributionEvent + ) => any | Promise +): CloudFunction>; +export function onNewTesterIosDevicePublished( + appIdOrOptsOrHandler: + | string + | AppDistributionOptions + | (( + event: AppDistributionEvent + ) => any | Promise), + handler?: ( + event: AppDistributionEvent + ) => any | Promise +): CloudFunction> { + if (typeof appIdOrOptsOrHandler === 'function') { + handler = appIdOrOptsOrHandler as ( + event: AppDistributionEvent + ) => any | Promise; + appIdOrOptsOrHandler = {}; + } + + const [opts, appId] = getOptsAndApp(appIdOrOptsOrHandler); + + const func = (raw: CloudEvent) => { + return handler(raw as AppDistributionEvent); + }; + + func.run = handler; + func.__endpoint = getEndpointAnnotation(opts, newTesterIosDeviceAlert, appId); + + return func; +} + +/** + * @internal + * Helper function to parse the function opts and appId. + */ +export function getOptsAndApp( + appIdOrOpts: string | AppDistributionOptions +): [options.EventHandlerOptions, string | undefined] { + let opts: options.EventHandlerOptions; + let appId: string | undefined; + if (typeof appIdOrOpts === 'string') { + opts = {}; + appId = appIdOrOpts; + } else { + appId = appIdOrOpts.appId; + opts = { ...appIdOrOpts }; + delete (opts as any).appId; + } + return [opts, appId]; +} diff --git a/v2/alerts/appDistribution.js b/v2/alerts/appDistribution.js new file mode 100644 index 000000000..7d725acc3 --- /dev/null +++ b/v2/alerts/appDistribution.js @@ -0,0 +1,26 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// This file is not part of the firebase-functions SDK. It is used to silence the +// imports eslint plugin until it can understand import paths defined by node +// package exports. +// For more information, see github.com/import-js/eslint-plugin-import/issues/1810 From c633bd9fd9ae19bdc37da7373b32d92989bad7bd Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Thu, 27 Jan 2022 14:22:48 -0800 Subject: [PATCH 10/12] adding import/export statement --- src/v2/providers/alerts/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/v2/providers/alerts/index.ts b/src/v2/providers/alerts/index.ts index c40670cd1..44ae0b5de 100644 --- a/src/v2/providers/alerts/index.ts +++ b/src/v2/providers/alerts/index.ts @@ -1 +1,4 @@ +import * as appDistribution from './appDistribution'; + +export { appDistribution }; export * from './alerts'; From b51ec902c00a894fb0115a52512bbab6f407e66e Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Thu, 27 Jan 2022 15:04:27 -0800 Subject: [PATCH 11/12] Specific interface for provider enhancement (2/3) (#1022) * adding in billing changes * removing comments & adding package refs * jsdoc comments * addressing pr comments * change handler doc string * changing to BillingEventHandler type * remove BillingEventHandler type --- package.json | 6 +- spec/v2/providers/alerts/billing.spec.ts | 126 +++++++++++++++++++++++ src/v2/providers/alerts/billing.ts | 110 ++++++++++++++++++++ src/v2/providers/alerts/index.ts | 3 +- v2/alerts/billing.js | 26 +++++ 5 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 spec/v2/providers/alerts/billing.spec.ts create mode 100644 src/v2/providers/alerts/billing.ts create mode 100644 v2/alerts/billing.js diff --git a/package.json b/package.json index 2add3d3d2..d30964d69 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ "./v2/pubsub": "./lib/v2/providers/pubsub.js", "./v2/storage": "./lib/v2/providers/storage.js", "./v2/alerts": "./lib/v2/providers/alerts/index.js", - "./v2/alerts/appDistribution": "./lib/v2/providers/alerts/appDistribution.js" + "./v2/alerts/appDistribution": "./lib/v2/providers/alerts/appDistribution.js", + "./v2/alerts/billing": "./lib/v2/providers/alerts/billing.js" }, "typesVersions": { "*": { @@ -122,6 +123,9 @@ ], "v2/alerts/appDistribution": [ "lib/v2/providers/alerts/appDistribution" + ], + "v2/alerts/billing": [ + "lib/v2/providers/alerts/billing" ] } }, diff --git a/spec/v2/providers/alerts/billing.spec.ts b/spec/v2/providers/alerts/billing.spec.ts new file mode 100644 index 000000000..a9e4e173d --- /dev/null +++ b/spec/v2/providers/alerts/billing.spec.ts @@ -0,0 +1,126 @@ +import { expect } from 'chai'; +import * as alerts from '../../../../src/v2/providers/alerts'; +import * as billing from '../../../../src/v2/providers/alerts/billing'; +import { FULL_ENDPOINT, FULL_OPTIONS } from '../helpers'; + +const ALERT_TYPE = 'new-alert-type'; +const myHandler = () => 42; + +describe('billing', () => { + describe('onPlanUpdatePublished', () => { + it('should create a function with only handler', () => { + const func = billing.onPlanUpdatePublished(myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: billing.planUpdateAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with opts & handler', () => { + const func = billing.onPlanUpdatePublished( + { ...FULL_OPTIONS }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: billing.planUpdateAlert, + }, + retry: false, + }, + }); + }); + }); + + describe('onAutomatedPlanUpdatePublished', () => { + it('should create a function with only handler', () => { + const func = billing.onAutomatedPlanUpdatePublished(myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: billing.automatedPlanUpdateAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with opts & handler', () => { + const func = billing.onAutomatedPlanUpdatePublished( + { ...FULL_OPTIONS }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: billing.automatedPlanUpdateAlert, + }, + retry: false, + }, + }); + }); + }); + + describe('onOperation', () => { + it('should create a function with alertType only', () => { + const func = billing.onOperation(ALERT_TYPE, myHandler, undefined); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: ALERT_TYPE, + }, + retry: false, + }, + }); + }); + + it('should create a function with opts', () => { + const func = billing.onOperation( + ALERT_TYPE, + { ...FULL_OPTIONS }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: ALERT_TYPE, + }, + retry: false, + }, + }); + }); + + it('should create a function with a run method', () => { + const func = billing.onOperation(ALERT_TYPE, (event) => event, undefined); + + const res = func.run('input' as any); + + expect(res).to.equal('input'); + }); + }); +}); diff --git a/src/v2/providers/alerts/billing.ts b/src/v2/providers/alerts/billing.ts new file mode 100644 index 000000000..f2ecd4289 --- /dev/null +++ b/src/v2/providers/alerts/billing.ts @@ -0,0 +1,110 @@ +import { getEndpointAnnotation, FirebaseAlertData } from '.'; +import { CloudEvent, CloudFunction } from '../../core'; +import * as options from '../../options'; + +/** + * The internal payload object for billing plan updates. + * Payload is wrapped inside a FirebaseAlertData object. + */ +export interface PlanUpdatePayload { + ['@type']: 'com.google.firebase.firebasealerts.PlanUpdatePayload'; + billingPlan: string; + principalEmail: string; +} + +/** + * The internal payload object for billing plan automated updates. + * Payload is wrapped inside a FirebaseAlertData object. + */ +export interface PlanAutomatedUpdatePayload { + ['@type']: 'com.google.firebase.firebasealerts.PlanAutomatedUpdatePayload'; + billingPlan: string; +} + +interface WithAlertType { + alertType: string; +} +/** + * A custom CloudEvent for billing Firebase Alerts (with custom extension attributes). + */ +export type BillingEvent = CloudEvent, WithAlertType>; + +/** @internal */ +export const planUpdateAlert = 'billing.planUpdate'; +/** @internal */ +export const automatedPlanUpdateAlert = 'billing.automatedPlanUpdate'; + +/** + * Declares a function that can handle a billing plan update event. + */ +export function onPlanUpdatePublished( + handler: (event: BillingEvent) => any | Promise +): CloudFunction>; +export function onPlanUpdatePublished( + opts: options.EventHandlerOptions, + handler: (event: BillingEvent) => any | Promise +): CloudFunction>; +export function onPlanUpdatePublished( + optsOrHandler: + | options.EventHandlerOptions + | ((event: BillingEvent) => any | Promise), + handler?: (event: BillingEvent) => any | Promise +): CloudFunction> { + return onOperation( + planUpdateAlert, + optsOrHandler, + handler + ); +} + +/** + * Declares a function that can handle an automated billing plan update event. + */ +export function onAutomatedPlanUpdatePublished( + handler: ( + event: BillingEvent + ) => any | Promise +): CloudFunction>; +export function onAutomatedPlanUpdatePublished( + opts: options.EventHandlerOptions, + handler: ( + event: BillingEvent + ) => any | Promise +): CloudFunction>; +export function onAutomatedPlanUpdatePublished( + optsOrHandler: + | options.EventHandlerOptions + | ((event: BillingEvent) => any | Promise), + handler?: ( + event: BillingEvent + ) => any | Promise +): CloudFunction> { + return onOperation( + automatedPlanUpdateAlert, + optsOrHandler, + handler + ); +} + +/** @internal */ +export function onOperation( + alertType: string, + optsOrHandler: + | options.EventHandlerOptions + | ((event: BillingEvent) => any | Promise), + handler: (event: BillingEvent) => any | Promise +): CloudFunction> { + if (typeof optsOrHandler === 'function') { + handler = optsOrHandler as (event: BillingEvent) => any | Promise; + optsOrHandler = {}; + } + + const func = (raw: CloudEvent) => { + return handler(raw as BillingEvent); + }; + + func.run = handler; + func.__endpoint = getEndpointAnnotation(optsOrHandler, alertType); + + return func; +} diff --git a/src/v2/providers/alerts/index.ts b/src/v2/providers/alerts/index.ts index 44ae0b5de..8290c38fb 100644 --- a/src/v2/providers/alerts/index.ts +++ b/src/v2/providers/alerts/index.ts @@ -1,4 +1,5 @@ import * as appDistribution from './appDistribution'; +import * as billing from './billing'; -export { appDistribution }; +export { appDistribution, billing }; export * from './alerts'; diff --git a/v2/alerts/billing.js b/v2/alerts/billing.js new file mode 100644 index 000000000..7d725acc3 --- /dev/null +++ b/v2/alerts/billing.js @@ -0,0 +1,26 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// This file is not part of the firebase-functions SDK. It is used to silence the +// imports eslint plugin until it can understand import paths defined by node +// package exports. +// For more information, see github.com/import-js/eslint-plugin-import/issues/1810 From 459838334ff2f2b63b4bcf8abe5e8d6feff5de05 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Thu, 27 Jan 2022 15:43:12 -0800 Subject: [PATCH 12/12] Specific interface for provider enhancement (3/3) (#1023) * adding in crashlytics changes * comments & adding package refs * address comments and make doc strings better * add opts.retry to event trigger --- package.json | 6 +- spec/v2/providers/alerts/crashlytics.spec.ts | 562 +++++++++++++++++++ src/v2/providers/alerts/alerts.ts | 2 +- src/v2/providers/alerts/crashlytics.ts | 360 ++++++++++++ src/v2/providers/alerts/index.ts | 3 +- v2/alerts/crashlytics.js | 26 + 6 files changed, 956 insertions(+), 3 deletions(-) create mode 100644 spec/v2/providers/alerts/crashlytics.spec.ts create mode 100644 src/v2/providers/alerts/crashlytics.ts create mode 100644 v2/alerts/crashlytics.js diff --git a/package.json b/package.json index d30964d69..0be8d9ef9 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,8 @@ "./v2/storage": "./lib/v2/providers/storage.js", "./v2/alerts": "./lib/v2/providers/alerts/index.js", "./v2/alerts/appDistribution": "./lib/v2/providers/alerts/appDistribution.js", - "./v2/alerts/billing": "./lib/v2/providers/alerts/billing.js" + "./v2/alerts/billing": "./lib/v2/providers/alerts/billing.js", + "./v2/alerts/crashlytics": "./lib/v2/providers/alerts/crashlytics.js" }, "typesVersions": { "*": { @@ -126,6 +127,9 @@ ], "v2/alerts/billing": [ "lib/v2/providers/alerts/billing" + ], + "v2/alerts/crashlytics": [ + "lib/v2/providers/alerts/crashlytics" ] } }, diff --git a/spec/v2/providers/alerts/crashlytics.spec.ts b/spec/v2/providers/alerts/crashlytics.spec.ts new file mode 100644 index 000000000..1d5c1a8b6 --- /dev/null +++ b/spec/v2/providers/alerts/crashlytics.spec.ts @@ -0,0 +1,562 @@ +import { expect } from 'chai'; +import * as alerts from '../../../../src/v2/providers/alerts'; +import * as crashlytics from '../../../../src/v2/providers/alerts/crashlytics'; +import { FULL_ENDPOINT, FULL_OPTIONS } from '../helpers'; + +const ALERT_TYPE = 'new-alert-type'; +const APPID = '123456789'; +const myHandler = () => 42; + +describe('crashlytics', () => { + describe('onNewFatalIssuePublished', () => { + it('should create a function only handler', () => { + const func = crashlytics.onNewFatalIssuePublished(myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newFatalIssueAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with appId', () => { + const func = crashlytics.onNewFatalIssuePublished(APPID, myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newFatalIssueAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + + it('should create a function with base opts', () => { + const func = crashlytics.onNewFatalIssuePublished( + { ...FULL_OPTIONS }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newFatalIssueAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with opts', () => { + const func = crashlytics.onNewFatalIssuePublished( + { ...FULL_OPTIONS, appId: APPID }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newFatalIssueAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + }); + + describe('onNewNonfatalIssuePublished', () => { + it('should create a function only handler', () => { + const func = crashlytics.onNewNonfatalIssuePublished(myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newNonfatalIssueAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with appId', () => { + const func = crashlytics.onNewNonfatalIssuePublished(APPID, myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newNonfatalIssueAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + + it('should create a function with base opts', () => { + const func = crashlytics.onNewNonfatalIssuePublished( + { ...FULL_OPTIONS }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newNonfatalIssueAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with opts', () => { + const func = crashlytics.onNewNonfatalIssuePublished( + { ...FULL_OPTIONS, appId: APPID }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newNonfatalIssueAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + }); + + describe('onRegressionAlertPublished', () => { + it('should create a function only handler', () => { + const func = crashlytics.onRegressionAlertPublished(myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.regressionAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with appId', () => { + const func = crashlytics.onRegressionAlertPublished(APPID, myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.regressionAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + + it('should create a function with base opts', () => { + const func = crashlytics.onRegressionAlertPublished( + { ...FULL_OPTIONS }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.regressionAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with opts', () => { + const func = crashlytics.onRegressionAlertPublished( + { ...FULL_OPTIONS, appId: APPID }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.regressionAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + }); + + describe('onStabilityDigestPublished', () => { + it('should create a function only handler', () => { + const func = crashlytics.onStabilityDigestPublished(myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.stabilityDigestAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with appId', () => { + const func = crashlytics.onStabilityDigestPublished(APPID, myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.stabilityDigestAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + + it('should create a function with base opts', () => { + const func = crashlytics.onStabilityDigestPublished( + { ...FULL_OPTIONS }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.stabilityDigestAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with opts', () => { + const func = crashlytics.onStabilityDigestPublished( + { ...FULL_OPTIONS, appId: APPID }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.stabilityDigestAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + }); + + describe('onVelocityAlertPublished', () => { + it('should create a function only handler', () => { + const func = crashlytics.onVelocityAlertPublished(myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.velocityAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with appId', () => { + const func = crashlytics.onVelocityAlertPublished(APPID, myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.velocityAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + + it('should create a function with base opts', () => { + const func = crashlytics.onVelocityAlertPublished( + { ...FULL_OPTIONS }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.velocityAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with opts', () => { + const func = crashlytics.onVelocityAlertPublished( + { ...FULL_OPTIONS, appId: APPID }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.velocityAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + }); + + describe('onNewAnrIssuePublished', () => { + it('should create a function only handler', () => { + const func = crashlytics.onNewAnrIssuePublished(myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newAnrIssueAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with appId', () => { + const func = crashlytics.onNewAnrIssuePublished(APPID, myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newAnrIssueAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + + it('should create a function with base opts', () => { + const func = crashlytics.onNewAnrIssuePublished( + { ...FULL_OPTIONS }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newAnrIssueAlert, + }, + retry: false, + }, + }); + }); + + it('should create a function with opts', () => { + const func = crashlytics.onNewAnrIssuePublished( + { ...FULL_OPTIONS, appId: APPID }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: crashlytics.newAnrIssueAlert, + appId: APPID, + }, + retry: false, + }, + }); + }); + }); + + describe('onOperation', () => { + it('should create a function with alertType only', () => { + const func = crashlytics.onOperation(ALERT_TYPE, myHandler, undefined); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: ALERT_TYPE, + }, + retry: false, + }, + }); + }); + + it('should create a function with alertType & appId', () => { + const func = crashlytics.onOperation(ALERT_TYPE, APPID, myHandler); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: ALERT_TYPE, + appId: APPID, + }, + retry: false, + }, + }); + }); + + it('should create a function with base opts', () => { + const func = crashlytics.onOperation( + ALERT_TYPE, + { ...FULL_OPTIONS }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: ALERT_TYPE, + }, + retry: false, + }, + }); + }); + + it('should create a function with appid in opts', () => { + const func = crashlytics.onOperation( + ALERT_TYPE, + { ...FULL_OPTIONS, appId: APPID }, + myHandler + ); + + expect(func.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: { + eventType: alerts.eventType, + eventFilters: { + alertType: ALERT_TYPE, + appId: APPID, + }, + retry: false, + }, + }); + }); + + it('should create a function with a run method', () => { + const func = crashlytics.onOperation( + ALERT_TYPE, + (event) => event, + undefined + ); + + const res = func.run('input' as any); + + expect(res).to.equal('input'); + }); + }); + + describe('getOptsAndApp', () => { + it('should parse a string', () => { + const APPID = '123456789'; + + const [opts, appId] = crashlytics.getOptsAndApp(APPID); + + expect(opts).to.deep.equal({}); + expect(appId).to.equal(APPID); + }); + + it('should parse an options object without appId', () => { + const myOpts: crashlytics.CrashlyticsOptions = { + region: 'us-west1', + }; + + const [opts, appId] = crashlytics.getOptsAndApp(myOpts); + + expect(opts).to.deep.equal({ region: 'us-west1' }); + expect(appId).to.be.undefined; + }); + + it('should parse an options object with appId', () => { + const myOpts: crashlytics.CrashlyticsOptions = { + appId: '123456789', + region: 'us-west1', + }; + + const [opts, appId] = crashlytics.getOptsAndApp(myOpts); + + expect(opts).to.deep.equal({ region: 'us-west1' }); + expect(appId).to.equal(myOpts.appId); + }); + }); +}); diff --git a/src/v2/providers/alerts/alerts.ts b/src/v2/providers/alerts/alerts.ts index 41af22dd7..4b7f0b952 100644 --- a/src/v2/providers/alerts/alerts.ts +++ b/src/v2/providers/alerts/alerts.ts @@ -94,7 +94,7 @@ export function getEndpointAnnotation( eventFilters: { alertType, }, - retry: false, + retry: !!opts.retry, }, }; if (appId) { diff --git a/src/v2/providers/alerts/crashlytics.ts b/src/v2/providers/alerts/crashlytics.ts new file mode 100644 index 000000000..d9cece913 --- /dev/null +++ b/src/v2/providers/alerts/crashlytics.ts @@ -0,0 +1,360 @@ +import { getEndpointAnnotation, FirebaseAlertData } from '.'; +import { CloudEvent, CloudFunction } from '../../core'; +import * as options from '../../options'; + +/** Generic crashlytics issue interface */ +interface Issue { + id: string; + title: string; + subtitle: string; + appVersion: string; +} + +/** + * The internal payload object for a new fatal issue. + * Payload is wrapped inside a FirebaseAlertData object. + */ +export interface NewFatalIssuePayload { + ['@type']: 'com.google.firebase.firebasealerts.CrashlyticsNewFatalIssuePayload'; + issue: Issue; +} + +/** + * The internal payload object for a new non-fatal issue. + * Payload is wrapped inside a FirebaseAlertData object. + */ +export interface NewNonfatalIssuePayload { + ['@type']: 'com.google.firebase.firebasealerts.CrashlyticsNewNonfatalIssuePayload'; + issue: Issue; +} + +/** + * The internal payload object for a regression alert. + * Payload is wrapped inside a FirebaseAlertData object. + */ +export interface RegressionAlertPayload { + ['@type']: 'com.google.firebase.firebasealerts.CrashlyticsRegressionAlertPayload'; + type: string; + issue: Issue; + resolveTime: string; +} + +/** Generic crashlytics trending issue interface */ +interface TrendingIssueDetails { + type: string; + issue: Issue; + eventCount: number; + userCount: number; +} + +/** + * The internal payload object for a stability digest. + * Payload is wrapped inside a FirebaseAlertData object. + */ +export interface StabilityDigestPayload { + ['@type']: 'com.google.firebase.firebasealerts.CrashlyticsStabilityDigestPayload'; + digestDate: string; + trendingIssues: TrendingIssueDetails[]; +} + +/** + * The internal payload object for a velocity alert. + * Payload is wrapped inside a FirebaseAlertData object. + */ +export interface VelocityAlertPayload { + ['@type']: 'com.google.firebase.firebasealerts.VelocityAlertPayload'; + issue: Issue; + createTime: string; + crashCount: number; + crashPercentage: number; + firstVersion: string; +} + +/** + * The internal payload object for a new Application Not Responding issue. + * Payload is wrapped inside a FirebaseAlertData object. + */ +export interface NewAnrIssuePayload { + ['@type']: 'com.google.firebase.firebasealerts.NewAnrIssuePayload'; + issue: Issue; +} + +interface WithAlertTypeAndApp { + alertType: string; + appId: string; +} +/** + * A custom CloudEvent for Firebase Alerts (with custom extension attributes). + */ +export type CrashlyticsEvent = CloudEvent< + FirebaseAlertData, + WithAlertTypeAndApp +>; + +/** @internal */ +export const newFatalIssueAlert = 'crashlytics.newFatalIssue'; +/** @internal */ +export const newNonfatalIssueAlert = 'crashlytics.newNonfatalIssue'; +/** @internal */ +export const regressionAlert = 'crashlytics.regression'; +/** @internal */ +export const stabilityDigestAlert = 'crashlytics.stabilityDigest'; +/** @internal */ +export const velocityAlert = 'crashlytics.velocity'; +/** @internal */ +export const newAnrIssueAlert = 'crashlytics.newAnrIssue'; + +/** + * Configuration for crashlytics functions. + */ +export interface CrashlyticsOptions extends options.EventHandlerOptions { + appId?: string; +} + +/** + * Declares a function that can handle a new fatal issue published to crashlytics. + */ +export function onNewFatalIssuePublished( + handler: (event: CrashlyticsEvent) => any | Promise +): CloudFunction>; +export function onNewFatalIssuePublished( + appId: string, + handler: (event: CrashlyticsEvent) => any | Promise +): CloudFunction>; +export function onNewFatalIssuePublished( + opts: CrashlyticsOptions, + handler: (event: CrashlyticsEvent) => any | Promise +): CloudFunction>; +export function onNewFatalIssuePublished( + appIdOrOptsOrHandler: + | string + | CrashlyticsOptions + | ((event: CrashlyticsEvent) => any | Promise), + handler?: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction> { + return onOperation( + newFatalIssueAlert, + appIdOrOptsOrHandler, + handler + ); +} + +/** + * Declares a function that can handle aa new non-fatal issue published to crashlytics. + */ +export function onNewNonfatalIssuePublished( + handler: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction>; +export function onNewNonfatalIssuePublished( + appId: string, + handler: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction>; +export function onNewNonfatalIssuePublished( + opts: CrashlyticsOptions, + handler: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction>; +export function onNewNonfatalIssuePublished( + appIdOrOptsOrHandler: + | string + | CrashlyticsOptions + | (( + event: CrashlyticsEvent + ) => any | Promise), + handler?: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction> { + return onOperation( + newNonfatalIssueAlert, + appIdOrOptsOrHandler, + handler + ); +} + +/** + * Declares a function that can handle a regression alert published to crashlytics. + */ +export function onRegressionAlertPublished( + handler: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction>; +export function onRegressionAlertPublished( + appId: string, + handler: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction>; +export function onRegressionAlertPublished( + opts: CrashlyticsOptions, + handler: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction>; +export function onRegressionAlertPublished( + appIdOrOptsOrHandler: + | string + | CrashlyticsOptions + | ((event: CrashlyticsEvent) => any | Promise), + handler?: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction> { + return onOperation( + regressionAlert, + appIdOrOptsOrHandler, + handler + ); +} + +/** + * Declares a function that can handle a stability digest published to crashlytics. + */ +export function onStabilityDigestPublished( + handler: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction>; +export function onStabilityDigestPublished( + appId: string, + handler: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction>; +export function onStabilityDigestPublished( + opts: CrashlyticsOptions, + handler: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction>; +export function onStabilityDigestPublished( + appIdOrOptsOrHandler: + | string + | CrashlyticsOptions + | ((event: CrashlyticsEvent) => any | Promise), + handler?: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction> { + return onOperation( + stabilityDigestAlert, + appIdOrOptsOrHandler, + handler + ); +} + +/** + * Declares a function that can handle a velocity alert published to crashlytics. + */ +export function onVelocityAlertPublished( + handler: (event: CrashlyticsEvent) => any | Promise +): CloudFunction>; +export function onVelocityAlertPublished( + appId: string, + handler: (event: CrashlyticsEvent) => any | Promise +): CloudFunction>; +export function onVelocityAlertPublished( + opts: CrashlyticsOptions, + handler: (event: CrashlyticsEvent) => any | Promise +): CloudFunction>; +export function onVelocityAlertPublished( + appIdOrOptsOrHandler: + | string + | CrashlyticsOptions + | ((event: CrashlyticsEvent) => any | Promise), + handler?: ( + event: CrashlyticsEvent + ) => any | Promise +): CloudFunction> { + return onOperation( + velocityAlert, + appIdOrOptsOrHandler, + handler + ); +} + +/** + * Declares a function that can handle a new Application Not Responding issue published to crashlytics. + */ +export function onNewAnrIssuePublished( + handler: (event: CrashlyticsEvent) => any | Promise +): CloudFunction>; +export function onNewAnrIssuePublished( + appId: string, + handler: (event: CrashlyticsEvent) => any | Promise +): CloudFunction>; +export function onNewAnrIssuePublished( + opts: CrashlyticsOptions, + handler: (event: CrashlyticsEvent) => any | Promise +): CloudFunction>; +export function onNewAnrIssuePublished( + appIdOrOptsOrHandler: + | string + | CrashlyticsOptions + | ((event: CrashlyticsEvent) => any | Promise), + handler?: (event: CrashlyticsEvent) => any | Promise +): CloudFunction> { + return onOperation( + newAnrIssueAlert, + appIdOrOptsOrHandler, + handler + ); +} + +/** @internal */ +export function onOperation( + alertType: string, + appIdOrOptsOrHandler: + | string + | CrashlyticsOptions + | ((event: CrashlyticsEvent) => any | Promise), + handler: (event: CrashlyticsEvent) => any | Promise +): CloudFunction> { + if (typeof appIdOrOptsOrHandler === 'function') { + handler = appIdOrOptsOrHandler as ( + event: CrashlyticsEvent + ) => any | Promise; + appIdOrOptsOrHandler = {}; + } + + const [opts, appId] = getOptsAndApp( + appIdOrOptsOrHandler as string | CrashlyticsOptions + ); + + const func = (raw: CloudEvent) => { + return handler(raw as CrashlyticsEvent); + }; + + func.run = handler; + func.__endpoint = getEndpointAnnotation(opts, alertType, appId); + + return func; +} + +/** + * @internal + * Helper function to parse the function opts and appId. + */ +export function getOptsAndApp( + appIdOrOpts: string | CrashlyticsOptions +): [options.EventHandlerOptions, string | undefined] { + let opts: options.EventHandlerOptions; + let appId: string | undefined; + if (typeof appIdOrOpts === 'string') { + opts = {}; + appId = appIdOrOpts; + } else { + appId = appIdOrOpts.appId; + opts = { ...appIdOrOpts }; + delete (opts as any).appId; + } + return [opts, appId]; +} diff --git a/src/v2/providers/alerts/index.ts b/src/v2/providers/alerts/index.ts index 8290c38fb..ecf90a422 100644 --- a/src/v2/providers/alerts/index.ts +++ b/src/v2/providers/alerts/index.ts @@ -1,5 +1,6 @@ import * as appDistribution from './appDistribution'; import * as billing from './billing'; +import * as crashlytics from './crashlytics'; -export { appDistribution, billing }; +export { appDistribution, billing, crashlytics }; export * from './alerts'; diff --git a/v2/alerts/crashlytics.js b/v2/alerts/crashlytics.js new file mode 100644 index 000000000..7d725acc3 --- /dev/null +++ b/v2/alerts/crashlytics.js @@ -0,0 +1,26 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// This file is not part of the firebase-functions SDK. It is used to silence the +// imports eslint plugin until it can understand import paths defined by node +// package exports. +// For more information, see github.com/import-js/eslint-plugin-import/issues/1810