From 46c3ffa9fabb8d0955d470bb764ab32aa28ddcea Mon Sep 17 00:00:00 2001 From: IzaakGough Date: Thu, 9 Apr 2026 15:25:45 +0100 Subject: [PATCH 1/4] feat: accept Expression for v2 RTDB instance option --- spec/v2/providers/database.spec.ts | 16 ++++++++++++++++ src/v2/providers/database.ts | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/spec/v2/providers/database.spec.ts b/spec/v2/providers/database.spec.ts index 4435faeea..ae7e7b880 100644 --- a/spec/v2/providers/database.spec.ts +++ b/spec/v2/providers/database.spec.ts @@ -26,6 +26,7 @@ import * as database from "../../../src/v2/providers/database"; import { expectType } from "../../common/metaprogramming"; import { MINIMAL_V2_ENDPOINT } from "../../fixtures"; import { CloudEvent, onInit } from "../../../src/v2/core"; +import * as params from "../../../src/params"; const RAW_RTDB_EVENT: database.RawRTDBCloudEvent = { data: { @@ -146,6 +147,21 @@ describe("database", () => { }, }); }); + + it("should resolve instance Expression to runtime string", () => { + const name = "TEST_RTDB_INSTANCE_FOR_GETOPTS"; + const prev = process.env[name]; + process.env[name] = "resolved-instance"; + try { + const p = params.defineString(name); + expect( + database.getOpts({ ref: "/foo", instance: p }) + ).to.deep.include({ path: "foo", instance: "resolved-instance" }); + } finally { + if (prev === undefined) delete process.env[name]; + else process.env[name] = prev; + } + }); }); describe("makeEndpoint", () => { diff --git a/src/v2/providers/database.ts b/src/v2/providers/database.ts index 6c5f5e83c..010c531ba 100644 --- a/src/v2/providers/database.ts +++ b/src/v2/providers/database.ts @@ -117,7 +117,7 @@ export interface ReferenceOptions extends options.E * Examples: 'my-instance-1', 'my-instance-*' * Note: The capture syntax cannot be used for 'instance'. */ - instance?: string; + instance?: string | Expression; /** * If true, do not deploy or emulate this function. @@ -488,7 +488,7 @@ export function getOpts(referenceOrOpts: string | ReferenceOptions) { opts = {}; } else { path = normalizePath(referenceOrOpts.ref); - instance = referenceOrOpts.instance || "*"; + instance = referenceOrOpts.instance instanceof Expression ? referenceOrOpts.instance.value() : (referenceOrOpts.instance || '*'); opts = { ...referenceOrOpts }; delete (opts as any).ref; delete (opts as any).instance; From 821943423662bb3f63119444343f0e6cbd2b15cb Mon Sep 17 00:00:00 2001 From: IzaakGough Date: Thu, 9 Apr 2026 17:11:58 +0100 Subject: [PATCH 2/4] style: fix lint --- spec/v2/providers/database.spec.ts | 7 ++++--- src/v2/providers/database.ts | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/spec/v2/providers/database.spec.ts b/spec/v2/providers/database.spec.ts index ae7e7b880..79ff3ddab 100644 --- a/spec/v2/providers/database.spec.ts +++ b/spec/v2/providers/database.spec.ts @@ -154,9 +154,10 @@ describe("database", () => { process.env[name] = "resolved-instance"; try { const p = params.defineString(name); - expect( - database.getOpts({ ref: "/foo", instance: p }) - ).to.deep.include({ path: "foo", instance: "resolved-instance" }); + expect(database.getOpts({ ref: "/foo", instance: p })).to.deep.include({ + path: "foo", + instance: "resolved-instance", + }); } finally { if (prev === undefined) delete process.env[name]; else process.env[name] = prev; diff --git a/src/v2/providers/database.ts b/src/v2/providers/database.ts index 010c531ba..16b3b0dc0 100644 --- a/src/v2/providers/database.ts +++ b/src/v2/providers/database.ts @@ -488,7 +488,10 @@ export function getOpts(referenceOrOpts: string | ReferenceOptions) { opts = {}; } else { path = normalizePath(referenceOrOpts.ref); - instance = referenceOrOpts.instance instanceof Expression ? referenceOrOpts.instance.value() : (referenceOrOpts.instance || '*'); + instance = + referenceOrOpts.instance instanceof Expression + ? referenceOrOpts.instance.value() + : referenceOrOpts.instance || "*"; opts = { ...referenceOrOpts }; delete (opts as any).ref; delete (opts as any).instance; From 4542780db972e70e5f27e1e5605b1c6cdef69475 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Thu, 23 Apr 2026 10:09:08 +0100 Subject: [PATCH 3/4] test: replace getOpts magic string with name constants --- spec/v2/providers/database.spec.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/spec/v2/providers/database.spec.ts b/spec/v2/providers/database.spec.ts index 79ff3ddab..9d87bed70 100644 --- a/spec/v2/providers/database.spec.ts +++ b/spec/v2/providers/database.spec.ts @@ -47,6 +47,9 @@ const RAW_RTDB_EVENT: database.RawRTDBCloudEvent = { authtype: "unauthenticated", }; +const TEST_RTDB_INSTANCE_ENV_VAR = "TEST_RTDB_INSTANCE_FOR_GETOPTS"; +const RESOLVED_RTDB_INSTANCE = "resolved-instance"; + describe("database", () => { describe("makeParams", () => { it("should make params with basic path", () => { @@ -149,18 +152,17 @@ describe("database", () => { }); it("should resolve instance Expression to runtime string", () => { - const name = "TEST_RTDB_INSTANCE_FOR_GETOPTS"; - const prev = process.env[name]; - process.env[name] = "resolved-instance"; + const prev = process.env[TEST_RTDB_INSTANCE_ENV_VAR]; + process.env[TEST_RTDB_INSTANCE_ENV_VAR] = RESOLVED_RTDB_INSTANCE; try { - const p = params.defineString(name); + const p = params.defineString(TEST_RTDB_INSTANCE_ENV_VAR); expect(database.getOpts({ ref: "/foo", instance: p })).to.deep.include({ path: "foo", - instance: "resolved-instance", + instance: RESOLVED_RTDB_INSTANCE, }); } finally { - if (prev === undefined) delete process.env[name]; - else process.env[name] = prev; + if (prev === undefined) delete process.env[TEST_RTDB_INSTANCE_ENV_VAR]; + else process.env[TEST_RTDB_INSTANCE_ENV_VAR] = prev; } }); }); From ec734337b1b716b4c87b91ad4b69ad0199255639 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Fri, 15 May 2026 14:28:54 +0100 Subject: [PATCH 4/4] fix(database): preserve instance param expressions in v2 RTDB triggers --- spec/v2/providers/database.spec.ts | 121 +++++++++++++++++++++++++---- src/v2/providers/database.ts | 39 ++++++---- 2 files changed, 134 insertions(+), 26 deletions(-) diff --git a/spec/v2/providers/database.spec.ts b/spec/v2/providers/database.spec.ts index 9d87bed70..b17d1363e 100644 --- a/spec/v2/providers/database.spec.ts +++ b/spec/v2/providers/database.spec.ts @@ -21,12 +21,15 @@ // SOFTWARE. import { expect } from "chai"; +import * as sinon from "sinon"; import { PathPattern } from "../../../src/common/utilities/path-pattern"; import * as database from "../../../src/v2/providers/database"; import { expectType } from "../../common/metaprogramming"; import { MINIMAL_V2_ENDPOINT } from "../../fixtures"; import { CloudEvent, onInit } from "../../../src/v2/core"; import * as params from "../../../src/params"; +import * as logger from "../../../src/logger"; +import { stackToWire } from "../../../src/runtime/manifest"; const RAW_RTDB_EVENT: database.RawRTDBCloudEvent = { data: { @@ -51,6 +54,13 @@ const TEST_RTDB_INSTANCE_ENV_VAR = "TEST_RTDB_INSTANCE_FOR_GETOPTS"; const RESOLVED_RTDB_INSTANCE = "resolved-instance"; describe("database", () => { + afterEach(() => { + params.clearParams(); + sinon.restore(); + delete process.env.FUNCTIONS_CONTROL_API; + delete process.env[TEST_RTDB_INSTANCE_ENV_VAR]; + }); + describe("makeParams", () => { it("should make params with basic path", () => { const event: database.RawRTDBCloudEvent = { @@ -151,19 +161,13 @@ describe("database", () => { }); }); - it("should resolve instance Expression to runtime string", () => { - const prev = process.env[TEST_RTDB_INSTANCE_ENV_VAR]; - process.env[TEST_RTDB_INSTANCE_ENV_VAR] = RESOLVED_RTDB_INSTANCE; - try { - const p = params.defineString(TEST_RTDB_INSTANCE_ENV_VAR); - expect(database.getOpts({ ref: "/foo", instance: p })).to.deep.include({ - path: "foo", - instance: RESOLVED_RTDB_INSTANCE, - }); - } finally { - if (prev === undefined) delete process.env[TEST_RTDB_INSTANCE_ENV_VAR]; - else process.env[TEST_RTDB_INSTANCE_ENV_VAR] = prev; - } + it("should preserve instance Expression", () => { + const p = params.defineString(TEST_RTDB_INSTANCE_ENV_VAR); + + expect(database.getOpts({ ref: "/foo", instance: p })).to.deep.include({ + path: "foo", + instance: p, + }); }); }); @@ -228,6 +232,37 @@ describe("database", () => { }, }); }); + + it("should create an endpoint with an instance Expression as a path pattern", () => { + const instance = params.defineString(TEST_RTDB_INSTANCE_ENV_VAR); + const ep = database.makeEndpoint( + database.writtenEventType, + { + region: "us-central1", + labels: { 1: "2" }, + }, + new PathPattern("foo/bar"), + instance + ); + + expect(ep).to.deep.equal({ + ...MINIMAL_V2_ENDPOINT, + platform: "gcfv2", + labels: { + 1: "2", + }, + region: ["us-central1"], + eventTrigger: { + eventType: database.writtenEventType, + eventFilters: {}, + eventFilterPathPatterns: { + ref: "foo/bar", + instance, + }, + retry: false, + }, + }); + }); }); describe("onChangedOperation", () => { @@ -562,6 +597,66 @@ describe("database", () => { }, }); }); + + it("should create a function with instance Expression without deployment warning", () => { + process.env.FUNCTIONS_CONTROL_API = "true"; + const warnSpy = sinon.spy(logger, "warn"); + const instance = params.defineString(TEST_RTDB_INSTANCE_ENV_VAR); + + const func = database.onValueCreated( + { + ref: "/foo/{bar}/", + instance, + }, + () => 2 + ); + + expect(warnSpy.callCount).to.equal(0); + expect(func.__endpoint.eventTrigger.eventFilters).to.deep.equal({}); + expect(func.__endpoint.eventTrigger.eventFilterPathPatterns).to.deep.equal({ + ref: "foo/{bar}", + instance, + }); + + const wire = stackToWire({ + specVersion: "v1alpha1", + requiredAPIs: [], + endpoints: { + exprInstance: func.__endpoint, + }, + }) as any; + + expect(wire.endpoints.exprInstance.eventTrigger.eventFilterPathPatterns.instance).to.equal( + `{{ params.${TEST_RTDB_INSTANCE_ENV_VAR} }}` + ); + expect(warnSpy.callCount).to.equal(0); + }); + + it("should resolve instance Expression at runtime", async () => { + process.env[TEST_RTDB_INSTANCE_ENV_VAR] = RESOLVED_RTDB_INSTANCE; + const instance = params.defineString(TEST_RTDB_INSTANCE_ENV_VAR); + const valueSpy = sinon.spy(instance, "value"); + let capturedEvent: database.DatabaseEvent; + + const func = database.onValueCreated( + { + ref: "/foo/{bar}/", + instance, + }, + (event) => { + capturedEvent = event; + } + ); + + await func({ + ...RAW_RTDB_EVENT, + instance: RESOLVED_RTDB_INSTANCE, + ref: "foo/bar-value", + } as any); + + expect(valueSpy.callCount).to.equal(1); + expect(capturedEvent.params).to.deep.equal({ bar: "bar-value" }); + }); }); describe("onValueUpdated", () => { diff --git a/src/v2/providers/database.ts b/src/v2/providers/database.ts index 16b3b0dc0..238be707b 100644 --- a/src/v2/providers/database.ts +++ b/src/v2/providers/database.ts @@ -480,7 +480,7 @@ export function onValueDeleted( /** @internal */ export function getOpts(referenceOrOpts: string | ReferenceOptions) { let path: string; - let instance: string; + let instance: string | Expression; let opts: options.EventHandlerOptions; if (typeof referenceOrOpts === "string") { path = normalizePath(referenceOrOpts); @@ -488,10 +488,7 @@ export function getOpts(referenceOrOpts: string | ReferenceOptions) { opts = {}; } else { path = normalizePath(referenceOrOpts.ref); - instance = - referenceOrOpts.instance instanceof Expression - ? referenceOrOpts.instance.value() - : referenceOrOpts.instance || "*"; + instance = referenceOrOpts.instance || "*"; opts = { ...referenceOrOpts }; delete (opts as any).ref; delete (opts as any).instance; @@ -601,17 +598,19 @@ export function makeEndpoint( eventType: string, opts: options.EventHandlerOptions, path: PathPattern, - instance: PathPattern + instance: PathPattern | Expression ): ManifestEndpoint { const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); const specificOpts = options.optionsToEndpoint(opts); - const eventFilters: Record = {}; - const eventFilterPathPatterns: Record = { + const eventFilters: Record> = {}; + const eventFilterPathPatterns: Record> = { // Note: Eventarc always treats ref as a path pattern ref: path.getValue(), }; - if (instance.hasWildcards()) { + if (instance instanceof Expression) { + eventFilterPathPatterns.instance = instance; + } else if (instance.hasWildcards()) { eventFilterPathPatterns.instance = instance.getValue(); } else { eventFilters.instance = instance.getValue(); @@ -635,6 +634,10 @@ export function makeEndpoint( }; } +function resolveInstancePattern(instance: PathPattern | Expression): PathPattern { + return instance instanceof Expression ? new PathPattern(instance.value()) : instance; +} + /** @internal */ export function onChangedOperation( eventType: string, @@ -644,13 +647,18 @@ export function onChangedOperation( const { path, instance, opts } = getOpts(referenceOrOpts); const pathPattern = new PathPattern(path); - const instancePattern = new PathPattern(instance); + const instancePattern = instance instanceof Expression ? instance : new PathPattern(instance); // wrap the handler const func = (raw: CloudEvent) => { const event = raw as RawRTDBCloudEvent; const instanceUrl = getInstance(event); - const params = makeParams(event, pathPattern, instancePattern) as unknown as ParamsOf; + const resolvedInstancePattern = resolveInstancePattern(instancePattern); + const params = makeParams( + event, + pathPattern, + resolvedInstancePattern + ) as unknown as ParamsOf; const databaseEvent = makeChangedDatabaseEvent(event, instanceUrl, params); const compatEvent = addV1Compat(databaseEvent, { @@ -679,13 +687,18 @@ export function onOperation( const { path, instance, opts } = getOpts(referenceOrOpts); const pathPattern = new PathPattern(path); - const instancePattern = new PathPattern(instance); + const instancePattern = instance instanceof Expression ? instance : new PathPattern(instance); // wrap the handler const func = (raw: CloudEvent) => { const event = raw as RawRTDBCloudEvent; const instanceUrl = getInstance(event); - const params = makeParams(event, pathPattern, instancePattern) as unknown as ParamsOf; + const resolvedInstancePattern = resolveInstancePattern(instancePattern); + const params = makeParams( + event, + pathPattern, + resolvedInstancePattern + ) as unknown as ParamsOf; const data = eventType === deletedEventType ? event.data.data : event.data.delta; const databaseEvent = makeDatabaseEvent(event, data, instanceUrl, params);