From 139e1d94d585c26a5d76586685a976a5643df869 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Tue, 28 Jun 2022 00:20:36 -0400 Subject: [PATCH] Enhance RTDB v2 Triggers (#1153) * moving change into v2 core * moving Change to common and fixing imports * fix tsdoc * rexporting Change from v1&v2 * remove unused functions from Change namespace * fixing argument * rename interface to onValue* and rework Change Co-authored-by: Thomas Bouldin --- spec/common/change.spec.ts | 120 +++++++++++++++++++++++++++++ spec/v1/cloud-functions.spec.ts | 97 ----------------------- spec/v2/providers/database.spec.ts | 24 +++--- src/cloud-functions.ts | 89 --------------------- src/common/change.ts | 109 ++++++++++++++++++++++++++ src/providers/database.ts | 2 +- src/providers/firestore.ts | 2 +- src/v2/core.ts | 3 + src/v2/providers/database.ts | 120 +++++++++++++++++++++++++---- 9 files changed, 353 insertions(+), 213 deletions(-) create mode 100644 spec/common/change.spec.ts create mode 100644 src/common/change.ts diff --git a/spec/common/change.spec.ts b/spec/common/change.spec.ts new file mode 100644 index 000000000..258fc8463 --- /dev/null +++ b/spec/common/change.spec.ts @@ -0,0 +1,120 @@ +// The MIT License (MIT) +// +// Copyright (c) 2022 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. + +import { expect } from 'chai'; +import * as change from '../../src/common/change'; + +describe('Change', () => { + describe('applyFieldMask', () => { + const after = { + foo: 'bar', + num: 2, + obj: { + a: 1, + b: 2, + }, + }; + + it('should handle deleted values', () => { + const sparseBefore = { baz: 'qux' }; + const fieldMask = 'baz'; + expect( + change.applyFieldMask(sparseBefore, after, fieldMask) + ).to.deep.equal({ + foo: 'bar', + num: 2, + obj: { + a: 1, + b: 2, + }, + baz: 'qux', + }); + }); + + it('should handle created values', () => { + const sparseBefore = {}; + const fieldMask = 'num,obj.a'; + expect( + change.applyFieldMask(sparseBefore, after, fieldMask) + ).to.deep.equal({ + foo: 'bar', + obj: { + b: 2, + }, + }); + }); + + it('should handle mutated values', () => { + const sparseBefore = { + num: 3, + obj: { + a: 3, + }, + }; + const fieldMask = 'num,obj.a'; + expect( + change.applyFieldMask(sparseBefore, after, fieldMask) + ).to.deep.equal({ + foo: 'bar', + num: 3, + obj: { + a: 3, + b: 2, + }, + }); + }); + }); + + describe('fromJSON', () => { + it('should create a Change object with a `before` and `after`', () => { + const created = change.Change.fromJSON({ + before: { foo: 'bar' }, + after: { foo: 'faz' }, + }); + expect(created instanceof change.Change).to.equal(true); + expect(created.before).to.deep.equal({ foo: 'bar' }); + expect(created.after).to.deep.equal({ foo: 'faz' }); + }); + + it('should apply the customizer function to `before` and `after`', () => { + function customizer(input: any) { + input.another = 'value'; + return input as T; + } + const created = change.Change.fromJSON( + { + before: { foo: 'bar' }, + after: { foo: 'faz' }, + }, + customizer + ); + expect(created.before).to.deep.equal({ + foo: 'bar', + another: 'value', + }); + expect(created.after).to.deep.equal({ + foo: 'faz', + another: 'value', + }); + }); + }); +}); diff --git a/spec/v1/cloud-functions.spec.ts b/spec/v1/cloud-functions.spec.ts index fec1bd580..336c00282 100644 --- a/spec/v1/cloud-functions.spec.ts +++ b/spec/v1/cloud-functions.spec.ts @@ -24,7 +24,6 @@ import { expect } from 'chai'; import * as _ from 'lodash'; import { - Change, Event, EventContext, makeCloudFunction, @@ -354,99 +353,3 @@ describe('makeAuth and makeAuthType', () => { }); }); }); - -describe('Change', () => { - describe('applyFieldMask', () => { - const after = { - foo: 'bar', - num: 2, - obj: { - a: 1, - b: 2, - }, - }; - - it('should handle deleted values', () => { - const sparseBefore = { baz: 'qux' }; - const fieldMask = 'baz'; - expect( - Change.applyFieldMask(sparseBefore, after, fieldMask) - ).to.deep.equal({ - foo: 'bar', - num: 2, - obj: { - a: 1, - b: 2, - }, - baz: 'qux', - }); - }); - - it('should handle created values', () => { - const sparseBefore = {}; - const fieldMask = 'num,obj.a'; - expect( - Change.applyFieldMask(sparseBefore, after, fieldMask) - ).to.deep.equal({ - foo: 'bar', - obj: { - b: 2, - }, - }); - }); - - it('should handle mutated values', () => { - const sparseBefore = { - num: 3, - obj: { - a: 3, - }, - }; - const fieldMask = 'num,obj.a'; - expect( - Change.applyFieldMask(sparseBefore, after, fieldMask) - ).to.deep.equal({ - foo: 'bar', - num: 3, - obj: { - a: 3, - b: 2, - }, - }); - }); - }); - - describe('fromJSON', () => { - it('should create a Change object with a `before` and `after`', () => { - const created = Change.fromJSON({ - before: { foo: 'bar' }, - after: { foo: 'faz' }, - }); - expect(created instanceof Change).to.equal(true); - expect(created.before).to.deep.equal({ foo: 'bar' }); - expect(created.after).to.deep.equal({ foo: 'faz' }); - }); - - it('should apply the customizer function to `before` and `after`', () => { - function customizer(input: any) { - _.set(input, 'another', 'value'); - return input as T; - } - const created = Change.fromJSON( - { - before: { foo: 'bar' }, - after: { foo: 'faz' }, - }, - customizer - ); - expect(created.before).to.deep.equal({ - foo: 'bar', - another: 'value', - }); - expect(created.after).to.deep.equal({ - foo: 'faz', - another: 'value', - }); - }); - }); -}); diff --git a/spec/v2/providers/database.spec.ts b/spec/v2/providers/database.spec.ts index 58d4285ab..f700c58d3 100644 --- a/spec/v2/providers/database.spec.ts +++ b/spec/v2/providers/database.spec.ts @@ -363,9 +363,9 @@ describe('database', () => { }); }); - describe('onRefWritten', () => { + describe('onValueWritten', () => { it('should create a function with a reference', () => { - const func = database.onRefWritten('/foo/{bar}/', (event) => 2); + const func = database.onValueWritten('/foo/{bar}/', (event) => 2); expect(func.__endpoint).to.deep.equal({ platform: 'gcfv2', @@ -383,7 +383,7 @@ describe('database', () => { }); it('should create a function with opts', () => { - const func = database.onRefWritten( + const func = database.onValueWritten( { ref: '/foo/{path=**}/{bar}/', instance: 'my-instance', @@ -414,9 +414,9 @@ describe('database', () => { }); }); - describe('onRefCreated', () => { + describe('onValueCreated', () => { it('should create a function with a reference', () => { - const func = database.onRefCreated('/foo/{bar}/', (event) => 2); + const func = database.onValueCreated('/foo/{bar}/', (event) => 2); expect(func.__endpoint).to.deep.equal({ platform: 'gcfv2', @@ -434,7 +434,7 @@ describe('database', () => { }); it('should create a function with opts', () => { - const func = database.onRefCreated( + const func = database.onValueCreated( { ref: '/foo/{path=**}/{bar}/', instance: 'my-instance', @@ -465,9 +465,9 @@ describe('database', () => { }); }); - describe('onRefUpdated', () => { + describe('onValueUpdated', () => { it('should create a function with a reference', () => { - const func = database.onRefUpdated('/foo/{bar}/', (event) => 2); + const func = database.onValueUpdated('/foo/{bar}/', (event) => 2); expect(func.__endpoint).to.deep.equal({ platform: 'gcfv2', @@ -485,7 +485,7 @@ describe('database', () => { }); it('should create a function with opts', () => { - const func = database.onRefUpdated( + const func = database.onValueUpdated( { ref: '/foo/{path=**}/{bar}/', instance: 'my-instance', @@ -516,9 +516,9 @@ describe('database', () => { }); }); - describe('onRefDeleted', () => { + describe('onValueDeleted', () => { it('should create a function with a reference', () => { - const func = database.onRefDeleted('/foo/{bar}/', (event) => 2); + const func = database.onValueDeleted('/foo/{bar}/', (event) => 2); expect(func.__endpoint).to.deep.equal({ platform: 'gcfv2', @@ -536,7 +536,7 @@ describe('database', () => { }); it('should create a function with opts', () => { - const func = database.onRefDeleted( + const func = database.onValueDeleted( { ref: '/foo/{path=**}/{bar}/', instance: 'my-instance', diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index a362230fb..f866faa22 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -155,95 +155,6 @@ export interface EventContext { timestamp: string; } -/** - * The Functions interface for events that change state, such as - * Realtime Database or Cloud Firestore `onWrite` and `onUpdate`. - * - * For more information about the format used to construct `Change` objects, see - * [`cloud-functions.ChangeJson`](/docs/reference/functions/cloud_functions_.changejson). - * - */ -export class Change { - constructor(public before: T, public after: T) {} -} - -/** - * `ChangeJson` is the JSON format used to construct a Change object. - */ -export interface ChangeJson { - /** - * Key-value pairs representing state of data after the change. - */ - after?: any; - /** - * Key-value pairs representing state of data before the change. If - * `fieldMask` is set, then only fields that changed are present in `before`. - */ - before?: any; - /** - * @hidden - * Comma-separated string that represents names of fields that changed. - */ - fieldMask?: string; -} - -export namespace Change { - /** @hidden */ - function reinterpretCast(x: any) { - return x as T; - } - - /** - * @hidden - * Factory method for creating a Change from a `before` object and an `after` - * object. - */ - export function fromObjects(before: T, after: T) { - return new Change(before, after); - } - - /** - * @hidden - * Factory method for creating a Change from a JSON and an optional customizer - * function to be applied to both the `before` and the `after` fields. - */ - export function fromJSON( - json: ChangeJson, - customizer: (x: any) => T = reinterpretCast - ): Change { - let before = { ...json.before }; - if (json.fieldMask) { - before = applyFieldMask(before, json.after, json.fieldMask); - } - - return Change.fromObjects( - customizer(before || {}), - customizer(json.after || {}) - ); - } - - /** @hidden */ - export function applyFieldMask( - sparseBefore: any, - after: any, - fieldMask: string - ) { - const before = { ...after }; - const masks = fieldMask.split(','); - - masks.forEach((mask) => { - const val = _.get(sparseBefore, mask); - if (typeof val === 'undefined') { - _.unset(before, mask); - } else { - _.set(before, mask, val); - } - }); - - return before; - } -} - /** * Resource is a standard format for defining a resource * (google.rpc.context.AttributeContext.Resource). In Cloud Functions, it is the diff --git a/src/common/change.ts b/src/common/change.ts new file mode 100644 index 000000000..c0180e0a0 --- /dev/null +++ b/src/common/change.ts @@ -0,0 +1,109 @@ +// The MIT License (MIT) +// +// Copyright (c) 2022 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. + +/** + * `ChangeJson` is the JSON format used to construct a Change object. + */ +export interface ChangeJson { + /** + * Key-value pairs representing state of data after the change. + */ + after?: any; + /** + * Key-value pairs representing state of data before the change. If + * `fieldMask` is set, then only fields that changed are present in `before`. + */ + before?: any; + /** + * @hidden + * Comma-separated string that represents names of fields that changed. + */ + fieldMask?: string; +} + +/** @hidden */ +export function applyFieldMask( + sparseBefore: any, + after: any, + fieldMask: string +) { + const before = { ...after }; + const masks = fieldMask.split(','); + + for (const mask of masks) { + const parts = mask.split('.'); + const head = parts[0]; + const tail = parts.slice(1).join('.'); + if (parts.length > 1) { + before[head] = applyFieldMask(sparseBefore?.[head], after[head], tail); + continue; + } + const val = sparseBefore?.[head]; + if (typeof val === 'undefined') { + delete before[mask]; + } else { + before[mask] = val; + } + } + + return before; +} + +/** + * The Functions interface for events that change state, such as + * Realtime Database or Cloud Firestore `onWrite` and `onUpdate`. + * + * For more information about the format used to construct `Change` objects, see + * {@link ChangeJson} below. + * + */ +export class Change { + /** + * @hidden + * Factory method for creating a Change from a `before` object and an `after` + * object. + */ + static fromObjects(before: T, after: T) { + return new Change(before, after); + } + + /** + * @hidden + * Factory method for creating a Change from a JSON and an optional customizer + * function to be applied to both the `before` and the `after` fields. + */ + static fromJSON( + json: ChangeJson, + customizer: (x: any) => T = (x) => x as T + ): Change { + let before = { ...json.before }; + if (json.fieldMask) { + before = applyFieldMask(before, json.after, json.fieldMask); + } + + return Change.fromObjects( + customizer(before || {}), + customizer(json.after || {}) + ); + } + constructor(public before: T, public after: T) {} +} diff --git a/src/providers/database.ts b/src/providers/database.ts index 5b7dc59f5..39feaf0e8 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -22,12 +22,12 @@ import { apps } from '../apps'; import { - Change, CloudFunction, Event, EventContext, makeCloudFunction, } from '../cloud-functions'; +import { Change } from '../common/change'; import { DataSnapshot } from '../common/providers/database'; import { firebaseConfig } from '../config'; import { DeploymentOptions } from '../function-configuration'; diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index 620040d2b..73d8eed13 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -26,12 +26,12 @@ import * as _ from 'lodash'; import { posix } from 'path'; import { apps } from '../apps'; import { - Change, CloudFunction, Event, EventContext, makeCloudFunction, } from '../cloud-functions'; +import { Change } from '../common/change'; import { dateToTimestampProto } from '../encoder'; import { DeploymentOptions } from '../function-configuration'; import * as logger from '../logger'; diff --git a/src/v2/core.ts b/src/v2/core.ts index 5e71d2458..7bd88fc8d 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -25,8 +25,11 @@ * @packageDocumentation */ +import { Change } from '../common/change'; import { ManifestEndpoint } from '../runtime/manifest'; +export { Change }; + /** @internal */ export interface TriggerAnnotation { platform?: string; diff --git a/src/v2/providers/database.ts b/src/v2/providers/database.ts index e0b410422..1ab09f666 100644 --- a/src/v2/providers/database.ts +++ b/src/v2/providers/database.ts @@ -21,7 +21,7 @@ // SOFTWARE. import { apps } from '../../apps'; -import { Change } from '../../cloud-functions'; +import { Change } from '../../common/change'; import { DataSnapshot } from '../../common/providers/database'; import { ManifestEndpoint } from '../../runtime/manifest'; import { normalizePath } from '../../utilities/path'; @@ -84,12 +84,106 @@ export interface ReferenceOptions extends options.EventHandlerOptions { * Examples: '/foo/bar', '/foo/{bar}' */ ref: string; + /** * Specify the handler to trigger on a database instance(s). * If present, this value can either be a single instance or a pattern. * Examples: 'my-instance-1', '{instance}' */ instance?: string; + + /** + * Region where functions should be deployed. + */ + region?: options.SupportedRegion | string; + + /** + * Amount of memory to allocate to a function. + * A value of null restores the defaults of 256MB. + */ + memory?: options.MemoryOption | null; + + /** + * Timeout for the function in sections, possible values are 0 to 540. + * HTTPS functions can specify a higher timeout. + * A value of null restores the default of 60s + * The minimum timeout for a gen 2 function is 1s. The maximum timeout for a + * function depends on the type of function: Event handling functions have a + * maximum timeout of 540s (9 minutes). HTTPS and callable functions have a + * maximum timeout of 36,00s (1 hour). Task queue functions have a maximum + * timeout of 1,800s (30 minutes) + */ + timeoutSeconds?: number | null; + + /** + * Min number of actual instances to be running at a given time. + * Instances will be billed for memory allocation and 10% of CPU allocation + * while idle. + * A value of null restores the default min instances. + */ + minInstances?: number | null; + + /** + * Max number of instances to be running in parallel. + * A value of null restores the default max instances. + */ + maxInstances?: number | null; + + /** + * Number of requests a function can serve at once. + * Can only be applied to functions running on Cloud Functions v2. + * A value of null restores the default concurrency (80 when CPU >= 1, 1 otherwise). + * Concurrency cannot be set to any value other than 1 if `cpu` is less than 1. + * The maximum value for concurrency is 1,000. + */ + concurrency?: number | null; + + /** + * Fractional number of CPUs to allocate to a function. + * Defaults to 1 for functions with <= 2GB RAM and increases for larger memory sizes. + * This is different from the defaults when using the gcloud utility and is different from + * the fixed amount assigned in Google Cloud Functions generation 1. + * To revert to the CPU amounts used in gcloud or in Cloud Functions generation 1, set this + * to the value "gcf_gen1" + */ + cpu?: number | 'gcf_gen1'; + + /** + * Connect cloud function to specified VPC connector. + * A value of null removes the VPC connector + */ + vpcConnector?: string | null; + + /** + * Egress settings for VPC connector. + * A value of null turns off VPC connector egress settings + */ + vpcConnectorEgressSettings?: options.VpcEgressSetting | null; + + /** + * Specific service account for the function to run as. + * A value of null restores the default service account. + */ + serviceAccount?: string | null; + + /** + * Ingress settings which control where this function can be called from. + * A value of null turns off ingress settings. + */ + ingressSettings?: options.IngressSetting | null; + + /** + * User labels to set on the function. + */ + labels?: Record; + + /* + * Secrets to bind to a function. + */ + secrets?: string[]; + + /** Whether failed executions should be delivered again. */ + retry?: boolean; } /** @@ -98,7 +192,7 @@ export interface ReferenceOptions extends options.EventHandlerOptions { * @param reference - The database reference path to trigger on. * @param handler - Event handler which is run every time a Realtime Database create, update, or delete occurs. */ -export function onRefWritten( +export function onValueWritten( reference: string, handler: (event: DatabaseEvent>) => any | Promise ): CloudFunction>>; @@ -109,7 +203,7 @@ export function onRefWritten( * @param opts - Options that can be set on an individual event-handling function. * @param handler - Event handler which is run every time a Realtime Database create, update, or delete occurs. */ -export function onRefWritten( +export function onValueWritten( opts: ReferenceOptions, handler: (event: DatabaseEvent>) => any | Promise ): CloudFunction>>; @@ -120,7 +214,7 @@ export function onRefWritten( * @param referenceOrOpts - Options or a string reference. * @param handler - Event handler which is run every time a Realtime Database create, update, or delete occurs. */ -export function onRefWritten( +export function onValueWritten( referenceOrOpts: string | ReferenceOptions, handler: (event: DatabaseEvent>) => any | Promise ): CloudFunction>> { @@ -133,7 +227,7 @@ export function onRefWritten( * @param reference - The database reference path to trigger on. * @param handler - Event handler which is run every time a Realtime Database create occurs. */ -export function onRefCreated( +export function onValueCreated( reference: string, handler: (event: DatabaseEvent) => any | Promise ): CloudFunction>; @@ -144,7 +238,7 @@ export function onRefCreated( * @param opts - Options that can be set on an individual event-handling function. * @param handler - Event handler which is run every time a Realtime Database create occurs. */ -export function onRefCreated( +export function onValueCreated( opts: ReferenceOptions, handler: (event: DatabaseEvent) => any | Promise ): CloudFunction>; @@ -155,7 +249,7 @@ export function onRefCreated( * @param referenceOrOpts - Options or a string reference. * @param handler - Event handler which is run every time a Realtime Database create occurs. */ -export function onRefCreated( +export function onValueCreated( referenceOrOpts: string | ReferenceOptions, handler: (event: DatabaseEvent) => any | Promise ): CloudFunction> { @@ -168,7 +262,7 @@ export function onRefCreated( * @param reference - The database reference path to trigger on. * @param handler - Event handler which is run every time a Realtime Database update occurs. */ -export function onRefUpdated( +export function onValueUpdated( reference: string, handler: (event: DatabaseEvent>) => any | Promise ): CloudFunction>>; @@ -179,7 +273,7 @@ export function onRefUpdated( * @param opts - Options that can be set on an individual event-handling function. * @param handler - Event handler which is run every time a Realtime Database update occurs. */ -export function onRefUpdated( +export function onValueUpdated( opts: ReferenceOptions, handler: (event: DatabaseEvent>) => any | Promise ): CloudFunction>>; @@ -190,7 +284,7 @@ export function onRefUpdated( * @param referenceOrOpts - Options or a string reference. * @param handler - Event handler which is run every time a Realtime Database update occurs. */ -export function onRefUpdated( +export function onValueUpdated( referenceOrOpts: string | ReferenceOptions, handler: (event: DatabaseEvent>) => any | Promise ): CloudFunction>> { @@ -203,7 +297,7 @@ export function onRefUpdated( * @param reference - The database reference path to trigger on. * @param handler - Event handler which is run every time a Realtime Database deletion occurs. */ -export function onRefDeleted( +export function onValueDeleted( reference: string, handler: (event: DatabaseEvent) => any | Promise ): CloudFunction>; @@ -214,7 +308,7 @@ export function onRefDeleted( * @param opts - Options that can be set on an individual event-handling function. * @param handler - Event handler which is run every time a Realtime Database deletion occurs. */ -export function onRefDeleted( +export function onValueDeleted( opts: ReferenceOptions, handler: (event: DatabaseEvent) => any | Promise ): CloudFunction>; @@ -225,7 +319,7 @@ export function onRefDeleted( * @param referenceOrOpts - Options or a string reference. * @param handler - Event handler which is run every time a Realtime Database deletion occurs. */ -export function onRefDeleted( +export function onValueDeleted( referenceOrOpts: string | ReferenceOptions, handler: (event: DatabaseEvent) => any | Promise ): CloudFunction> {