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