diff --git a/spec/v2/providers/database.spec.ts b/spec/v2/providers/database.spec.ts index 4435faeea..b17d1363e 100644 --- a/spec/v2/providers/database.spec.ts +++ b/spec/v2/providers/database.spec.ts @@ -21,11 +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: { @@ -46,7 +50,17 @@ 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", () => { + 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 = { @@ -146,6 +160,15 @@ describe("database", () => { }, }); }); + + 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, + }); + }); }); describe("makeEndpoint", () => { @@ -209,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", () => { @@ -543,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 6c5f5e83c..238be707b 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. @@ -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); @@ -598,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(); @@ -632,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, @@ -641,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, { @@ -676,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);