diff --git a/CHANGELOG.md b/CHANGELOG.md index af4db828c..ae2bb13ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ -- Fixes a bug that disallowed setting customClaims and/or sessionClaims in blocking functions (#1199). -- Add v2 Schedule Triggers (#1177). -- Add performance monitoring triggers to v2 alerts (#1223). +### Breaking Changes + +- Deprecated `allowInvalidAppCheckToken` option. Instead use + `enforceAppCheck`. + +> App Check enforcement on callable functions is disabled by default in v4. +> Requests containing invalid App Check tokens won't be denied unless you +> explicitly enable App Check enforcement using the new `enforceAppCheck` option. +> Furthermore, when enforcement is enabled, callable functions will deny +> all requests without App Check tokens. + +- Dropped support for Node.js versions 8, 10, and 12. +- Dropped support for Admin SDK versions 8 and 9. +- Removed the `functions.handler` namespace. +- `DataSnapshot` passed to the Firebase Realtime Database trigger now + matches the `DataSnapshot` returned by the Admin SDK, with null values + removed. +- Removed `__trigger` object on function handlers. +- Reorganized source code location. This affects only apps that directly import files instead of using the recommend entry points specified in the +- Reworked the `apps` library and removed `lodash` as a runtime dependency. + +### Enhancements + +- Logs created with the `functions.logger` package in v2 functions + are now annotated with each request's trace ID, making it easy to correlate + log entries with the incoming request. Trace IDs are especially useful for + cases where 2nd gen's concurrency feature permits a function + to handle multiple requests at any given time. See + [Correlate log entries](https://cloud.google.com/logging/docs/view/correlate-logs) to learn more. +- `functions.logger.error` now always outputs an error object and is included in Google Cloud Error Reporting. +- The logging severity of Auth/App Check token validation has changed from `info` to `debug` level. +- Event parameters for 2nd generation functions are now strongly typed, permitting stronger TypeScript types for matched parameters. diff --git a/docgen/toc.ts b/docgen/toc.ts index b359092d8..0b5d862b2 100644 --- a/docgen/toc.ts +++ b/docgen/toc.ts @@ -6,18 +6,12 @@ * down the api model. */ import * as yaml from 'js-yaml'; -import { - ApiPackage, - ApiItem, - ApiItemKind, - ApiParameterListMixin, - ApiModel, -} from 'api-extractor-model-me'; -import { ModuleSource } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; -import { FileSystem, PackageName } from '@rushstack/node-core-library'; +import {ApiItem, ApiItemKind, ApiModel, ApiPackage, ApiParameterListMixin,} from 'api-extractor-model-me'; +import {ModuleSource} from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; +import {FileSystem, PackageName} from '@rushstack/node-core-library'; import yargs from 'yargs'; -import { writeFileSync } from 'fs'; -import { resolve, join } from 'path'; +import {writeFileSync} from 'fs'; +import {join, resolve} from 'path'; function getSafeFileName(f: string): string { return f.replace(/[^a-z0-9_\-\.]/gi, '_').toLowerCase(); @@ -108,13 +102,33 @@ export function generateToc({ } } - const toc = []; - generateTocRecursively(apiModel, g3Path, addFileNameSuffix, toc); + // Firebase Functions only have 1 entry point. Let's traverse the tree to find it. + const apiItems: ApiItem[] = []; + let cursor = apiModel as ApiItem; + while (cursor?.kind !== ApiItemKind.EntryPoint) { + apiItems.push(...cursor.members); + cursor = apiItems.pop(); + } + if (!cursor) { + throw new Error("Couldn't find entry point from api model. Are you sure you've generated the api model?") + } + + const entryPointName = ( + cursor.canonicalReference.source! as ModuleSource + ).escapedPath.replace('@firebase/', ''); + + const entryPointToc: ITocItem = { + title: entryPointName, + path: `${g3Path}/${getFilenameForApiItem(cursor, addFileNameSuffix)}`, + section: [], + }; + + generateTocRecursively(cursor, g3Path, addFileNameSuffix, entryPointToc); writeFileSync( resolve(outputFolder, 'toc.yaml'), yaml.dump( - { toc }, + { toc: entryPointToc }, { quotingType: '"', } @@ -126,43 +140,31 @@ function generateTocRecursively( apiItem: ApiItem, g3Path: string, addFileNameSuffix: boolean, - toc: ITocItem[] + toc: ITocItem ) { - // generate toc item only for entry points - if (apiItem.kind === ApiItemKind.EntryPoint) { - // Entry point - const entryPointName = ( - apiItem.canonicalReference.source! as ModuleSource - ).escapedPath.replace('@firebase/', ''); - const entryPointToc: ITocItem = { - title: entryPointName, - path: `${g3Path}/${getFilenameForApiItem(apiItem, addFileNameSuffix)}`, - section: [], - }; - for (const member of apiItem.members) { - // only classes and interfaces have dedicated pages + // only namespaces/classes gets included in ToC. if ( - member.kind === ApiItemKind.Interface || - member.kind === ApiItemKind.Namespace + [ + ApiItemKind.Class, + ApiItemKind.Namespace, + ApiItemKind.Interface, + ].includes(member.kind) ) { const fileName = getFilenameForApiItem(member, addFileNameSuffix); const title = member.displayName[0].toUpperCase() + member.displayName.slice(1); - entryPointToc.section!.push({ + const section: ITocItem = { title, path: `${g3Path}/${fileName}`, - }); + } + if (!toc.section) { + toc.section = []; + } + toc.section.push(section); + generateTocRecursively(member, g3Path, addFileNameSuffix, section); } } - - toc.push(entryPointToc); - } else { - // travel the api tree to find the next entry point - for (const member of apiItem.members) { - generateTocRecursively(member, g3Path, addFileNameSuffix, toc); - } - } } const { input, output, path } = yargs(process.argv.slice(2)) diff --git a/package-lock.json b/package-lock.json index c938cdc63..d1b16979a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "firebase-functions", - "version": "3.22.0", + "version": "3.23.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-functions", - "version": "3.22.0", + "version": "3.23.0", "license": "MIT", "dependencies": { "@types/cors": "^2.8.5", diff --git a/package.json b/package.json index 07fa042e5..94ce405d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.22.0", + "version": "3.23.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", @@ -56,7 +56,8 @@ "./v2/eventarc": "./lib/v2/providers/eventarc.js", "./v2/identity": "./lib/v2/providers/identity.js", "./v2/database": "./lib/v2/providers/database.js", - "./v2/scheduler": "./lib/v2/providers/scheduler.js" + "./v2/scheduler": "./lib/v2/providers/scheduler.js", + "./v2/remoteConfig": "./lib/v2/providers/remoteConfig.js" }, "typesVersions": { "*": { @@ -146,6 +147,9 @@ ], "v2/scheduler": [ "lib/v2/providers/scheduler" + ], + "v2/remoteConfig": [ + "lib/v2/providers/remoteConfig" ] } }, diff --git a/spec/v2/providers/remoteConfig.spec.ts b/spec/v2/providers/remoteConfig.spec.ts new file mode 100644 index 000000000..adcc0f009 --- /dev/null +++ b/spec/v2/providers/remoteConfig.spec.ts @@ -0,0 +1,74 @@ +// 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 remoteConfig from "../../../src/v2/providers/remoteConfig"; +import * as options from "../../../src/v2/options"; + +describe("onConfigUpdated", () => { + afterEach(() => { + options.setGlobalOptions({}); + }); + + it("should create a function with a handler", () => { + const fn = remoteConfig.onConfigUpdated(() => 2); + + expect(fn.__endpoint).to.deep.eq({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + eventType: remoteConfig.eventType, + eventFilters: {}, + retry: false, + }, + }); + expect(fn.run(1 as any)).to.eq(2); + }); + + it("should create a function with opts and a handler", () => { + options.setGlobalOptions({ + memory: "512MiB", + region: "us-west1", + }); + + const fn = remoteConfig.onConfigUpdated( + { + region: "us-central1", + retry: true, + }, + () => 2 + ); + + expect(fn.__endpoint).to.deep.eq({ + platform: "gcfv2", + availableMemoryMb: 512, + region: ["us-central1"], + labels: {}, + eventTrigger: { + eventType: remoteConfig.eventType, + eventFilters: {}, + retry: true, + }, + }); + expect(fn.run(1 as any)).to.eq(2); + }); +}); diff --git a/src/params/types.ts b/src/params/types.ts index 6f678fb1f..477531a8e 100644 --- a/src/params/types.ts +++ b/src/params/types.ts @@ -63,7 +63,7 @@ export class TernaryExpression< } value(): T { - return this.test.value ? this.ifTrue : this.ifFalse; + return this.test.value() ? this.ifTrue : this.ifFalse; } toString() { diff --git a/src/v1/cloud-functions.ts b/src/v1/cloud-functions.ts index 3a50141bc..00eba0040 100644 --- a/src/v1/cloud-functions.ts +++ b/src/v1/cloud-functions.ts @@ -220,7 +220,7 @@ export interface Runnable { } /** - * The Cloud Function type for HTTPS triggers. This should be exported from your + * The function type for HTTPS triggers. This should be exported from your * JavaScript file to define a Cloud Function. * * @remarks @@ -240,9 +240,16 @@ export interface HttpsFunction { } /** - * The Cloud Function type for Blocking triggers. + * The function type for Auth Blocking triggers. + * + * @remarks + * This type is a special JavaScript function for Auth Blocking triggers which takes Express + * {@link https://expressjs.com/en/api.html#req | `Request` } and + * {@link https://expressjs.com/en/api.html#res | `Response` } objects as its only + * arguments. */ export interface BlockingFunction { + /** @public */ (req: Request, resp: Response): void | Promise; /** @internal */ @@ -253,7 +260,7 @@ export interface BlockingFunction { } /** - * The Cloud Function type for all non-HTTPS triggers. This should be exported + * The function type for all non-HTTPS triggers. This should be exported * from your JavaScript file to define a Cloud Function. * * This type is a special JavaScript function which takes a templated diff --git a/src/v1/providers/auth.ts b/src/v1/providers/auth.ts index baafac98a..8d015eba4 100644 --- a/src/v1/providers/auth.ts +++ b/src/v1/providers/auth.ts @@ -71,9 +71,11 @@ export interface UserOptions { } /** - * Handles events related to Firebase authentication users. + * Handles events related to Firebase Auth users events. + * * @param userOptions - Resource level options - * @returns UserBuilder - Builder used to create Cloud Functions for Firebase Auth user lifecycle events + * @returns UserBuilder - Builder used to create functions for Firebase Auth user lifecycle events + * * @public */ export function user(userOptions?: UserOptions): UserBuilder { @@ -95,7 +97,7 @@ export function _userWithOptions(options: DeploymentOptions, userOptions: UserOp } /** - * Builder used to create Cloud Functions for Firebase Auth user lifecycle events. + * Builder used to create functions for Firebase Auth user lifecycle events. * @public */ export class UserBuilder { @@ -103,6 +105,7 @@ export class UserBuilder { return userRecordConstructor(raw.data); } + /* @internal */ constructor( private triggerResource: () => string, private options: DeploymentOptions, @@ -111,6 +114,9 @@ export class UserBuilder { /** * Responds to the creation of a Firebase Auth user. + * + * @param handler Event handler that responds to the creation of a Firebase Auth user. + * * @public */ onCreate( @@ -121,6 +127,9 @@ export class UserBuilder { /** * Responds to the deletion of a Firebase Auth user. + * + * @param handler Event handler that responds to the deletion of a Firebase Auth user. + * * @public */ onDelete( @@ -129,6 +138,13 @@ export class UserBuilder { return this.onOperation(handler, "user.delete"); } + /** + * Blocks request to create a Firebase Auth user. + * + * @param handler Event handler that blocks creation of a Firebase Auth user. + * + * @public + */ beforeCreate( handler: ( user: AuthUserRecord, @@ -138,6 +154,13 @@ export class UserBuilder { return this.beforeOperation(handler, "beforeCreate"); } + /** + * Blocks request to sign-in a Firebase Auth user. + * + * @param handler Event handler that blocks sign-in of a Firebase Auth user. + * + * @public + */ beforeSignIn( handler: ( user: AuthUserRecord, diff --git a/src/v2/index.ts b/src/v2/index.ts index 2f8c82f6e..c5fc91b2b 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -38,8 +38,21 @@ import * as pubsub from "./providers/pubsub"; import * as scheduler from "./providers/scheduler"; import * as storage from "./providers/storage"; import * as tasks from "./providers/tasks"; +import * as remoteConfig from "./providers/remoteConfig"; -export { alerts, database, storage, https, identity, pubsub, logger, tasks, eventarc, scheduler }; +export { + alerts, + database, + storage, + https, + identity, + pubsub, + logger, + tasks, + eventarc, + scheduler, + remoteConfig, +}; export { setGlobalOptions, diff --git a/src/v2/providers/remoteConfig.ts b/src/v2/providers/remoteConfig.ts new file mode 100644 index 000000000..8e11c038c --- /dev/null +++ b/src/v2/providers/remoteConfig.ts @@ -0,0 +1,156 @@ +// 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 { ManifestEndpoint } from "../../runtime/manifest"; +import { CloudEvent, CloudFunction } from "../core"; +import { EventHandlerOptions, getGlobalOptions, optionsToEndpoint } from "../options"; + +/** @internal */ +export const eventType = "google.firebase.remoteconfig.remoteConfig.v1.updated"; + +/* All the fields associated with the person/service account that wrote a Remote Config template. */ +export interface ConfigUser { + /* Display name. */ + name: string; + + /* Email address. */ + email: string; + + /* Image URL. */ + imageUrl: string; +} + +/* What type of update was associated with the Remote Config template version. */ +export type ConfigUpdateOrigin = + /* Catch-all for unrecognized values. */ + | "REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED" + /* The update came from the Firebase UI. */ + | "CONSOLE" + /* The update came from the Remote Config REST API. */ + | "REST_API" + /* The update came from the Firebase Admin Node SDK. */ + | "ADMIN_SDK_NODE"; + +/* Where the Remote Config update action originated. */ +export type ConfigUpdateType = + /* Catch-all for unrecognized enum values */ + | "REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED" + /* A regular incremental update */ + | "INCREMENTAL_UPDATE" + /* A forced update. The ETag was specified as "*" in an UpdateRemoteConfigRequest request or the "Force Update" button was pressed on the console */ + | "FORCED_UPDATE" + /* A rollback to a previous Remote Config template */ + | "ROLLBACK"; + +/* The data within Firebase Remote Config update events. */ +export interface ConfigUpdateData { + /* The version number of the version's corresponding Remote Config template. */ + versionNumber: number; + + /* When the Remote Config template was written to the Remote Config server. */ + updateTime: string; + + /* Aggregation of all metadata fields about the account that performed the update. */ + updateUser: ConfigUser; + + /* The user-provided description of the corresponding Remote Config template. */ + description: string; + + /* Where the update action originated. */ + updateOrigin: ConfigUpdateOrigin; + + /* What type of update was made. */ + updateType: ConfigUpdateType; + + /* Only present if this version is the result of a rollback, and will be the version number of the Remote Config template that was rolled-back to. */ + rollbackSource: number; +} + +/** + * Event handler which triggers when data is updated in a Remote Config. + * + * @param handler - Event handler which is run every time a Remote Config update occurs. + * @returns A Cloud Function that you can export and deploy. + * @alpha + */ +export function onConfigUpdated( + handler: (event: CloudEvent) => any | Promise +): CloudFunction>; + +/** + * Event handler which triggers when data is updated in a Remote Config. + * + * @param opts - Options that can be set on an individual event-handling function. + * @param handler - Event handler which is run every time a Remote Config update occurs. + * @returns A Cloud Function that you can export and deploy. + * @alpha + */ +export function onConfigUpdated( + opts: EventHandlerOptions, + handler: (event: CloudEvent) => any | Promise +): CloudFunction>; + +/** + * Event handler which triggers when data is updated in a Remote Config. + * + * @param optsOrHandler - Options or an event handler. + * @param handler - Event handler which is run every time a Remote Config update occurs. + * @returns A Cloud Function that you can export and deploy. + * @alpha + */ +export function onConfigUpdated( + optsOrHandler: + | EventHandlerOptions + | ((event: CloudEvent) => any | Promise), + handler?: (event: CloudEvent) => any | Promise +): CloudFunction> { + if (typeof optsOrHandler === "function") { + handler = optsOrHandler as (event: CloudEvent) => any | Promise; + optsOrHandler = {}; + } + + const baseOpts = optionsToEndpoint(getGlobalOptions()); + const specificOpts = optionsToEndpoint(optsOrHandler); + + const func: any = (raw: CloudEvent) => { + return handler(raw as CloudEvent); + }; + func.run = handler; + + const ep: ManifestEndpoint = { + platform: "gcfv2", + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + eventTrigger: { + eventType, + eventFilters: {}, + retry: !!optsOrHandler.retry, + }, + }; + func.__endpoint = ep; + + return func; +} diff --git a/src/v2/providers/scheduler.ts b/src/v2/providers/scheduler.ts index 6f677f2d7..90feb8dd7 100644 --- a/src/v2/providers/scheduler.ts +++ b/src/v2/providers/scheduler.ts @@ -21,12 +21,14 @@ // SOFTWARE. import * as express from "express"; + import { copyIfPresent } from "../../common/encoding"; import { timezone } from "../../common/timezone"; -import * as logger from "../../logger"; import { ManifestEndpoint, ManifestRequiredAPI } from "../../runtime/manifest"; -import * as options from "../options"; import { HttpsFunction } from "./https"; +import { wrapTraceContext } from "../trace"; +import * as logger from "../../logger"; +import * as options from "../options"; /** @hidden */ interface ScheduleArgs { @@ -122,7 +124,7 @@ export interface ScheduleOptions extends options.GlobalOptions { */ export function onSchedule( schedule: string, - handler: (req: ScheduledEvent) => void | Promise + handler: (event: ScheduledEvent) => void | Promise ): ScheduleFunction; /** @@ -134,7 +136,7 @@ export function onSchedule( */ export function onSchedule( options: ScheduleOptions, - handler: (req: ScheduledEvent) => void | Promise + handler: (event: ScheduledEvent) => void | Promise ): ScheduleFunction; /** @@ -146,11 +148,11 @@ export function onSchedule( */ export function onSchedule( args: string | ScheduleOptions, - handler: (req: ScheduledEvent) => void | Promise + handler: (event: ScheduledEvent) => void | Promise ): ScheduleFunction { const separatedOpts = getOpts(args); - const func: any = async (req: express.Request, res: express.Response): Promise => { + const httpFunc = async (req: express.Request, res: express.Response): Promise => { const event: ScheduledEvent = { jobName: req.header("X-CloudScheduler-JobName") || undefined, scheduleTime: req.header("X-CloudScheduler-ScheduleTime") || new Date().toISOString(), @@ -163,6 +165,7 @@ export function onSchedule( res.status(500).send(); } }; + const func: any = wrapTraceContext(httpFunc); func.run = handler; const baseOptsEndpoint = options.optionsToEndpoint(options.getGlobalOptions()); diff --git a/v2/remoteConfig.js b/v2/remoteConfig.js new file mode 100644 index 000000000..ae33ba821 --- /dev/null +++ b/v2/remoteConfig.js @@ -0,0 +1,26 @@ +// 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. + +// 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