Skip to content
114 changes: 114 additions & 0 deletions spec/v2/providers/database.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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 = {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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<any, { bar: string }>;

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", () => {
Expand Down
36 changes: 26 additions & 10 deletions src/v2/providers/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export interface ReferenceOptions<Ref extends string = string> extends options.E
* Examples: 'my-instance-1', 'my-instance-*'
* Note: The capture syntax cannot be used for 'instance'.
*/
instance?: string;
instance?: string | Expression<string>;

/**
* If true, do not deploy or emulate this function.
Expand Down Expand Up @@ -480,7 +480,7 @@ export function onValueDeleted<Ref extends string>(
/** @internal */
export function getOpts(referenceOrOpts: string | ReferenceOptions) {
let path: string;
let instance: string;
let instance: string | Expression<string>;
let opts: options.EventHandlerOptions;
if (typeof referenceOrOpts === "string") {
path = normalizePath(referenceOrOpts);
Expand Down Expand Up @@ -598,17 +598,19 @@ export function makeEndpoint(
eventType: string,
opts: options.EventHandlerOptions,
path: PathPattern,
instance: PathPattern
instance: PathPattern | Expression<string>
): ManifestEndpoint {
const baseOpts = options.optionsToEndpoint(options.getGlobalOptions());
const specificOpts = options.optionsToEndpoint(opts, "event");

const eventFilters: Record<string, string> = {};
const eventFilterPathPatterns: Record<string, string> = {
const eventFilters: Record<string, string | Expression<string>> = {};
const eventFilterPathPatterns: Record<string, string | Expression<string>> = {
// 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();
Expand All @@ -632,6 +634,10 @@ export function makeEndpoint(
};
}

function resolveInstancePattern(instance: PathPattern | Expression<string>): PathPattern {
return instance instanceof Expression ? new PathPattern(instance.value()) : instance;
}

/** @internal */
export function onChangedOperation<Ref extends string>(
eventType: string,
Expand All @@ -641,13 +647,18 @@ export function onChangedOperation<Ref extends string>(
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<unknown>) => {
const event = raw as RawRTDBCloudEvent;
const instanceUrl = getInstance(event);
const params = makeParams(event, pathPattern, instancePattern) as unknown as ParamsOf<Ref>;
const resolvedInstancePattern = resolveInstancePattern(instancePattern);
const params = makeParams(
event,
pathPattern,
resolvedInstancePattern
) as unknown as ParamsOf<Ref>;
const databaseEvent = makeChangedDatabaseEvent(event, instanceUrl, params);

const compatEvent = addV1Compat(databaseEvent, {
Expand Down Expand Up @@ -676,13 +687,18 @@ export function onOperation<Ref extends string>(
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<unknown>) => {
const event = raw as RawRTDBCloudEvent;
const instanceUrl = getInstance(event);
const params = makeParams(event, pathPattern, instancePattern) as unknown as ParamsOf<Ref>;
const resolvedInstancePattern = resolveInstancePattern(instancePattern);
const params = makeParams(
event,
pathPattern,
resolvedInstancePattern
) as unknown as ParamsOf<Ref>;
const data = eventType === deletedEventType ? event.data.data : event.data.delta;
const databaseEvent = makeDatabaseEvent(event, data, instanceUrl, params);

Expand Down
Loading