Skip to content

Commit ef0e5d8

Browse files
committed
Add new option to preserveExternalChanges.
1 parent 51f726c commit ef0e5d8

File tree

10 files changed

+253
-32
lines changed

10 files changed

+253
-32
lines changed

spec/common/options.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// The MIT License (MIT)
2+
//
3+
// Copyright (c) 2022 Firebase
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE ignoreUnusedWarning OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
import { ResettableKeys, ResetValue } from "../../src/common/options";
23+
import { expectNever, expectType } from "./metaprogramming";
24+
25+
describe("ResettableKeys", () => {
26+
it("should pick out keys with a type that includes ResetValue", () => {
27+
type A = { a: number; b: ResetValue; c: number | boolean | ResetValue };
28+
expectType<keyof ResettableKeys<A>>("b");
29+
expectType<keyof ResettableKeys<A>>("c");
30+
});
31+
32+
it("should return an empty type if no keys are resettable", () => {
33+
type A = { a: number };
34+
expectNever<keyof ResettableKeys<A>>();
35+
});
36+
});

src/common/options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,10 @@ export class ResetValue {
4040
* Special configuration value to reset configuration to platform default.
4141
*/
4242
export const RESET_VALUE = ResetValue.getInstance();
43+
44+
/**
45+
* @internal
46+
*/
47+
export type ResettableKeys<T> = Required<{
48+
[K in keyof T as [ResetValue] extends [T[K]] ? K : never]: null;
49+
}>;

src/common/providers/tasks.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { DecodedIdToken } from "firebase-admin/auth";
2626
import * as logger from "../../logger";
2727
import * as https from "./https";
2828
import { Expression } from "../../params";
29-
import { ResetValue } from "../options";
29+
import { ResettableKeys, ResetValue } from "../options";
3030

3131
/** How a task should be retried in the event of a non-2xx return. */
3232
export interface RetryConfig {
@@ -108,6 +108,21 @@ export interface Request<T = any> {
108108
type v1TaskHandler = (data: any, context: TaskContext) => void | Promise<void>;
109109
type v2TaskHandler<Req> = (request: Request<Req>) => void | Promise<void>;
110110

111+
/** @internal */
112+
export const RESETTABLE_RETRY_CONFIG_OPTIONS: ResettableKeys<RetryConfig> = {
113+
maxAttempts: null,
114+
maxDoublings: null,
115+
maxBackoffSeconds: null,
116+
maxRetrySeconds: null,
117+
minBackoffSeconds: null,
118+
};
119+
120+
/** @internal */
121+
export const RESETTABLE_RATE_LIMITS_OPTIONS: ResettableKeys<RateLimits> = {
122+
maxConcurrentDispatches: null,
123+
maxDispatchesPerSecond: null,
124+
};
125+
111126
/** @internal */
112127
export function onDispatchHandler<Req = any>(
113128
handler: v1TaskHandler | v2TaskHandler<Req>

src/runtime/manifest.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,33 @@ export interface ManifestEndpoint {
6363
serviceAccountEmail?: string | ResetValue;
6464
};
6565

66+
taskQueueTrigger?: {
67+
retryConfig: {
68+
maxAttempts?: number | Expression<number> | ResetValue;
69+
maxRetrySeconds?: number | Expression<number> | ResetValue;
70+
maxBackoffSeconds?: number | Expression<number> | ResetValue;
71+
maxDoublings?: number | Expression<number> | ResetValue;
72+
minBackoffSeconds?: number | Expression<number> | ResetValue;
73+
};
74+
rateLimits: {
75+
maxConcurrentDispatches?: number | Expression<number> | ResetValue;
76+
maxDispatchesPerSecond?: number | Expression<number> | ResetValue;
77+
};
78+
};
79+
6680
scheduleTrigger?: {
67-
schedule?: string | Expression<string>;
81+
schedule: string | Expression<string>;
6882
timeZone?: string | Expression<string> | ResetValue;
6983
retryConfig?: {
7084
retryCount?: number | Expression<number> | ResetValue;
7185
maxRetrySeconds?: string | Expression<string> | ResetValue;
7286
minBackoffSeconds?: string | Expression<string> | ResetValue;
7387
maxBackoffSeconds?: string | Expression<string> | ResetValue;
7488
maxDoublings?: number | Expression<number> | ResetValue;
89+
// Note: v1 schedule functions use *Duration instead of *Seconds
90+
maxRetryDuration?: string | Expression<string> | ResetValue;
91+
minBackoffDuration?: string | Expression<string> | ResetValue;
92+
maxBackoffDuration?: string | Expression<string> | ResetValue;
7593
};
7694
};
7795

src/v1/cloud-functions.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@
2222

2323
import { Request, Response } from "express";
2424
import { warn } from "../logger";
25-
import { DeploymentOptions } from "./function-configuration";
25+
import {
26+
DeploymentOptions,
27+
RESET_VALUE,
28+
RESETTABLE_SCHEDULE_OPTIONS,
29+
} from "./function-configuration";
2630
export { Request, Response };
2731
import { convertIfPresent, copyIfPresent } from "../common/encoding";
2832
import { ManifestEndpoint, ManifestRequiredAPI } from "../runtime/manifest";
29-
import { ResetValue } from "../common/options";
33+
import { ResettableKeys, ResetValue } from "../common/options";
3034

3135
export { Change } from "../common/change";
3236

@@ -373,7 +377,28 @@ export function makeCloudFunction<EventData>({
373377
};
374378

375379
if (options.schedule) {
376-
endpoint.scheduleTrigger = options.schedule as any;
380+
let scheduleTrigger: ManifestEndpoint["scheduleTrigger"] = {
381+
schedule: options.schedule.schedule,
382+
};
383+
if (!options.preserveExternalChanges) {
384+
const retryConfig = {};
385+
for (const key of Object.keys(RESETTABLE_SCHEDULE_OPTIONS)) {
386+
retryConfig[key] = RESET_VALUE;
387+
}
388+
scheduleTrigger = { ...scheduleTrigger, timeZone: RESET_VALUE, retryConfig };
389+
}
390+
endpoint.scheduleTrigger = scheduleTrigger;
391+
copyIfPresent(endpoint.scheduleTrigger, options.schedule, "timeZone");
392+
copyIfPresent(
393+
endpoint.scheduleTrigger.retryConfig,
394+
options.schedule.retryConfig,
395+
"retryCount",
396+
"maxDoublings",
397+
"maxBackoffDuration",
398+
"maxRetryDuration",
399+
"minBackoffDuration"
400+
);
401+
endpoint.scheduleTrigger = scheduleTrigger;
377402
} else {
378403
endpoint.eventTrigger = {
379404
eventType: legacyEventType || provider + "." + eventType,
@@ -455,9 +480,23 @@ function _detectAuthType(event: Event) {
455480
return "UNAUTHENTICATED";
456481
}
457482

483+
const RESETTABLE_OPTIONS: ResettableKeys<DeploymentOptions> = {
484+
memory: null,
485+
timeoutSeconds: null,
486+
minInstances: null,
487+
maxInstances: null,
488+
vpcConnector: null,
489+
serviceAccount: null,
490+
};
491+
458492
/** @internal */
459493
export function optionsToEndpoint(options: DeploymentOptions): ManifestEndpoint {
460494
const endpoint: ManifestEndpoint = {};
495+
if (!options.preserveExternalChanges) {
496+
for (const key of Object.keys(RESETTABLE_OPTIONS)) {
497+
endpoint[key] = RESET_VALUE;
498+
}
499+
}
461500
copyIfPresent(
462501
endpoint,
463502
options,

src/v1/function-configuration.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Expression } from "../params";
2-
import { ResetValue } from "../common/options";
2+
import { ResettableKeys, ResetValue } from "../common/options";
33

44
export { RESET_VALUE } from "../common/options";
55

@@ -255,4 +255,23 @@ export interface DeploymentOptions extends RuntimeOptions {
255255
* Schedule for the scheduled function.
256256
*/
257257
schedule?: Schedule;
258+
/**
259+
* Controls whether function configuration modified outside of function source is preserved. Defaults to false.
260+
*
261+
* @remarks
262+
* When setting configuration available in the underlying platform that is not yet available in the Firebase Functions
263+
* SDK, we highly recommend setting preserveExternalChanges to true. Otherwise, when Firebase Functions SDK releases
264+
* a new version of the SDK with the support for the missing configuration, your functions manually configured setting
265+
* may inadvertently be wiped out.
266+
*/
267+
preserveExternalChanges?: boolean;
258268
}
269+
270+
/** @internal */
271+
export const RESETTABLE_SCHEDULE_OPTIONS: ResettableKeys<ScheduleRetryConfig> = {
272+
retryCount: null,
273+
maxBackoffDuration: null,
274+
maxDoublings: null,
275+
maxRetryDuration: null,
276+
minBackoffDuration: null,
277+
};

src/v1/providers/tasks.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ import { Request } from "../../common/providers/https";
2727
import {
2828
onDispatchHandler,
2929
RateLimits,
30+
RESETTABLE_RATE_LIMITS_OPTIONS,
31+
RESETTABLE_RETRY_CONFIG_OPTIONS,
3032
RetryConfig,
3133
TaskContext,
3234
} from "../../common/providers/tasks";
3335
import { ManifestEndpoint, ManifestRequiredAPI } from "../../runtime/manifest";
3436
import { optionsToEndpoint } from "../cloud-functions";
35-
import { DeploymentOptions } from "../function-configuration";
37+
import { DeploymentOptions, RESET_VALUE } from "../function-configuration";
3638

3739
export { RetryConfig, RateLimits, TaskContext };
3840

@@ -101,10 +103,23 @@ export class TaskQueueBuilder {
101103
const fixedLen = (data: any, context: TaskContext) => handler(data, context);
102104
const func: any = onDispatchHandler(fixedLen);
103105

106+
let taskQueueTrigger = {};
107+
if (!this.depOpts?.preserveExternalChanges) {
108+
const retryConfig = {};
109+
for (const key of Object.keys(RESETTABLE_RETRY_CONFIG_OPTIONS)) {
110+
retryConfig[key] = RESET_VALUE;
111+
}
112+
const rateLimits = {};
113+
for (const key of Object.keys(RESETTABLE_RATE_LIMITS_OPTIONS)) {
114+
rateLimits[key] = RESET_VALUE;
115+
}
116+
taskQueueTrigger = { retryConfig, rateLimits };
117+
}
118+
104119
func.__endpoint = {
105120
platform: "gcfv1",
106121
...optionsToEndpoint(this.depOpts),
107-
taskQueueTrigger: {},
122+
taskQueueTrigger,
108123
};
109124
copyIfPresent(func.__endpoint.taskQueueTrigger, this.tqOpts, "retryConfig");
110125
copyIfPresent(func.__endpoint.taskQueueTrigger, this.tqOpts, "rateLimits");

src/v2/options.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
*/
2727

2828
import { convertIfPresent, copyIfPresent } from "../common/encoding";
29-
import { ResetValue } from "../common/options";
29+
import { RESET_VALUE, ResettableKeys, ResetValue } from "../common/options";
3030
import { ManifestEndpoint } from "../runtime/manifest";
3131
import { declaredParams, Expression } from "../params";
3232
import { ParamSpec } from "../params/types";
@@ -184,12 +184,25 @@ export interface GlobalOptions {
184184
secrets?: string[];
185185

186186
/**
187-
* Determines whether Firebase AppCheck is enforced.
187+
* Determines whether Firebase AppCheck is enforced. Defaults to false.
188+
*
189+
* @remarks
188190
* When true, requests with invalid tokens autorespond with a 401
189191
* (Unauthorized) error.
190192
* When false, requests with invalid tokens set event.app to undefiend.
191193
*/
192194
enforceAppCheck?: boolean;
195+
196+
/**
197+
* Controls whether function configuration modified outside of function source is preserved. Defaults to false.
198+
*
199+
* @remarks
200+
* When setting configuration available in the underlying platform that is not yet available in the Firebase Functions
201+
* SDK, we highly recommend setting preserveExternalChanges to true. Otherwise, when Firebase Functions SDK releases
202+
* a new version of the SDK with the support for the missing configuration, your functions manually configured setting
203+
* may inadvertently be wiped out.
204+
*/
205+
preserveExternalChanges?: boolean;
193206
}
194207

195208
let globalOptions: GlobalOptions | undefined;
@@ -237,14 +250,36 @@ export interface EventHandlerOptions extends Omit<GlobalOptions, "enforceAppChec
237250
channel?: string;
238251
}
239252

253+
const RESETTABLE_CONFIGS: ResettableKeys<GlobalOptions> = {
254+
memory: null,
255+
timeoutSeconds: null,
256+
minInstances: null,
257+
maxInstances: null,
258+
ingressSettings: null,
259+
vpcConnector: null,
260+
vpcConnectorEgressSettings: null,
261+
serviceAccount: null,
262+
concurrency: null,
263+
};
264+
265+
function initEndpoint(opts: GlobalOptions | EventHandlerOptions | HttpsOptions): ManifestEndpoint {
266+
const endpoint: ManifestEndpoint = {};
267+
if (!opts.preserveExternalChanges) {
268+
for (const k of Object.keys(RESETTABLE_CONFIGS)) {
269+
endpoint[k] = RESET_VALUE;
270+
}
271+
}
272+
return endpoint;
273+
}
274+
240275
/**
241276
* Apply GlobalOptions to endpoint manifest.
242277
* @internal
243278
*/
244279
export function optionsToEndpoint(
245280
opts: GlobalOptions | EventHandlerOptions | HttpsOptions
246281
): ManifestEndpoint {
247-
const endpoint: ManifestEndpoint = {};
282+
const endpoint = initEndpoint(opts);
248283
copyIfPresent(
249284
endpoint,
250285
opts,

0 commit comments

Comments
 (0)