diff --git a/spec/v1/cloud-functions.spec.ts b/spec/v1/cloud-functions.spec.ts index f68269030..3a9c99b67 100644 --- a/spec/v1/cloud-functions.spec.ts +++ b/spec/v1/cloud-functions.spec.ts @@ -41,7 +41,7 @@ describe("makeCloudFunction", () => { legacyEventType: "providers/provider/eventTypes/event", }; - it("should put a __endpoint on the returned CloudFunction", () => { + it("should put a __trigger/__endpoint on the returned CloudFunction", () => { const cf = makeCloudFunction({ provider: "mock.provider", eventType: "mock.event", @@ -50,6 +50,14 @@ describe("makeCloudFunction", () => { handler: () => null, }); + expect(cf.__trigger).to.deep.equal({ + eventTrigger: { + eventType: "mock.provider.mock.event", + resource: "resource", + service: "service", + }, + }); + expect(cf.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -64,9 +72,17 @@ describe("makeCloudFunction", () => { }); }); - it("should have legacy event type in __endpoint if provided", () => { + it("should have legacy event type in __trigger/__endpoint if provided", () => { const cf = makeCloudFunction(cloudFunctionArgs); + expect(cf.__trigger).to.deep.equal({ + eventTrigger: { + eventType: "providers/provider/eventTypes/event", + resource: "resource", + service: "service", + }, + }); + expect(cf.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", diff --git a/spec/v1/function-builder.spec.ts b/spec/v1/function-builder.spec.ts index da041e8f5..955a0ab38 100644 --- a/spec/v1/function-builder.spec.ts +++ b/spec/v1/function-builder.spec.ts @@ -24,7 +24,6 @@ import { expect } from "chai"; import { clearParams, defineSecret } from "../../src/params"; import * as functions from "../../src/v1"; -import { ResetValue } from "../../src/common/options"; describe("FunctionBuilder", () => { before(() => { @@ -41,6 +40,7 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); + expect(fn.__trigger.regions).to.deep.equal(["us-east1"]); expect(fn.__endpoint.region).to.deep.equal(["us-east1"]); }); @@ -50,6 +50,7 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); + expect(fn.__trigger.regions).to.deep.equal(["us-east1", "us-central1"]); expect(fn.__endpoint.region).to.deep.equal(["us-east1", "us-central1"]); }); @@ -68,6 +69,17 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); + expect(fn.__trigger.regions).to.deep.equal([ + "us-central1", + "us-east1", + "us-east4", + "europe-west1", + "europe-west2", + "europe-west3", + "asia-east2", + "asia-northeast1", + ]); + expect(fn.__endpoint.region).to.deep.equal([ "us-central1", "us-east1", @@ -93,6 +105,8 @@ describe("FunctionBuilder", () => { expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); expect(fn.__endpoint.eventTrigger.retry).to.deep.equal(true); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal("90s"); }); it("should allow SecretParams in the secrets array and convert them", () => { @@ -104,6 +118,11 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); + expect(fn.__trigger.secrets).to.deep.equal([ + { + name: "API_KEY", + }, + ]); expect(fn.__endpoint.secretEnvironmentVariables).to.deep.equal([ { key: "API_KEY", @@ -136,6 +155,9 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); + expect(fn.__trigger.regions).to.deep.equal(["europe-west2"]); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal("90s"); expect(fn.__endpoint.region).to.deep.equal(["europe-west2"]); expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); @@ -151,6 +173,9 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); + expect(fn.__trigger.regions).to.deep.equal(["europe-west1"]); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal("90s"); expect(fn.__endpoint.region).to.deep.equal(["europe-west1"]); expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); @@ -223,6 +248,7 @@ describe("FunctionBuilder", () => { .runWith({ ingressSettings: "ALLOW_INTERNAL_ONLY" }) .https.onRequest(() => undefined); + expect(fn.__trigger.ingressSettings).to.equal("ALLOW_INTERNAL_ONLY"); expect(fn.__endpoint.ingressSettings).to.equal("ALLOW_INTERNAL_ONLY"); }); @@ -245,11 +271,8 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); - if (!(fn.__endpoint.vpc instanceof ResetValue)) { - expect(fn.__endpoint.vpc.connector).to.equal("test-connector"); - } else { - expect.fail("__endpoint.vpc unexpectedly set to RESET_VALUE"); - } + expect(fn.__trigger.vpcConnector).to.equal("test-connector"); + expect(fn.__endpoint.vpc).to.deep.equal({ connector: "test-connector" }); }); it("should allow a vpcConnectorEgressSettings to be set", () => { @@ -261,11 +284,11 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); - if (!(fn.__endpoint.vpc instanceof ResetValue)) { - expect(fn.__endpoint.vpc.egressSettings).to.equal("PRIVATE_RANGES_ONLY"); - } else { - expect.fail("__endpoint.vpc unexpectedly set to RESET_VALUE"); - } + expect(fn.__trigger.vpcConnectorEgressSettings).to.equal("PRIVATE_RANGES_ONLY"); + expect(fn.__endpoint.vpc).to.deep.equal({ + connector: "test-connector", + egressSettings: "PRIVATE_RANGES_ONLY", + }); }); it("should throw an error if user chooses an invalid vpcConnectorEgressSettings", () => { @@ -292,10 +315,12 @@ describe("FunctionBuilder", () => { .onCreate((user) => user); expect(fn.__endpoint.serviceAccountEmail).to.equal(serviceAccount); + expect(fn.__trigger.serviceAccountEmail).to.equal(serviceAccount); }); it("should allow a serviceAccount to be set with generated service account email", () => { const serviceAccount = "test-service-account@"; + const projectId = process.env.GCLOUD_PROJECT; const fn = functions .runWith({ serviceAccount, @@ -303,6 +328,9 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); + expect(fn.__trigger.serviceAccountEmail).to.equal( + `test-service-account@${projectId}.iam.gserviceaccount.com` + ); expect(fn.__endpoint.serviceAccountEmail).to.equal(`test-service-account@`); }); @@ -315,7 +343,8 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); - expect(fn.__endpoint.serviceAccountEmail).to.equal("default"); + expect(fn.__trigger.serviceAccountEmail).to.be.null; + expect(fn.__endpoint.serviceAccountEmail).to.equal(serviceAccount); }); it("should throw an error if serviceAccount is set to an invalid value", () => { @@ -337,6 +366,7 @@ describe("FunctionBuilder", () => { .onCreate((user) => user); expect(fn.__endpoint.availableMemoryMb).to.deep.equal(4096); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(4096); }); it("should allow labels to be set", () => { @@ -349,6 +379,9 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); + expect(fn.__trigger.labels).to.deep.equal({ + "valid-key": "valid-value", + }); expect(fn.__endpoint.labels).to.deep.equal({ "valid-key": "valid-value", }); @@ -504,22 +537,20 @@ describe("FunctionBuilder", () => { .auth.user() .onCreate((user) => user); - expect(fn.__endpoint.secretEnvironmentVariables).to.deep.equal([ - { - key: "API_KEY", - }, - ]); + expect(fn.__trigger.secrets).to.deep.equal(secrets); + expect(fn.__endpoint.secretEnvironmentVariables).to.deep.equal([{ key: secrets[0] }]); }); it("should throw error given secrets expressed with full resource name", () => { - const sp = defineSecret("projects/my-project/secrets/API_KEY"); - expect(() => functions.runWith({ secrets: ["projects/my-project/secrets/API_KEY"], }) ).to.throw(); + }); + it("should throw error given invalid secret config", () => { + const sp = defineSecret("projects/my-project/secrets/API_KEY"); expect(() => functions.runWith({ secrets: [sp], diff --git a/spec/v1/providers/analytics.spec.ts b/spec/v1/providers/analytics.spec.ts index d7172360a..90a617686 100644 --- a/spec/v1/providers/analytics.spec.ts +++ b/spec/v1/providers/analytics.spec.ts @@ -48,6 +48,10 @@ describe("Analytics Functions", () => { .analytics.event("event") .onLog((event) => event); + expect(fn.__trigger.regions).to.deep.equal(["us-east1"]); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal("90s"); + expect(fn.__endpoint.region).to.deep.equal(["us-east1"]); expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); @@ -57,6 +61,14 @@ describe("Analytics Functions", () => { it("should return a trigger/endpoint with appropriate values", () => { const cloudFunction = analytics.event("first_open").onLog(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: "providers/google.firebase.analytics/eventTypes/event.log", + resource: "projects/project1/events/first_open", + service: "app-measurement.com", + }, + }); + expect(cloudFunction.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -293,7 +305,7 @@ describe("Analytics Functions", () => { }); describe("process.env.GCLOUD_PROJECT not set", () => { - it("should not throw if __endpoint is not accessed", () => { + it("should not throw if __trigger is not accessed", () => { expect(() => analytics.event("event").onLog(() => null)).to.not.throw(Error); }); @@ -301,6 +313,10 @@ describe("Analytics Functions", () => { expect(() => analytics.event("event").onLog(() => null).__endpoint).to.throw(Error); }); + it("should throw when trigger is accessed", () => { + expect(() => analytics.event("event").onLog(() => null).__trigger).to.throw(Error); + }); + it("should not throw when #run is called", () => { const cf = analytics.event("event").onLog(() => null); diff --git a/spec/v1/providers/auth.spec.ts b/spec/v1/providers/auth.spec.ts index b9604c37a..f5f6a806d 100644 --- a/spec/v1/providers/auth.spec.ts +++ b/spec/v1/providers/auth.spec.ts @@ -47,6 +47,16 @@ describe("Auth Functions", () => { }; describe("AuthBuilder", () => { + function expectedTrigger(project: string, eventType: string) { + return { + eventTrigger: { + resource: `projects/${project}`, + eventType: `providers/firebase.auth/eventTypes/${eventType}`, + service: "firebaseauth.googleapis.com", + }, + }; + } + function expectedEndpoint(project: string, eventType: string) { return { ...MINIMAL_V1_ENDPOINT, @@ -86,9 +96,9 @@ describe("Auth Functions", () => { .auth.user() .onCreate(() => null); - expect(fn.__endpoint.region).to.deep.equal(["us-east1"]); - expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); - expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); + expect(fn.__trigger.regions).to.deep.equal(["us-east1"]); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal("90s"); expect(fn.__endpoint.region).to.deep.equal(["us-east1"]); expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); @@ -99,6 +109,8 @@ describe("Auth Functions", () => { it("should return a trigger/endpoint with appropriate values", () => { const cloudFunction = auth.user().onCreate(() => null); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(project, "user.create")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint(project, "user.create")); }); }); @@ -107,6 +119,8 @@ describe("Auth Functions", () => { it("should return a trigger/endpoint with appropriate values", () => { const cloudFunction = auth.user().onDelete(handler); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(project, "user.delete")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint(project, "user.delete")); }); }); @@ -115,6 +129,17 @@ describe("Auth Functions", () => { it("should create the function without options", () => { const fn = auth.user().beforeCreate(() => Promise.resolve()); + expect(fn.__trigger).to.deep.equal({ + labels: {}, + blockingTrigger: { + eventType: "providers/cloud.auth/eventTypes/user.beforeCreate", + options: { + accessToken: false, + idToken: false, + refreshToken: false, + }, + }, + }); expect(fn.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -151,6 +176,20 @@ describe("Auth Functions", () => { }) .beforeCreate(() => Promise.resolve()); + expect(fn.__trigger).to.deep.equal({ + labels: {}, + regions: ["us-east1"], + availableMemoryMb: 256, + timeout: "90s", + blockingTrigger: { + eventType: "providers/cloud.auth/eventTypes/user.beforeCreate", + options: { + accessToken: true, + idToken: false, + refreshToken: false, + }, + }, + }); expect(fn.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -180,6 +219,17 @@ describe("Auth Functions", () => { it("should create the function without options", () => { const fn = auth.user().beforeSignIn(() => Promise.resolve()); + expect(fn.__trigger).to.deep.equal({ + labels: {}, + blockingTrigger: { + eventType: "providers/cloud.auth/eventTypes/user.beforeSignIn", + options: { + accessToken: false, + idToken: false, + refreshToken: false, + }, + }, + }); expect(fn.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -216,6 +266,20 @@ describe("Auth Functions", () => { }) .beforeSignIn(() => Promise.resolve()); + expect(fn.__trigger).to.deep.equal({ + labels: {}, + regions: ["us-east1"], + availableMemoryMb: 256, + timeout: "90s", + blockingTrigger: { + eventType: "providers/cloud.auth/eventTypes/user.beforeSignIn", + options: { + accessToken: true, + idToken: false, + refreshToken: false, + }, + }, + }); expect(fn.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -258,10 +322,14 @@ describe("Auth Functions", () => { }); describe("process.env.GCLOUD_PROJECT not set", () => { - it("should not throw if __endpoint is not accessed", () => { + it("should not throw if __trigger is not accessed", () => { expect(() => auth.user().onCreate(() => null)).to.not.throw(Error); }); + it("should throw when trigger is accessed", () => { + expect(() => auth.user().onCreate(() => null).__trigger).to.throw(Error); + }); + it("should throw when endpoint is accessed", () => { expect(() => auth.user().onCreate(() => null).__endpoint).to.throw(Error); }); diff --git a/spec/v1/providers/database.spec.ts b/spec/v1/providers/database.spec.ts index 2d1368d59..9651961dc 100644 --- a/spec/v1/providers/database.spec.ts +++ b/spec/v1/providers/database.spec.ts @@ -33,6 +33,16 @@ describe("Database Functions", () => { describe("DatabaseBuilder", () => { // TODO add tests for building a data or change based on the type of operation + function expectedTrigger(resource: string, eventType: string) { + return { + eventTrigger: { + resource, + eventType: `providers/google.firebase.database/eventTypes/${eventType}`, + service: "firebaseio.com", + }, + }; + } + function expectedEndpoint(resource: string, eventType: string) { return { ...MINIMAL_V1_ENDPOINT, @@ -69,9 +79,9 @@ describe("Database Functions", () => { .database.ref("/") .onCreate((snap) => snap); - expect(fn.__endpoint.region).to.deep.equal(["us-east1"]); - expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); - expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); + expect(fn.__trigger.regions).to.deep.equal(["us-east1"]); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal("90s"); expect(fn.__endpoint.region).to.deep.equal(["us-east1"]); expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); @@ -79,9 +89,13 @@ describe("Database Functions", () => { }); describe("#onWrite()", () => { - it("should return a endpoint with appropriate values", () => { + it("should return a trigger/endpoint with appropriate values", () => { const func = database.ref("foo").onWrite(() => null); + expect(func.__trigger).to.deep.equal( + expectedTrigger("projects/_/instances/subdomain/refs/foo", "ref.write") + ); + expect(func.__endpoint).to.deep.equal( expectedEndpoint("projects/_/instances/subdomain/refs/foo", "ref.write") ); @@ -93,6 +107,10 @@ describe("Database Functions", () => { .ref("foo") .onWrite(() => null); + expect(func.__trigger).to.deep.equal( + expectedTrigger("projects/_/instances/custom/refs/foo", "ref.write") + ); + expect(func.__endpoint).to.deep.equal( expectedEndpoint("projects/_/instances/custom/refs/foo", "ref.write") ); @@ -135,6 +153,10 @@ describe("Database Functions", () => { it("should return a trigger/endpoint with appropriate values", () => { const func = database.ref("foo").onCreate(() => null); + expect(func.__trigger).to.deep.equal( + expectedTrigger("projects/_/instances/subdomain/refs/foo", "ref.create") + ); + expect(func.__endpoint).to.deep.equal( expectedEndpoint("projects/_/instances/subdomain/refs/foo", "ref.create") ); @@ -146,6 +168,10 @@ describe("Database Functions", () => { .ref("foo") .onCreate(() => null); + expect(func.__trigger).to.deep.equal( + expectedTrigger("projects/_/instances/custom/refs/foo", "ref.create") + ); + expect(func.__endpoint).to.deep.equal( expectedEndpoint("projects/_/instances/custom/refs/foo", "ref.create") ); @@ -189,6 +215,10 @@ describe("Database Functions", () => { it("should return a trigger/endpoint with appropriate values", () => { const func = database.ref("foo").onUpdate(() => null); + expect(func.__trigger).to.deep.equal( + expectedTrigger("projects/_/instances/subdomain/refs/foo", "ref.update") + ); + expect(func.__endpoint).to.deep.equal( expectedEndpoint("projects/_/instances/subdomain/refs/foo", "ref.update") ); @@ -200,6 +230,10 @@ describe("Database Functions", () => { .ref("foo") .onUpdate(() => null); + expect(func.__trigger).to.deep.equal( + expectedTrigger("projects/_/instances/custom/refs/foo", "ref.update") + ); + expect(func.__endpoint).to.deep.equal( expectedEndpoint("projects/_/instances/custom/refs/foo", "ref.update") ); @@ -243,6 +277,10 @@ describe("Database Functions", () => { it("should return a trigger/endpoint with appropriate values", () => { const func = database.ref("foo").onDelete(() => null); + expect(func.__trigger).to.deep.equal( + expectedTrigger("projects/_/instances/subdomain/refs/foo", "ref.delete") + ); + expect(func.__endpoint).to.deep.equal( expectedEndpoint("projects/_/instances/subdomain/refs/foo", "ref.delete") ); @@ -254,6 +292,10 @@ describe("Database Functions", () => { .ref("foo") .onDelete(() => null); + expect(func.__trigger).to.deep.equal( + expectedTrigger("projects/_/instances/custom/refs/foo", "ref.delete") + ); + expect(func.__endpoint).to.deep.equal( expectedEndpoint("projects/_/instances/custom/refs/foo", "ref.delete") ); @@ -279,34 +321,38 @@ describe("Database Functions", () => { return handler(event.data, event.context); }); - }); - it("Should have params of the correct type", () => { - database.ref("foo").onDelete((event, context) => { - expectType>(context.params); - }); - database.ref("foo/{bar}").onDelete((event, context) => { - expectType<{ bar: string }>(context.params); - }); - database.ref("foo/{bar}/{baz}").onDelete((event, context) => { - expectType<{ bar: string; baz: string }>(context.params); + it("Should have params of the correct type", () => { + database.ref("foo").onDelete((event, context) => { + expectType>(context.params); + }); + database.ref("foo/{bar}").onDelete((event, context) => { + expectType<{ bar: string }>(context.params); + }); + database.ref("foo/{bar}/{baz}").onDelete((event, context) => { + expectType<{ bar: string; baz: string }>(context.params); + }); }); }); }); describe("process.env.FIREBASE_CONFIG not set", () => { - it("should not throw if __endpoint is not accessed", () => { + it("should not throw if __trigger is not accessed", () => { expect(() => database.ref("/path").onWrite(() => null)).to.not.throw(Error); }); + }); - it("should throw when endpoint is accessed", () => { - expect(() => database.ref("/path").onWrite(() => null).__endpoint).to.throw(Error); - }); + it("should throw when trigger is accessed", () => { + expect(() => database.ref("/path").onWrite(() => null).__trigger).to.throw(Error); + }); - it("should not throw when #run is called", () => { - const cf = database.ref("/path").onWrite(() => null); - expect(cf.run).to.not.throw(Error); - }); + it("should throw when endpoint is accessed", () => { + expect(() => database.ref("/path").onWrite(() => null).__endpoint).to.throw(Error); + }); + + it("should not throw when #run is called", () => { + const cf = database.ref("/path").onWrite(() => null); + expect(cf.run).to.not.throw(Error); }); describe("extractInstanceAndPath", () => { @@ -382,346 +428,346 @@ describe("Database Functions", () => { delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; }); }); +}); - describe("DataSnapshot", () => { - let subject: any; - - const populate = (data: any) => { - const [instance, path] = database.extractInstanceAndPath( - "projects/_/instances/other-subdomain/refs/foo", - "firebaseio-staging.com" - ); - subject = new database.DataSnapshot(data, path, getApp(), instance); - }; - - describe("#ref: firebase.database.Reference", () => { - it("should return a ref for correct instance, not the default instance", () => { - populate({}); - expect(subject.ref.toJSON()).to.equal("https://other-subdomain.firebaseio-staging.com/foo"); - }); +describe("DataSnapshot", () => { + let subject: any; + + const populate = (data: any) => { + const [instance, path] = database.extractInstanceAndPath( + "projects/_/instances/other-subdomain/refs/foo", + "firebaseio-staging.com" + ); + subject = new database.DataSnapshot(data, path, getApp(), instance); + }; + + describe("#ref: firebase.database.Reference", () => { + it("should return a ref for correct instance, not the default instance", () => { + populate({}); + expect(subject.ref.toJSON()).to.equal("https://other-subdomain.firebaseio-staging.com/foo"); }); + }); - describe("#val(): any", () => { - it("should return child values based on the child path", () => { - populate(applyChange({ a: { b: "c" } }, { a: { d: "e" } })); - expect(subject.child("a").val()).to.deep.equal({ b: "c", d: "e" }); - }); + describe("#val(): any", () => { + it("should return child values based on the child path", () => { + populate(applyChange({ a: { b: "c" } }, { a: { d: "e" } })); + expect(subject.child("a").val()).to.deep.equal({ b: "c", d: "e" }); + }); - it("should return null for children past a leaf", () => { - populate(applyChange({ a: 23 }, { b: 33 })); - expect(subject.child("a/b").val()).to.be.null; - expect(subject.child("b/c").val()).to.be.null; - }); + it("should return null for children past a leaf", () => { + populate(applyChange({ a: 23 }, { b: 33 })); + expect(subject.child("a/b").val()).to.be.null; + expect(subject.child("b/c").val()).to.be.null; + }); - it("should return a leaf value", () => { - populate(23); - expect(subject.val()).to.eq(23); - populate({ b: 23, a: null }); - expect(subject.child("b").val()).to.eq(23); - }); + it("should return a leaf value", () => { + populate(23); + expect(subject.val()).to.eq(23); + populate({ b: 23, a: null }); + expect(subject.child("b").val()).to.eq(23); + }); - it("should coerce object into array if all keys are integers", () => { - populate({ 0: "a", 1: "b", 2: { c: "d" } }); - expect(subject.val()).to.deep.equal(["a", "b", { c: "d" }]); - populate({ 0: "a", 2: "b", 3: { c: "d" } }); - expect(subject.val()).to.deep.equal(["a", undefined, "b", { c: "d" }]); - populate({ foo: { 0: "a", 1: "b" } }); - expect(subject.val()).to.deep.equal({ foo: ["a", "b"] }); - }); + it("should coerce object into array if all keys are integers", () => { + populate({ 0: "a", 1: "b", 2: { c: "d" } }); + expect(subject.val()).to.deep.equal(["a", "b", { c: "d" }]); + populate({ 0: "a", 2: "b", 3: { c: "d" } }); + expect(subject.val()).to.deep.equal(["a", undefined, "b", { c: "d" }]); + populate({ foo: { 0: "a", 1: "b" } }); + expect(subject.val()).to.deep.equal({ foo: ["a", "b"] }); + }); - // Regression test: zero-values (including children) were accidentally forwarded as 'null'. - it("should deal with zero-values appropriately", () => { - populate(0); - expect(subject.val()).to.equal(0); - populate({ myKey: 0 }); - expect(subject.val()).to.deep.equal({ myKey: 0 }); - }); + // Regression test: zero-values (including children) were accidentally forwarded as 'null'. + it("should deal with zero-values appropriately", () => { + populate(0); + expect(subject.val()).to.equal(0); + populate({ myKey: 0 }); + expect(subject.val()).to.deep.equal({ myKey: 0 }); + }); - // Regression test: .val() was returning array of nulls when there's a property called length (BUG#37683995) - it('should return correct values when data has "length" property', () => { - populate({ length: 3, foo: "bar" }); - expect(subject.val()).to.deep.equal({ length: 3, foo: "bar" }); - }); + // Regression test: .val() was returning array of nulls when there's a property called length (BUG#37683995) + it('should return correct values when data has "length" property', () => { + populate({ length: 3, foo: "bar" }); + expect(subject.val()).to.deep.equal({ length: 3, foo: "bar" }); + }); - it("should deal with null-values appropriately", () => { - populate(null); - expect(subject.val()).to.be.null; + it("should deal with null-values appropriately", () => { + populate(null); + expect(subject.val()).to.be.null; - populate({ myKey: null }); - expect(subject.val()).to.be.null; - }); + populate({ myKey: null }); + expect(subject.val()).to.be.null; + }); - it("should deal with empty object values appropriately", () => { - populate({}); - expect(subject.val()).to.be.null; + it("should deal with empty object values appropriately", () => { + populate({}); + expect(subject.val()).to.be.null; - populate({ myKey: {} }); - expect(subject.val()).to.be.null; + populate({ myKey: {} }); + expect(subject.val()).to.be.null; - populate({ myKey: { child: null } }); - expect(subject.val()).to.be.null; - }); + populate({ myKey: { child: null } }); + expect(subject.val()).to.be.null; + }); - it("should deal with empty array values appropriately", () => { - populate([]); - expect(subject.val()).to.be.null; + it("should deal with empty array values appropriately", () => { + populate([]); + expect(subject.val()).to.be.null; - populate({ myKey: [] }); - expect(subject.val()).to.be.null; + populate({ myKey: [] }); + expect(subject.val()).to.be.null; - populate({ myKey: [null] }); - expect(subject.val()).to.be.null; + populate({ myKey: [null] }); + expect(subject.val()).to.be.null; - populate({ myKey: [{}] }); - expect(subject.val()).to.be.null; + populate({ myKey: [{}] }); + expect(subject.val()).to.be.null; - populate({ myKey: [{ myKey: null }] }); - expect(subject.val()).to.be.null; + populate({ myKey: [{ myKey: null }] }); + expect(subject.val()).to.be.null; - populate({ myKey: [{ myKey: {} }] }); - expect(subject.val()).to.be.null; - }); + populate({ myKey: [{ myKey: {} }] }); + expect(subject.val()).to.be.null; }); + }); - describe("#child(): DataSnapshot", () => { - it("should work with multiple calls", () => { - populate({ a: { b: { c: "d" } } }); - expect(subject.child("a").child("b/c").val()).to.equal("d"); - }); + describe("#child(): DataSnapshot", () => { + it("should work with multiple calls", () => { + populate({ a: { b: { c: "d" } } }); + expect(subject.child("a").child("b/c").val()).to.equal("d"); }); + }); - describe("#exists(): boolean", () => { - it("should be true for an object value", () => { - populate({ a: { b: "c" } }); - expect(subject.child("a").exists()).to.be.true; - }); + describe("#exists(): boolean", () => { + it("should be true for an object value", () => { + populate({ a: { b: "c" } }); + expect(subject.child("a").exists()).to.be.true; + }); - it("should be true for a leaf value", () => { - populate({ a: { b: "c" } }); - expect(subject.child("a/b").exists()).to.be.true; - }); + it("should be true for a leaf value", () => { + populate({ a: { b: "c" } }); + expect(subject.child("a/b").exists()).to.be.true; + }); - it("should be false for a non-existent value", () => { - populate({ a: { b: "c", nullChild: null } }); - expect(subject.child("d").exists()).to.be.false; - expect(subject.child("nullChild").exists()).to.be.false; - }); + it("should be false for a non-existent value", () => { + populate({ a: { b: "c", nullChild: null } }); + expect(subject.child("d").exists()).to.be.false; + expect(subject.child("nullChild").exists()).to.be.false; + }); - it("should be false for a value pathed beyond a leaf", () => { - populate({ a: { b: "c" } }); - expect(subject.child("a/b/c").exists()).to.be.false; - }); + it("should be false for a value pathed beyond a leaf", () => { + populate({ a: { b: "c" } }); + expect(subject.child("a/b/c").exists()).to.be.false; + }); - it("should be false for an empty object value", () => { - populate({ a: {} }); - expect(subject.child("a").exists()).to.be.false; + it("should be false for an empty object value", () => { + populate({ a: {} }); + expect(subject.child("a").exists()).to.be.false; - populate({ a: { child: null } }); - expect(subject.child("a").exists()).to.be.false; + populate({ a: { child: null } }); + expect(subject.child("a").exists()).to.be.false; - populate({ a: { child: {} } }); - expect(subject.child("a").exists()).to.be.false; - }); + populate({ a: { child: {} } }); + expect(subject.child("a").exists()).to.be.false; + }); - it("should be false for an empty array value", () => { - populate({ a: [] }); - expect(subject.child("a").exists()).to.be.false; + it("should be false for an empty array value", () => { + populate({ a: [] }); + expect(subject.child("a").exists()).to.be.false; - populate({ a: [null] }); - expect(subject.child("a").exists()).to.be.false; + populate({ a: [null] }); + expect(subject.child("a").exists()).to.be.false; - populate({ a: [{}] }); - expect(subject.child("a").exists()).to.be.false; - }); + populate({ a: [{}] }); + expect(subject.child("a").exists()).to.be.false; }); + }); - describe("#forEach(action: (a: DataSnapshot) => boolean): boolean", () => { - it("should iterate through child snapshots", () => { - populate({ a: "b", c: "d" }); - let out = ""; - subject.forEach((snap: any) => { - out += snap.val(); - }); - expect(out).to.equal("bd"); + describe("#forEach(action: (a: DataSnapshot) => boolean): boolean", () => { + it("should iterate through child snapshots", () => { + populate({ a: "b", c: "d" }); + let out = ""; + subject.forEach((snap: any) => { + out += snap.val(); }); + expect(out).to.equal("bd"); + }); - it("should have correct key values for child snapshots", () => { - populate({ a: "b", c: "d" }); - let out = ""; - subject.forEach((snap: any) => { - out += snap.key; - }); - expect(out).to.equal("ac"); + it("should have correct key values for child snapshots", () => { + populate({ a: "b", c: "d" }); + let out = ""; + subject.forEach((snap: any) => { + out += snap.key; }); + expect(out).to.equal("ac"); + }); - it("should not execute for leaf or null nodes", () => { - populate(23); - let count = 0; - const counter = () => count++; - - expect(subject.forEach(counter)).to.equal(false); - expect(count).to.eq(0); + it("should not execute for leaf or null nodes", () => { + populate(23); + let count = 0; + const counter = () => count++; - populate({ - a: "foo", - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); - count = 0; + expect(subject.forEach(counter)).to.equal(false); + expect(count).to.eq(0); - expect(subject.forEach(counter)).to.equal(false); - expect(count).to.eq(1); + populate({ + a: "foo", + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], }); + count = 0; - it("should cancel further enumeration if callback returns true", () => { - populate({ a: "b", c: "d", e: "f", g: "h" }); - let out = ""; - const ret = subject.forEach((snap: any) => { - if (snap.val() === "f") { - return true; - } - out += snap.val(); - }); - expect(out).to.equal("bd"); - expect(ret).to.equal(true); - }); + expect(subject.forEach(counter)).to.equal(false); + expect(count).to.eq(1); + }); - it("should not cancel further enumeration if callback returns a truthy value", () => { - populate({ a: "b", c: "d", e: "f", g: "h" }); - let out = ""; - const ret = subject.forEach((snap: any) => { - out += snap.val(); - return 1; - }); - expect(out).to.equal("bdfh"); - expect(ret).to.equal(false); + it("should cancel further enumeration if callback returns true", () => { + populate({ a: "b", c: "d", e: "f", g: "h" }); + let out = ""; + const ret = subject.forEach((snap: any) => { + if (snap.val() === "f") { + return true; + } + out += snap.val(); + }); + expect(out).to.equal("bd"); + expect(ret).to.equal(true); + }); + + it("should not cancel further enumeration if callback returns a truthy value", () => { + populate({ a: "b", c: "d", e: "f", g: "h" }); + let out = ""; + const ret = subject.forEach((snap: any) => { + out += snap.val(); + return 1; }); + expect(out).to.equal("bdfh"); + expect(ret).to.equal(false); + }); - it("should not cancel further enumeration if callback does not return", () => { - populate({ a: "b", c: "d", e: "f", g: "h" }); - let out = ""; - const ret = subject.forEach((snap: any) => { - out += snap.val(); - }); - expect(out).to.equal("bdfh"); - expect(ret).to.equal(false); + it("should not cancel further enumeration if callback does not return", () => { + populate({ a: "b", c: "d", e: "f", g: "h" }); + let out = ""; + const ret = subject.forEach((snap: any) => { + out += snap.val(); }); + expect(out).to.equal("bdfh"); + expect(ret).to.equal(false); }); + }); - describe("#numChildren()", () => { - it("should be key count for objects", () => { - populate({ - a: "b", - c: "d", - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); - expect(subject.numChildren()).to.eq(2); + describe("#numChildren()", () => { + it("should be key count for objects", () => { + populate({ + a: "b", + c: "d", + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], }); + expect(subject.numChildren()).to.eq(2); + }); - it("should be 0 for non-objects", () => { - populate(23); - expect(subject.numChildren()).to.eq(0); + it("should be 0 for non-objects", () => { + populate(23); + expect(subject.numChildren()).to.eq(0); - populate({ - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); - expect(subject.numChildren()).to.eq(0); + populate({ + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], }); + expect(subject.numChildren()).to.eq(0); }); + }); - describe("#hasChildren()", () => { - it("should true for objects", () => { - populate({ - a: "b", - c: "d", - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); - expect(subject.hasChildren()).to.be.true; + describe("#hasChildren()", () => { + it("should true for objects", () => { + populate({ + a: "b", + c: "d", + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], }); + expect(subject.hasChildren()).to.be.true; + }); - it("should be false for non-objects", () => { - populate(23); - expect(subject.hasChildren()).to.be.false; + it("should be false for non-objects", () => { + populate(23); + expect(subject.hasChildren()).to.be.false; - populate({ - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); - expect(subject.hasChildren()).to.be.false; + populate({ + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], }); + expect(subject.hasChildren()).to.be.false; }); + }); - describe("#hasChild(childPath): boolean", () => { - it("should return true for a child or deep child", () => { - populate({ a: { b: "c" }, d: 23 }); - expect(subject.hasChild("a/b")).to.be.true; - expect(subject.hasChild("d")).to.be.true; - }); + describe("#hasChild(childPath): boolean", () => { + it("should return true for a child or deep child", () => { + populate({ a: { b: "c" }, d: 23 }); + expect(subject.hasChild("a/b")).to.be.true; + expect(subject.hasChild("d")).to.be.true; + }); - it("should return false if a child is missing", () => { - populate({ - a: "b", - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); - expect(subject.hasChild("c")).to.be.false; - expect(subject.hasChild("a/b")).to.be.false; - expect(subject.hasChild("nullChild")).to.be.false; - expect(subject.hasChild("emptyObjectChild")).to.be.false; - expect(subject.hasChild("emptyArrayChild")).to.be.false; - expect(subject.hasChild("c")).to.be.false; - expect(subject.hasChild("a/b")).to.be.false; - }); + it("should return false if a child is missing", () => { + populate({ + a: "b", + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], + }); + expect(subject.hasChild("c")).to.be.false; + expect(subject.hasChild("a/b")).to.be.false; + expect(subject.hasChild("nullChild")).to.be.false; + expect(subject.hasChild("emptyObjectChild")).to.be.false; + expect(subject.hasChild("emptyArrayChild")).to.be.false; + expect(subject.hasChild("c")).to.be.false; + expect(subject.hasChild("a/b")).to.be.false; }); + }); - describe("#key: string", () => { - it("should return the key name", () => { - expect(subject.key).to.equal("foo"); - }); + describe("#key: string", () => { + it("should return the key name", () => { + expect(subject.key).to.equal("foo"); + }); - it("should return null for the root", () => { - const [instance, path] = database.extractInstanceAndPath( - "projects/_/instances/foo/refs/", - undefined - ); - const snapshot = new database.DataSnapshot(null, path, getApp(), instance); - expect(snapshot.key).to.be.null; - }); + it("should return null for the root", () => { + const [instance, path] = database.extractInstanceAndPath( + "projects/_/instances/foo/refs/", + undefined + ); + const snapshot = new database.DataSnapshot(null, path, getApp(), instance); + expect(snapshot.key).to.be.null; + }); - it("should work for child paths", () => { - expect(subject.child("foo/bar").key).to.equal("bar"); - }); + it("should work for child paths", () => { + expect(subject.child("foo/bar").key).to.equal("bar"); }); + }); - describe("#toJSON(): Object", () => { - it("should return the current value", () => { - populate({ - a: "b", - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); - expect(subject.toJSON()).to.deep.equal(subject.val()); + describe("#toJSON(): Object", () => { + it("should return the current value", () => { + populate({ + a: "b", + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], }); + expect(subject.toJSON()).to.deep.equal(subject.val()); + }); - it("should be stringifyable", () => { - populate({ - a: "b", - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); - expect(JSON.stringify(subject)).to.deep.equal('{"a":"b"}'); + it("should be stringifyable", () => { + populate({ + a: "b", + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], }); + expect(JSON.stringify(subject)).to.deep.equal('{"a":"b"}'); }); }); }); diff --git a/spec/v1/providers/firestore.spec.ts b/spec/v1/providers/firestore.spec.ts index 1c66ec31b..f8f4288db 100644 --- a/spec/v1/providers/firestore.spec.ts +++ b/spec/v1/providers/firestore.spec.ts @@ -92,6 +92,16 @@ describe("Firestore Functions", () => { } describe("document builders and event types", () => { + function expectedTrigger(resource: string, eventType: string) { + return { + eventTrigger: { + resource, + eventType: `providers/cloud.firestore/eventTypes/${eventType}`, + service: "firestore.googleapis.com", + }, + }; + } + function expectedEndpoint(resource: string, eventType: string) { return { ...MINIMAL_V1_ENDPOINT, @@ -129,9 +139,20 @@ describe("Firestore Functions", () => { const cloudFunction = firestore .namespace("v2") .document("users/{uid}") - .onWrite((snap, context) => { - expectType<{ uid: string }>(context.params); - }); + .onWrite(() => null); + + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(resource, "document.write")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint(resource, "document.write")); + }); + + it("should allow custom namespaces", () => { + const resource = "projects/project1/databases/(default)/documents@v2/users/{uid}"; + const cloudFunction = firestore + .namespace("v2") + .document("users/{uid}") + .onWrite(() => null); + + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(resource, "document.write")); expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint(resource, "document.write")); }); @@ -143,6 +164,8 @@ describe("Firestore Functions", () => { .document("users/{uid}") .onWrite(() => null); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(resource, "document.write")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint(resource, "document.write")); }); @@ -156,6 +179,8 @@ describe("Firestore Functions", () => { expectType<{ uid: string }>(context.params); }); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(resource, "document.write")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint(resource, "document.write")); }); @@ -171,6 +196,10 @@ describe("Firestore Functions", () => { expectType>(context.params); }); + expect(fn.__trigger.regions).to.deep.equal(["us-east1"]); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal("90s"); + expect(fn.__endpoint.region).to.deep.equal(["us-east1"]); expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); @@ -178,10 +207,14 @@ describe("Firestore Functions", () => { }); describe("process.env.GCLOUD_PROJECT not set", () => { - it("should not throw if __endpoint is not accessed", () => { + it("should not throw if __trigger is not accessed", () => { expect(() => firestore.document("input").onCreate(() => null)).to.not.throw(Error); }); + it("should throw when trigger is accessed", () => { + expect(() => firestore.document("input").onCreate(() => null).__trigger).to.throw(Error); + }); + it("should throw when endpoint is accessed", () => { expect(() => firestore.document("input").onCreate(() => null).__endpoint).to.throw(Error); }); diff --git a/spec/v1/providers/https.spec.ts b/spec/v1/providers/https.spec.ts index 7d06a15c3..f829aa6d3 100644 --- a/spec/v1/providers/https.spec.ts +++ b/spec/v1/providers/https.spec.ts @@ -34,6 +34,7 @@ describe("CloudHttpsBuilder", () => { const result = https.onRequest((req, resp) => { resp.send(200); }); + expect(result.__trigger).to.deep.equal({ httpsTrigger: {} }); expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -51,6 +52,11 @@ describe("CloudHttpsBuilder", () => { }) .https.onRequest(() => null); + expect(fn.__trigger.regions).to.deep.equal(["us-east1"]); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal("90s"); + expect(fn.__trigger.httpsTrigger.invoker).to.deep.equal(["private"]); + expect(fn.__endpoint.region).to.deep.equal(["us-east1"]); expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); @@ -65,6 +71,11 @@ describe("#onCall", () => { return "response"; }); + expect(result.__trigger).to.deep.equal({ + httpsTrigger: {}, + labels: { "deployment-callable": "true" }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -82,6 +93,10 @@ describe("#onCall", () => { }) .https.onCall(() => null); + expect(fn.__trigger.regions).to.deep.equal(["us-east1"]); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal("90s"); + expect(fn.__endpoint.region).to.deep.equal(["us-east1"]); expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); diff --git a/spec/v1/providers/pubsub.spec.ts b/spec/v1/providers/pubsub.spec.ts index 573ac0f1c..77b6fe24a 100644 --- a/spec/v1/providers/pubsub.spec.ts +++ b/spec/v1/providers/pubsub.spec.ts @@ -83,6 +83,10 @@ describe("Pubsub Functions", () => { .pubsub.topic("toppy") .onPublish(() => null); + expect(fn.__trigger.regions).to.deep.equal(["us-east1"]); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal("90s"); + expect(fn.__endpoint.region).to.deep.equal(["us-east1"]); expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); @@ -93,6 +97,14 @@ describe("Pubsub Functions", () => { // Pick up project from process.env.GCLOUD_PROJECT const result = pubsub.topic("toppy").onPublish(() => null); + expect(result.__trigger).to.deep.equal({ + eventTrigger: { + eventType: "google.pubsub.topic.publish", + resource: "projects/project1/topics/toppy", + service: "pubsub.googleapis.com", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -151,6 +163,10 @@ describe("Pubsub Functions", () => { it("should return a trigger/endpoint with schedule", () => { const result = pubsub.schedule("every 5 minutes").onRun(() => null); + expect(result.__trigger.schedule).to.deep.equal({ + schedule: "every 5 minutes", + }); + expect(result.__endpoint.scheduleTrigger).to.deep.equal({ ...MINIMAL_SCHEDULE_TRIGGER, schedule: "every 5 minutes", @@ -163,6 +179,11 @@ describe("Pubsub Functions", () => { .timeZone("America/New_York") .onRun(() => null); + expect(result.__trigger.schedule).to.deep.equal({ + schedule: "every 5 minutes", + timeZone: "America/New_York", + }); + expect(result.__endpoint.scheduleTrigger).to.deep.equal({ ...MINIMAL_SCHEDULE_TRIGGER, schedule: "every 5 minutes", @@ -183,6 +204,14 @@ describe("Pubsub Functions", () => { .retryConfig(retryConfig) .onRun(() => null); + expect(result.__trigger.schedule).to.deep.equal({ + schedule: "every 5 minutes", + retryConfig, + }); + expect(result.__trigger.labels).to.deep.equal({ + "deployment-scheduled": "true", + }); + expect(result.__endpoint.scheduleTrigger).to.deep.equal({ ...MINIMAL_SCHEDULE_TRIGGER, schedule: "every 5 minutes", @@ -208,6 +237,15 @@ describe("Pubsub Functions", () => { .retryConfig(retryConfig) .onRun(() => null); + expect(result.__trigger.schedule).to.deep.equal({ + schedule: "every 5 minutes", + retryConfig, + timeZone: "America/New_York", + }); + expect(result.__trigger.labels).to.deep.equal({ + "deployment-scheduled": "true", + }); + expect(result.__endpoint.scheduleTrigger).to.deep.equal({ ...MINIMAL_SCHEDULE_TRIGGER, schedule: "every 5 minutes", @@ -227,6 +265,12 @@ describe("Pubsub Functions", () => { }) .pubsub.schedule("every 5 minutes") .onRun(() => null); + expect(result.__trigger.schedule).to.deep.equal({ + schedule: "every 5 minutes", + }); + expect(result.__trigger.regions).to.deep.equal(["us-east1"]); + expect(result.__trigger.availableMemoryMb).to.deep.equal(256); + expect(result.__trigger.timeout).to.deep.equal("90s"); expect(result.__endpoint.scheduleTrigger).to.deep.equal({ ...MINIMAL_SCHEDULE_TRIGGER, @@ -247,6 +291,13 @@ describe("Pubsub Functions", () => { .pubsub.schedule("every 5 minutes") .timeZone("America/New_York") .onRun(() => null); + expect(result.__trigger.schedule).to.deep.equal({ + schedule: "every 5 minutes", + timeZone: "America/New_York", + }); + expect(result.__trigger.regions).to.deep.equal(["us-east1"]); + expect(result.__trigger.availableMemoryMb).to.deep.equal(256); + expect(result.__trigger.timeout).to.deep.equal("90s"); expect(result.__endpoint.scheduleTrigger).to.deep.equal({ ...MINIMAL_SCHEDULE_TRIGGER, @@ -275,6 +326,16 @@ describe("Pubsub Functions", () => { .pubsub.schedule("every 5 minutes") .retryConfig(retryConfig) .onRun(() => null); + expect(result.__trigger.schedule).to.deep.equal({ + schedule: "every 5 minutes", + retryConfig, + }); + expect(result.__trigger.labels).to.deep.equal({ + "deployment-scheduled": "true", + }); + expect(result.__trigger.regions).to.deep.equal(["us-east1"]); + expect(result.__trigger.availableMemoryMb).to.deep.equal(256); + expect(result.__trigger.timeout).to.deep.equal("90s"); expect(result.__endpoint.scheduleTrigger).to.deep.equal({ ...MINIMAL_SCHEDULE_TRIGGER, @@ -305,6 +366,17 @@ describe("Pubsub Functions", () => { .timeZone("America/New_York") .retryConfig(retryConfig) .onRun(() => null); + expect(result.__trigger.schedule).to.deep.equal({ + schedule: "every 5 minutes", + timeZone: "America/New_York", + retryConfig, + }); + expect(result.__trigger.labels).to.deep.equal({ + "deployment-scheduled": "true", + }); + expect(result.__trigger.regions).to.deep.equal(["us-east1"]); + expect(result.__trigger.availableMemoryMb).to.deep.equal(256); + expect(result.__trigger.timeout).to.deep.equal("90s"); expect(result.__endpoint.scheduleTrigger).to.deep.equal({ ...MINIMAL_SCHEDULE_TRIGGER, @@ -320,10 +392,14 @@ describe("Pubsub Functions", () => { }); describe("process.env.GCLOUD_PROJECT not set", () => { - it("should not throw if __endpoint is not accessed", () => { + it("should not throw if __trigger is not accessed", () => { expect(() => pubsub.topic("toppy").onPublish(() => null)).to.not.throw(Error); }); + it("should throw when trigger is accessed", () => { + expect(() => pubsub.topic("toppy").onPublish(() => null).__trigger).to.throw(Error); + }); + it("should throw when endpoint is accessed", () => { expect(() => pubsub.topic("toppy").onPublish(() => null).__endpoint).to.throw(Error); }); diff --git a/spec/v1/providers/remoteConfig.spec.ts b/spec/v1/providers/remoteConfig.spec.ts index 45d13bb32..f5fb427e6 100644 --- a/spec/v1/providers/remoteConfig.spec.ts +++ b/spec/v1/providers/remoteConfig.spec.ts @@ -53,6 +53,14 @@ describe("RemoteConfig Functions", () => { it("should have the correct trigger", () => { const cloudFunction = remoteConfig.onUpdate(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + resource: "projects/project1", + eventType: "google.firebase.remoteconfig.update", + service: "firebaseremoteconfig.googleapis.com", + }, + }); + expect(cloudFunction.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -76,6 +84,10 @@ describe("RemoteConfig Functions", () => { }) .remoteConfig.onUpdate(() => null); + expect(cloudFunction.__trigger.regions).to.deep.equal(["us-east1"]); + expect(cloudFunction.__trigger.availableMemoryMb).to.deep.equal(256); + expect(cloudFunction.__trigger.timeout).to.deep.equal("90s"); + expect(cloudFunction.__endpoint.region).to.deep.equal(["us-east1"]); expect(cloudFunction.__endpoint.availableMemoryMb).to.deep.equal(256); expect(cloudFunction.__endpoint.timeoutSeconds).to.deep.equal(90); diff --git a/spec/v1/providers/storage.spec.ts b/spec/v1/providers/storage.spec.ts index 0055e8769..77f8610fc 100644 --- a/spec/v1/providers/storage.spec.ts +++ b/spec/v1/providers/storage.spec.ts @@ -29,6 +29,16 @@ import { MINIMAL_V1_ENDPOINT } from "../../fixtures"; describe("Storage Functions", () => { describe("ObjectBuilder", () => { + function expectedTrigger(bucket: string, eventType: string) { + return { + eventTrigger: { + resource: `projects/_/buckets/${bucket}`, + eventType: `google.storage.object.${eventType}`, + service: "storage.googleapis.com", + }, + }; + } + function expectedEndpoint(bucket: string, eventType: string) { return { ...MINIMAL_V1_ENDPOINT, @@ -66,6 +76,10 @@ describe("Storage Functions", () => { .storage.object() .onArchive(() => null); + expect(fn.__trigger.regions).to.deep.equal(["us-east1"]); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal("90s"); + expect(fn.__endpoint.region).to.deep.equal(["us-east1"]); expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); @@ -78,12 +92,16 @@ describe("Storage Functions", () => { .object() .onArchive(() => null); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger("bucky", "archive")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint("bucky", "archive")); }); it("should use the default bucket when none is provided", () => { const cloudFunction = storage.object().onArchive(() => null); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(defaultBucket, "archive")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint(defaultBucket, "archive")); }); @@ -91,10 +109,20 @@ describe("Storage Functions", () => { const subjectQualified = new storage.ObjectBuilder(() => "projects/_/buckets/bucky", {}); const result = subjectQualified.onArchive(() => null); + expect(result.__trigger).to.deep.equal(expectedTrigger("bucky", "archive")); + expect(result.__endpoint).to.deep.equal(expectedEndpoint("bucky", "archive")); }); it("should throw with improperly formatted buckets", () => { + expect( + () => + storage + .bucket("bad/bucket/format") + .object() + .onArchive(() => null).__trigger + ).to.throw(Error); + expect( () => storage @@ -139,12 +167,16 @@ describe("Storage Functions", () => { .object() .onDelete(() => null); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger("bucky", "delete")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint("bucky", "delete")); }); it("should use the default bucket when none is provided", () => { const cloudFunction = storage.object().onDelete(() => null); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(defaultBucket, "delete")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint(defaultBucket, "delete")); }); @@ -152,6 +184,8 @@ describe("Storage Functions", () => { const subjectQualified = new storage.ObjectBuilder(() => "projects/_/buckets/bucky", {}); const result = subjectQualified.onDelete(() => null); + expect(result.__trigger).to.deep.equal(expectedTrigger("bucky", "delete")); + expect(result.__endpoint).to.deep.equal(expectedEndpoint("bucky", "delete")); }); @@ -161,6 +195,8 @@ describe("Storage Functions", () => { .object() .onDelete(() => null); + expect(() => fn.__trigger).to.throw(Error); + expect(() => fn.__endpoint).to.throw(Error); }); @@ -199,12 +235,16 @@ describe("Storage Functions", () => { .object() .onFinalize(() => null); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger("bucky", "finalize")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint("bucky", "finalize")); }); it("should use the default bucket when none is provided", () => { const cloudFunction = storage.object().onFinalize(() => null); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(defaultBucket, "finalize")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint(defaultBucket, "finalize")); }); @@ -212,6 +252,8 @@ describe("Storage Functions", () => { const subjectQualified = new storage.ObjectBuilder(() => "projects/_/buckets/bucky", {}); const result = subjectQualified.onFinalize(() => null); + expect(result.__trigger).to.deep.equal(expectedTrigger("bucky", "finalize")); + expect(result.__endpoint).to.deep.equal(expectedEndpoint("bucky", "finalize")); }); @@ -221,6 +263,8 @@ describe("Storage Functions", () => { .object() .onFinalize(() => null); + expect(() => fn.__trigger).to.throw(Error); + expect(() => fn.__endpoint).to.throw(Error); }); @@ -259,12 +303,18 @@ describe("Storage Functions", () => { .object() .onMetadataUpdate(() => null); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger("bucky", "metadataUpdate")); + expect(cloudFunction.__endpoint).to.deep.equal(expectedEndpoint("bucky", "metadataUpdate")); }); it("should use the default bucket when none is provided", () => { const cloudFunction = storage.object().onMetadataUpdate(() => null); + expect(cloudFunction.__trigger).to.deep.equal( + expectedTrigger(defaultBucket, "metadataUpdate") + ); + expect(cloudFunction.__endpoint).to.deep.equal( expectedEndpoint(defaultBucket, "metadataUpdate") ); @@ -274,6 +324,8 @@ describe("Storage Functions", () => { const subjectQualified = new storage.ObjectBuilder(() => "projects/_/buckets/bucky", {}); const result = subjectQualified.onMetadataUpdate(() => null); + expect(result.__trigger).to.deep.equal(expectedTrigger("bucky", "metadataUpdate")); + expect(result.__endpoint).to.deep.equal(expectedEndpoint("bucky", "metadataUpdate")); }); @@ -283,6 +335,7 @@ describe("Storage Functions", () => { .object() .onMetadataUpdate(() => null); + expect(() => fn.__trigger).to.throw(Error); expect(() => fn.__endpoint).to.throw(Error); }); @@ -321,10 +374,14 @@ describe("Storage Functions", () => { delete process.env.FIREBASE_CONFIG; }); - it("should not throw if __endpoint is not accessed", () => { + it("should not throw if __trigger is not accessed", () => { expect(() => storage.object().onArchive(() => null)).to.not.throw(Error); }); + it("should throw when trigger is accessed", () => { + expect(() => storage.object().onArchive(() => null).__trigger).to.throw(Error); + }); + it("should throw when endpoint is accessed", () => { expect(() => storage.object().onArchive(() => null).__endpoint).to.throw(Error); }); diff --git a/spec/v1/providers/tasks.spec.ts b/spec/v1/providers/tasks.spec.ts index ec9e6d318..dc0b5e0c6 100644 --- a/spec/v1/providers/tasks.spec.ts +++ b/spec/v1/providers/tasks.spec.ts @@ -46,6 +46,23 @@ describe("#onDispatch", () => { invoker: "private", }).onDispatch(() => undefined); + expect(result.__trigger).to.deep.equal({ + taskQueueTrigger: { + rateLimits: { + maxConcurrentDispatches: 30, + maxDispatchesPerSecond: 40, + }, + retryConfig: { + maxAttempts: 5, + maxRetrySeconds: 10, + maxBackoffSeconds: 20, + maxDoublings: 3, + minBackoffSeconds: 5, + }, + invoker: ["private"], + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -76,6 +93,17 @@ describe("#onDispatch", () => { .tasks.taskQueue({ retryConfig: { maxAttempts: 5 } }) .onDispatch(() => null); + expect(fn.__trigger).to.deep.equal({ + regions: ["us-east1"], + availableMemoryMb: 256, + timeout: "90s", + taskQueueTrigger: { + retryConfig: { + maxAttempts: 5, + }, + }, + }); + expect(fn.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", diff --git a/spec/v1/providers/testLab.spec.ts b/spec/v1/providers/testLab.spec.ts index 6aaccad2e..ba8bfc27a 100644 --- a/spec/v1/providers/testLab.spec.ts +++ b/spec/v1/providers/testLab.spec.ts @@ -39,6 +39,14 @@ describe("Test Lab Functions", () => { it("should return a trigger/endpoint with appropriate values", () => { const func = testLab.testMatrix().onComplete(() => null); + expect(func.__trigger).to.deep.equal({ + eventTrigger: { + service: "testing.googleapis.com", + eventType: "google.testing.testMatrix.complete", + resource: "projects/project1/testMatrices/{matrix}", + }, + }); + expect(func.__endpoint).to.deep.equal({ ...MINIMAL_V1_ENDPOINT, platform: "gcfv1", @@ -147,10 +155,14 @@ describe("Test Lab Functions", () => { }); describe("process.env.GCLOUD_PROJECT not set", () => { - it("should not throw if __endpoint is not accessed", () => { + it("should not throw if trigger is not accessed", () => { expect(() => testLab.testMatrix().onComplete(() => null)).to.not.throw(Error); }); + it("should throw when trigger is accessed", () => { + expect(() => testLab.testMatrix().onComplete(() => null).__trigger).to.throw(Error); + }); + it("should throw when endpoint is accessed", () => { expect(() => testLab.testMatrix().onComplete(() => null).__endpoint).to.throw(Error); }); diff --git a/spec/v2/providers/fixtures.ts b/spec/v2/providers/fixtures.ts index f2c5b60e6..1766a3dfb 100644 --- a/spec/v2/providers/fixtures.ts +++ b/spec/v2/providers/fixtures.ts @@ -1,5 +1,9 @@ +import { ManifestEndpoint } from "../../../src/runtime/manifest"; +import { TriggerAnnotation } from "../../../src/v2/core"; import * as options from "../../../src/v2/options"; +export { MINIMAL_V1_ENDPOINT, MINIMAL_V2_ENDPOINT } from "../../fixtures"; + export const FULL_OPTIONS: options.GlobalOptions = { region: "us-west1", memory: "512MiB", @@ -17,3 +21,42 @@ export const FULL_OPTIONS: options.GlobalOptions = { }, secrets: ["MY_SECRET"], }; + +export const FULL_TRIGGER: TriggerAnnotation = { + platform: "gcfv2", + regions: ["us-west1"], + availableMemoryMb: 512, + timeout: "60s", + minInstances: 1, + maxInstances: 3, + concurrency: 20, + vpcConnector: "aConnector", + vpcConnectorEgressSettings: "ALL_TRAFFIC", + serviceAccountEmail: "root@aProject.iam.gserviceaccount.com", + ingressSettings: "ALLOW_ALL", + labels: { + hello: "world", + }, + secrets: ["MY_SECRET"], +}; + +export const FULL_ENDPOINT: ManifestEndpoint = { + platform: "gcfv2", + region: ["us-west1"], + availableMemoryMb: 512, + timeoutSeconds: 60, + minInstances: 1, + maxInstances: 3, + concurrency: 20, + vpc: { + connector: "aConnector", + egressSettings: "ALL_TRAFFIC", + }, + serviceAccountEmail: "root@", + ingressSettings: "ALLOW_ALL", + cpu: "gcf_gen1", + labels: { + hello: "world", + }, + secretEnvironmentVariables: [{ key: "MY_SECRET" }], +}; diff --git a/spec/v2/providers/https.spec.ts b/spec/v2/providers/https.spec.ts index eabf471fb..f41dd2f90 100644 --- a/spec/v2/providers/https.spec.ts +++ b/spec/v2/providers/https.spec.ts @@ -28,8 +28,7 @@ import * as options from "../../../src/v2/options"; import * as https from "../../../src/v2/providers/https"; import { expectedResponseHeaders, MockRequest } from "../../fixtures/mockrequest"; import { runHandler } from "../../helper"; -import { FULL_OPTIONS } from "./fixtures"; -import { FULL_ENDPOINT, MINIMAL_V2_ENDPOINT } from "../../fixtures"; +import { FULL_ENDPOINT, MINIMAL_V2_ENDPOINT, FULL_OPTIONS, FULL_TRIGGER } from "./fixtures"; describe("onRequest", () => { beforeEach(() => { @@ -46,6 +45,14 @@ describe("onRequest", () => { res.send(200); }); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + httpsTrigger: { + allowInsecure: false, + }, + labels: {}, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -66,6 +73,15 @@ describe("onRequest", () => { } ); + expect(result.__trigger).to.deep.equal({ + ...FULL_TRIGGER, + httpsTrigger: { + allowInsecure: false, + invoker: ["service-account1@", "service-account2@"], + }, + regions: ["us-west1", "us-central1"], + }); + expect(result.__endpoint).to.deep.equal({ ...FULL_ENDPOINT, platform: "gcfv2", @@ -95,6 +111,18 @@ describe("onRequest", () => { } ); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + httpsTrigger: { + allowInsecure: false, + invoker: ["private"], + }, + concurrency: 20, + minInstances: 3, + regions: ["us-west1", "us-central1"], + labels: {}, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -202,6 +230,16 @@ describe("onCall", () => { it("should return a minimal trigger/endpoint with appropriate values", () => { const result = https.onCall(() => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + httpsTrigger: { + allowInsecure: false, + }, + labels: { + "deployment-callable": "true", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -213,6 +251,17 @@ describe("onCall", () => { it("should create a complex trigger/endpoint with appropriate values", () => { const result = https.onCall(FULL_OPTIONS, () => 42); + expect(result.__trigger).to.deep.equal({ + ...FULL_TRIGGER, + httpsTrigger: { + allowInsecure: false, + }, + labels: { + ...FULL_TRIGGER.labels, + "deployment-callable": "true", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...FULL_ENDPOINT, platform: "gcfv2", @@ -235,6 +284,19 @@ describe("onCall", () => { () => 42 ); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + httpsTrigger: { + allowInsecure: false, + }, + concurrency: 20, + minInstances: 3, + regions: ["us-west1", "us-central1"], + labels: { + "deployment-callable": "true", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", diff --git a/spec/v2/providers/pubsub.spec.ts b/spec/v2/providers/pubsub.spec.ts index 396dbd406..3b712044e 100644 --- a/spec/v2/providers/pubsub.spec.ts +++ b/spec/v2/providers/pubsub.spec.ts @@ -1,10 +1,9 @@ import { expect } from "chai"; import { CloudEvent } from "../../../src/v2/core"; -import { FULL_OPTIONS } from "./fixtures"; -import { FULL_ENDPOINT, MINIMAL_V2_ENDPOINT } from "../../fixtures"; import * as options from "../../../src/v2/options"; import * as pubsub from "../../../src/v2/providers/pubsub"; +import { FULL_ENDPOINT, MINIMAL_V2_ENDPOINT, FULL_OPTIONS, FULL_TRIGGER } from "./fixtures"; const EVENT_TRIGGER = { eventType: "google.cloud.pubsub.topic.v1.messagePublished", @@ -32,6 +31,12 @@ describe("onMessagePublished", () => { it("should return a minimal trigger/endpoint with appropriate values", () => { const result = pubsub.onMessagePublished("topic", () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + eventTrigger: EVENT_TRIGGER, + labels: {}, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -43,6 +48,11 @@ describe("onMessagePublished", () => { it("should create a complex trigger/endpoint with appropriate values", () => { const result = pubsub.onMessagePublished({ ...FULL_OPTIONS, topic: "topic" }, () => 42); + expect(result.__trigger).to.deep.equal({ + ...FULL_TRIGGER, + eventTrigger: EVENT_TRIGGER, + }); + expect(result.__endpoint).to.deep.equal({ ...FULL_ENDPOINT, platform: "gcfv2", @@ -66,6 +76,15 @@ describe("onMessagePublished", () => { () => 42 ); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + concurrency: 20, + minInstances: 3, + regions: ["us-west1"], + labels: {}, + eventTrigger: EVENT_TRIGGER, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -88,6 +107,15 @@ describe("onMessagePublished", () => { () => 42 ); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + minInstances: 3, + regions: ["us-west1"], + labels: {}, + eventTrigger: EVENT_TRIGGER, + failurePolicy: { retry: true }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", diff --git a/spec/v2/providers/storage.spec.ts b/spec/v2/providers/storage.spec.ts index 5a820ea7d..d5a699d70 100644 --- a/spec/v2/providers/storage.spec.ts +++ b/spec/v2/providers/storage.spec.ts @@ -2,8 +2,12 @@ import { expect } from "chai"; import * as config from "../../../src/common/config"; import * as options from "../../../src/v2/options"; import * as storage from "../../../src/v2/providers/storage"; -import { FULL_OPTIONS } from "./fixtures"; -import { FULL_ENDPOINT, MINIMAL_V2_ENDPOINT } from "../../fixtures"; +import { FULL_ENDPOINT, MINIMAL_V2_ENDPOINT, FULL_OPTIONS, FULL_TRIGGER } from "./fixtures"; + +const EVENT_TRIGGER = { + eventType: "event-type", + resource: "some-bucket", +}; const ENDPOINT_EVENT_TRIGGER = { eventType: "event-type", @@ -75,6 +79,12 @@ describe("v2/storage", () => { it("should create a minimal trigger/endpoint with bucket", () => { const result = storage.onOperation("event-type", "some-bucket", () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: EVENT_TRIGGER, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -88,6 +98,16 @@ describe("v2/storage", () => { const result = storage.onOperation("event-type", { region: "us-west1" }, () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...EVENT_TRIGGER, + resource: "default-bucket", + }, + regions: ["us-west1"], + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -103,6 +123,12 @@ describe("v2/storage", () => { it("should create a minimal trigger with bucket with opts and bucket", () => { const result = storage.onOperation("event-type", { bucket: "some-bucket" }, () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: EVENT_TRIGGER, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -121,6 +147,11 @@ describe("v2/storage", () => { () => 42 ); + expect(result.__trigger).to.deep.equal({ + ...FULL_TRIGGER, + eventTrigger: EVENT_TRIGGER, + }); + expect(result.__endpoint).to.deep.equal({ ...FULL_ENDPOINT, platform: "gcfv2", @@ -145,6 +176,15 @@ describe("v2/storage", () => { () => 42 ); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + concurrency: 20, + minInstances: 3, + regions: ["us-west1"], + labels: {}, + eventTrigger: EVENT_TRIGGER, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -158,6 +198,10 @@ describe("v2/storage", () => { }); describe("onObjectArchived", () => { + const ARCHIVED_TRIGGER = { + ...EVENT_TRIGGER, + eventType: storage.archivedEvent, + }; const ENDPOINT_ARCHIVED_TRIGGER = { ...ENDPOINT_EVENT_TRIGGER, eventType: storage.archivedEvent, @@ -172,6 +216,15 @@ describe("v2/storage", () => { const result = storage.onObjectArchived(() => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...ARCHIVED_TRIGGER, + resource: "default-bucket", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -186,6 +239,15 @@ describe("v2/storage", () => { it("should accept bucket and handler", () => { const result = storage.onObjectArchived("my-bucket", () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...ARCHIVED_TRIGGER, + resource: "my-bucket", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -203,6 +265,16 @@ describe("v2/storage", () => { () => 42 ); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...ARCHIVED_TRIGGER, + resource: "my-bucket", + }, + regions: ["us-west1"], + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -217,9 +289,18 @@ describe("v2/storage", () => { it("should accept opts and handler, default bucket", () => { config.resetCache({ storageBucket: "default-bucket" }); - const result = storage.onObjectArchived({ region: "us-west1" }, () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...ARCHIVED_TRIGGER, + resource: "default-bucket", + }, + regions: ["us-west1"], + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -234,6 +315,10 @@ describe("v2/storage", () => { }); describe("onObjectFinalized", () => { + const FINALIZED_TRIGGER = { + ...EVENT_TRIGGER, + eventType: storage.finalizedEvent, + }; const ENDPOINT_FINALIZED_TRIGGER = { ...ENDPOINT_EVENT_TRIGGER, eventType: storage.finalizedEvent, @@ -248,6 +333,15 @@ describe("v2/storage", () => { const result = storage.onObjectFinalized(() => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...FINALIZED_TRIGGER, + resource: "default-bucket", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -262,6 +356,15 @@ describe("v2/storage", () => { it("should accept bucket and handler", () => { const result = storage.onObjectFinalized("my-bucket", () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...FINALIZED_TRIGGER, + resource: "my-bucket", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -279,6 +382,16 @@ describe("v2/storage", () => { () => 42 ); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...FINALIZED_TRIGGER, + resource: "my-bucket", + }, + regions: ["us-west1"], + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -293,9 +406,18 @@ describe("v2/storage", () => { it("should accept opts and handler, default bucket", () => { config.resetCache({ storageBucket: "default-bucket" }); - const result = storage.onObjectFinalized({ region: "us-west1" }, () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...FINALIZED_TRIGGER, + resource: "default-bucket", + }, + regions: ["us-west1"], + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -310,6 +432,10 @@ describe("v2/storage", () => { }); describe("onObjectDeleted", () => { + const DELETED_TRIGGER = { + ...EVENT_TRIGGER, + eventType: storage.deletedEvent, + }; const ENDPOINT_DELETED_TRIGGER = { ...ENDPOINT_EVENT_TRIGGER, eventType: storage.deletedEvent, @@ -324,6 +450,15 @@ describe("v2/storage", () => { const result = storage.onObjectDeleted(() => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...DELETED_TRIGGER, + resource: "default-bucket", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -338,6 +473,15 @@ describe("v2/storage", () => { it("should accept bucket and handler", () => { const result = storage.onObjectDeleted("my-bucket", () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...DELETED_TRIGGER, + resource: "my-bucket", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -352,6 +496,16 @@ describe("v2/storage", () => { it("should accept opts and handler", () => { const result = storage.onObjectDeleted({ bucket: "my-bucket", region: "us-west1" }, () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...DELETED_TRIGGER, + resource: "my-bucket", + }, + regions: ["us-west1"], + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -366,9 +520,18 @@ describe("v2/storage", () => { it("should accept opts and handler, default bucket", () => { config.resetCache({ storageBucket: "default-bucket" }); - const result = storage.onObjectDeleted({ region: "us-west1" }, () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...DELETED_TRIGGER, + resource: "default-bucket", + }, + regions: ["us-west1"], + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -383,6 +546,10 @@ describe("v2/storage", () => { }); describe("onObjectMetadataUpdated", () => { + const METADATA_TRIGGER = { + ...EVENT_TRIGGER, + eventType: storage.metadataUpdatedEvent, + }; const ENDPOINT_METADATA_TRIGGER = { ...ENDPOINT_EVENT_TRIGGER, eventType: storage.metadataUpdatedEvent, @@ -397,6 +564,15 @@ describe("v2/storage", () => { const result = storage.onObjectMetadataUpdated(() => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...METADATA_TRIGGER, + resource: "default-bucket", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -411,6 +587,15 @@ describe("v2/storage", () => { it("should accept bucket and handler", () => { const result = storage.onObjectMetadataUpdated("my-bucket", () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...METADATA_TRIGGER, + resource: "my-bucket", + }, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -428,6 +613,16 @@ describe("v2/storage", () => { () => 42 ); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...METADATA_TRIGGER, + resource: "my-bucket", + }, + regions: ["us-west1"], + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -445,6 +640,16 @@ describe("v2/storage", () => { const result = storage.onObjectMetadataUpdated({ region: "us-west1" }, () => 42); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + labels: {}, + eventTrigger: { + ...METADATA_TRIGGER, + resource: "default-bucket", + }, + regions: ["us-west1"], + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", diff --git a/spec/v2/providers/tasks.spec.ts b/spec/v2/providers/tasks.spec.ts index 8131a3725..4962b342e 100644 --- a/spec/v2/providers/tasks.spec.ts +++ b/spec/v2/providers/tasks.spec.ts @@ -23,12 +23,11 @@ import { expect } from "chai"; import { ManifestEndpoint } from "../../../src/runtime/manifest"; +import * as options from "../../../src/v2/options"; import { onTaskDispatched, Request } from "../../../src/v2/providers/tasks"; import { MockRequest } from "../../fixtures/mockrequest"; import { runHandler } from "../../helper"; -import { FULL_OPTIONS } from "./fixtures"; -import { FULL_ENDPOINT, MINIMAL_V2_ENDPOINT } from "../../fixtures"; -import * as options from "../../../src/v2/options"; +import { FULL_ENDPOINT, MINIMAL_V2_ENDPOINT, FULL_OPTIONS, FULL_TRIGGER } from "./fixtures"; const MINIMIAL_TASK_QUEUE_TRIGGER: ManifestEndpoint["taskQueueTrigger"] = { rateLimits: { @@ -57,6 +56,12 @@ describe("onTaskDispatched", () => { it("should return a minimal trigger/endpoint with appropriate values", () => { const result = onTaskDispatched(() => undefined); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + taskQueueTrigger: {}, + labels: {}, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", @@ -85,6 +90,24 @@ describe("onTaskDispatched", () => { () => undefined ); + expect(result.__trigger).to.deep.equal({ + ...FULL_TRIGGER, + taskQueueTrigger: { + retryConfig: { + maxAttempts: 4, + maxRetrySeconds: 10, + maxDoublings: 3, + minBackoffSeconds: 1, + maxBackoffSeconds: 2, + }, + rateLimits: { + maxConcurrentDispatches: 5, + maxDispatchesPerSecond: 10, + }, + invoker: ["private"], + }, + }); + expect(result.__endpoint).to.deep.equal({ ...FULL_ENDPOINT, platform: "gcfv2", @@ -120,6 +143,15 @@ describe("onTaskDispatched", () => { () => undefined ); + expect(result.__trigger).to.deep.equal({ + platform: "gcfv2", + taskQueueTrigger: {}, + concurrency: 20, + minInstances: 3, + regions: ["us-west1"], + labels: {}, + }); + expect(result.__endpoint).to.deep.equal({ ...MINIMAL_V2_ENDPOINT, platform: "gcfv2", diff --git a/src/common/encoding.ts b/src/common/encoding.ts index 019182864..88b40ef3f 100644 --- a/src/common/encoding.ts +++ b/src/common/encoding.ts @@ -22,6 +22,17 @@ // Copied from firebase-tools/src/gcp/proto +/** + * A type alias used to annotate interfaces as using a google.protobuf.Duration. + * This type is parsed/encoded as a string of seconds + the "s" prefix. + */ +export type Duration = string; + +/** Get a google.protobuf.Duration for a number of seconds. */ +export function durationFromSeconds(s: number): Duration { + return `${s}s`; +} + /** * Utility function to help copy fields from type A to B. * As a safety net, catches typos or fields that aren't named the same @@ -61,6 +72,25 @@ export function convertIfPresent( dest[destField] = converter(src[srcField]); } +export function serviceAccountFromShorthand(serviceAccount: string): string | null { + if (serviceAccount === "default") { + return null; + } else if (serviceAccount.endsWith("@")) { + if (!process.env.GCLOUD_PROJECT) { + throw new Error( + `Unable to determine email for service account '${serviceAccount}' because process.env.GCLOUD_PROJECT is not set.` + ); + } + return `${serviceAccount}${process.env.GCLOUD_PROJECT}.iam.gserviceaccount.com`; + } else if (serviceAccount.includes("@")) { + return serviceAccount; + } else { + throw new Error( + `Invalid option for serviceAccount: '${serviceAccount}'. Valid options are 'default', a service account email, or '{serviceAccountName}@'` + ); + } +} + export function convertInvoker(invoker: string | string[]): string[] { if (typeof invoker === "string") { invoker = [invoker]; diff --git a/src/v1/cloud-functions.ts b/src/v1/cloud-functions.ts index 1b2f397d2..d2551e516 100644 --- a/src/v1/cloud-functions.ts +++ b/src/v1/cloud-functions.ts @@ -22,9 +22,20 @@ import { Request, Response } from "express"; import { warn } from "../logger"; -import { DeploymentOptions, RESET_VALUE } from "./function-configuration"; +import { + DEFAULT_FAILURE_POLICY, + DeploymentOptions, + RESET_VALUE, + FailurePolicy, + Schedule, +} from "./function-configuration"; export { Request, Response }; -import { convertIfPresent, copyIfPresent } from "../common/encoding"; +import { + convertIfPresent, + copyIfPresent, + serviceAccountFromShorthand, + durationFromSeconds, +} from "../common/encoding"; import { initV1Endpoint, initV1ScheduleTrigger, @@ -217,6 +228,36 @@ export interface Resource { labels?: { [tag: string]: string }; } +/** + * TriggerAnnotion is used internally by the firebase CLI to understand what + * type of Cloud Function to deploy. + */ +interface TriggerAnnotation { + availableMemoryMb?: number; + blockingTrigger?: { + eventType: string; + options?: Record; + }; + eventTrigger?: { + eventType: string; + resource: string; + service: string; + }; + failurePolicy?: FailurePolicy; + httpsTrigger?: { + invoker?: string[]; + }; + labels?: { [key: string]: string }; + regions?: string[]; + schedule?: Schedule; + timeout?: string; + vpcConnector?: string; + vpcConnectorEgressSettings?: string; + serviceAccountEmail?: string; + ingressSettings?: string; + secrets?: string[]; +} + /** * A Runnable has a `run` method which directly invokes the user-defined * function - useful for unit testing. @@ -239,6 +280,9 @@ export interface Runnable { export interface HttpsFunction { (req: Request, resp: Response): void | Promise; + /** @alpha */ + __trigger: TriggerAnnotation; + /** @alpha */ __endpoint: ManifestEndpoint; @@ -259,6 +303,9 @@ export interface BlockingFunction { /** @public */ (req: Request, resp: Response): void | Promise; + /** @alpha */ + __trigger: TriggerAnnotation; + /** @alpha */ __endpoint: ManifestEndpoint; @@ -276,6 +323,9 @@ export interface BlockingFunction { export interface CloudFunction extends Runnable { (input: any, context?: any): PromiseLike | any; + /** @alpha */ + __trigger: TriggerAnnotation; + /** @alpha */ __endpoint: ManifestEndpoint; @@ -367,6 +417,27 @@ export function makeCloudFunction({ return Promise.resolve(promise); }; + Object.defineProperty(cloudFunction, "__trigger", { + get: () => { + if (triggerResource() == null) { + return {}; + } + + const trigger: any = { + ...optionsToTrigger(options), + eventTrigger: { + resource: triggerResource(), + eventType: legacyEventType || provider + "." + eventType, + service, + }, + }; + if (!!labels && Object.keys(labels).length) { + trigger.labels = { ...trigger.labels, ...labels }; + } + return trigger; + }, + }); + Object.defineProperty(cloudFunction, "__endpoint", { get: () => { if (triggerResource() == null) { @@ -472,7 +543,55 @@ function _detectAuthType(event: Event) { return "UNAUTHENTICATED"; } -/** @internal */ +/** @hidden */ +export function optionsToTrigger(options: DeploymentOptions) { + const trigger: any = {}; + copyIfPresent( + trigger, + options, + "regions", + "schedule", + "minInstances", + "maxInstances", + "ingressSettings", + "vpcConnectorEgressSettings", + "vpcConnector", + "labels", + "secrets" + ); + convertIfPresent(trigger, options, "failurePolicy", "failurePolicy", (policy) => { + if (policy === false) { + return undefined; + } else if (policy === true) { + return DEFAULT_FAILURE_POLICY; + } else { + return policy; + } + }); + convertIfPresent(trigger, options, "timeout", "timeoutSeconds", durationFromSeconds); + convertIfPresent(trigger, options, "availableMemoryMb", "memory", (mem) => { + const memoryLookup = { + "128MB": 128, + "256MB": 256, + "512MB": 512, + "1GB": 1024, + "2GB": 2048, + "4GB": 4096, + "8GB": 8192, + }; + return memoryLookup[mem]; + }); + convertIfPresent( + trigger, + options, + "serviceAccountEmail", + "serviceAccount", + serviceAccountFromShorthand + ); + + return trigger; +} + export function optionsToEndpoint(options: DeploymentOptions): ManifestEndpoint { const endpoint: ManifestEndpoint = {}; copyIfPresent( diff --git a/src/v1/function-configuration.ts b/src/v1/function-configuration.ts index 848d103ad..1f0d9c4b5 100644 --- a/src/v1/function-configuration.ts +++ b/src/v1/function-configuration.ts @@ -158,6 +158,10 @@ export interface FailurePolicy { retry: Record; } +export const DEFAULT_FAILURE_POLICY: FailurePolicy = { + retry: {}, +}; + export const MAX_NUMBER_USER_LABELS = 58; /** diff --git a/src/v1/handler-builder.ts b/src/v1/handler-builder.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/v1/providers/auth.ts b/src/v1/providers/auth.ts index 8ec455a8b..edef7b0bb 100644 --- a/src/v1/providers/auth.ts +++ b/src/v1/providers/auth.ts @@ -40,6 +40,7 @@ import { EventContext, makeCloudFunction, optionsToEndpoint, + optionsToTrigger, } from "../cloud-functions"; import { DeploymentOptions } from "../function-configuration"; import { initV1Endpoint } from "../../runtime/manifest"; @@ -213,6 +214,19 @@ export class UserBuilder { const legacyEventType = `providers/cloud.auth/eventTypes/user.${eventType}`; + func.__trigger = { + labels: {}, + ...optionsToTrigger(this.options), + blockingTrigger: { + eventType: legacyEventType, + options: { + accessToken, + idToken, + refreshToken, + }, + }, + }; + func.__endpoint = { platform: "gcfv1", labels: {}, diff --git a/src/v1/providers/https.ts b/src/v1/providers/https.ts index d9d1a9b29..0cdcc52c1 100644 --- a/src/v1/providers/https.ts +++ b/src/v1/providers/https.ts @@ -30,7 +30,7 @@ import { onCallHandler, Request, } from "../../common/providers/https"; -import { HttpsFunction, optionsToEndpoint, Runnable } from "../cloud-functions"; +import { HttpsFunction, optionsToEndpoint, optionsToTrigger, Runnable } from "../cloud-functions"; import { DeploymentOptions } from "../function-configuration"; import { initV1Endpoint } from "../../runtime/manifest"; @@ -66,6 +66,17 @@ export function _onRequestWithOptions( const cloudFunction: any = (req: Request, res: express.Response) => { return handler(req, res); }; + cloudFunction.__trigger = { + ...optionsToTrigger(options), + httpsTrigger: {}, + }; + convertIfPresent( + cloudFunction.__trigger.httpsTrigger, + options, + "invoker", + "invoker", + convertInvoker + ); // TODO parse the options cloudFunction.__endpoint = { @@ -101,6 +112,13 @@ export function _onCallWithOptions( fixedLen ); + func.__trigger = { + labels: {}, + ...optionsToTrigger(options), + httpsTrigger: {}, + }; + func.__trigger.labels["deployment-callable"] = "true"; + func.__endpoint = { platform: "gcfv1", labels: {}, diff --git a/src/v1/providers/tasks.ts b/src/v1/providers/tasks.ts index 20a516e68..9126588a5 100644 --- a/src/v1/providers/tasks.ts +++ b/src/v1/providers/tasks.ts @@ -36,7 +36,7 @@ import { ManifestEndpoint, ManifestRequiredAPI, } from "../../runtime/manifest"; -import { optionsToEndpoint } from "../cloud-functions"; +import { optionsToEndpoint, optionsToTrigger } from "../cloud-functions"; import { DeploymentOptions } from "../function-configuration"; export { RetryConfig, RateLimits, TaskContext }; @@ -65,6 +65,9 @@ export interface TaskQueueOptions { export interface TaskQueueFunction { (req: Request, res: express.Response): Promise; + /** @alpha */ + __trigger: unknown; + /** @alpha */ __endpoint: ManifestEndpoint; @@ -106,6 +109,20 @@ export class TaskQueueBuilder { const fixedLen = (data: any, context: TaskContext) => handler(data, context); const func: any = onDispatchHandler(fixedLen); + func.__trigger = { + ...optionsToTrigger(this.depOpts || {}), + taskQueueTrigger: {}, + }; + copyIfPresent(func.__trigger.taskQueueTrigger, this.tqOpts, "retryConfig"); + copyIfPresent(func.__trigger.taskQueueTrigger, this.tqOpts, "rateLimits"); + convertIfPresent( + func.__trigger.taskQueueTrigger, + this.tqOpts, + "invoker", + "invoker", + convertInvoker + ); + func.__endpoint = { platform: "gcfv1", ...initV1Endpoint(this.depOpts), diff --git a/src/v2/core.ts b/src/v2/core.ts index 7b79654e1..72c7e8e7d 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -32,6 +32,37 @@ export { Change }; export { ParamsOf } from "../common/params"; +/** @internal */ +export interface TriggerAnnotation { + platform?: string; + concurrency?: number; + minInstances?: number; + maxInstances?: number; + availableMemoryMb?: number; + eventTrigger?: { + eventType: string; + resource: string; + service: string; + }; + failurePolicy?: { retry: boolean }; + httpsTrigger?: { + invoker?: string[]; + }; + labels?: { [key: string]: string }; + regions?: string[]; + timeout?: string; + vpcConnector?: string; + vpcConnectorEgressSettings?: string; + serviceAccountEmail?: string; + ingressSettings?: string; + secrets?: string[]; + blockingTrigger?: { + eventType: string; + options?: Record; + }; + // TODO: schedule +} + /** * A CloudEventBase is the base of a cross-platform format for encoding a serverless event. * More information can be found in https://github.com/cloudevents/spec @@ -70,6 +101,8 @@ export interface CloudEvent { export interface CloudFunction> { (raw: CloudEvent): any | Promise; + /** @alpha */ + __trigger?: unknown; /** @alpha */ __endpoint: ManifestEndpoint; diff --git a/src/v2/options.ts b/src/v2/options.ts index 91b9b8de6..cfe951917 100644 --- a/src/v2/options.ts +++ b/src/v2/options.ts @@ -25,9 +25,15 @@ * @packageDocumentation */ -import { convertIfPresent, copyIfPresent } from "../common/encoding"; +import { + convertIfPresent, + copyIfPresent, + durationFromSeconds, + serviceAccountFromShorthand, +} from "../common/encoding"; import { RESET_VALUE, ResetValue } from "../common/options"; import { ManifestEndpoint } from "../runtime/manifest"; +import { TriggerAnnotation } from "./core"; import { declaredParams, Expression } from "../params"; import { ParamSpec, SecretParam } from "../params/types"; import { HttpsOptions } from "./providers/https"; @@ -250,6 +256,56 @@ export interface EventHandlerOptions extends Omit { + return MemoryOptionToMB[mem]; + }); + convertIfPresent(annotation, opts, "regions", "region", (region) => { + if (typeof region === "string") { + return [region]; + } + return region; + }); + convertIfPresent( + annotation, + opts, + "serviceAccountEmail", + "serviceAccount", + serviceAccountFromShorthand + ); + convertIfPresent(annotation, opts, "timeout", "timeoutSeconds", durationFromSeconds); + convertIfPresent( + annotation, + opts as any as EventHandlerOptions, + "failurePolicy", + "retry", + (retry: boolean) => { + return retry ? { retry: true } : null; + } + ); + + return annotation; +} + /** * Apply GlobalOptions to endpoint manifest. * @internal diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index 0bbc43d18..010a4a355 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -173,6 +173,8 @@ export type HttpsFunction = (( /** An Express response object, for this function to respond to callers. */ res: express.Response ) => void | Promise) & { + /** @alpha */ + __trigger?: unknown; /** @alpha */ __endpoint: ManifestEndpoint; }; @@ -237,6 +239,29 @@ export function onRequest( handler = wrapTraceContext(handler); + Object.defineProperty(handler, "__trigger", { + get: () => { + const baseOpts = options.optionsToTriggerAnnotations(options.getGlobalOptions()); + // global options calls region a scalar and https allows it to be an array, + // but optionsToTriggerAnnotations handles both cases. + const specificOpts = options.optionsToTriggerAnnotations(opts as options.GlobalOptions); + const trigger: any = { + platform: "gcfv2", + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + httpsTrigger: { + allowInsecure: false, + }, + }; + convertIfPresent(trigger.httpsTrigger, opts, "invoker", "invoker", convertInvoker); + return trigger; + }, + }); + const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); // global options calls region a scalar and https allows it to be an array, // but optionsToTriggerAnnotations handles both cases. @@ -301,6 +326,28 @@ export function onCall>( fixedLen ); + Object.defineProperty(func, "__trigger", { + get: () => { + const baseOpts = options.optionsToTriggerAnnotations(options.getGlobalOptions()); + // global options calls region a scalar and https allows it to be an array, + // but optionsToTriggerAnnotations handles both cases. + const specificOpts = options.optionsToTriggerAnnotations(opts); + return { + platform: "gcfv2", + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + "deployment-callable": "true", + }, + httpsTrigger: { + allowInsecure: false, + }, + }; + }, + }); + const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); // global options calls region a scalar and https allows it to be an array, // but optionsToEndpoint handles both cases. diff --git a/src/v2/providers/pubsub.ts b/src/v2/providers/pubsub.ts index 0be727adc..2f215d52b 100644 --- a/src/v2/providers/pubsub.ts +++ b/src/v2/providers/pubsub.ts @@ -303,6 +303,27 @@ export function onMessagePublished( func.run = handler; + Object.defineProperty(func, "__trigger", { + get: () => { + const baseOpts = options.optionsToTriggerAnnotations(options.getGlobalOptions()); + const specificOpts = options.optionsToTriggerAnnotations(opts); + + return { + platform: "gcfv2", + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + eventTrigger: { + eventType: "google.cloud.pubsub.topic.v1.messagePublished", + resource: `projects/${process.env.GCLOUD_PROJECT}/topics/${topic}`, + }, + }; + }, + }); + const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); const specificOpts = options.optionsToEndpoint(opts); diff --git a/src/v2/providers/storage.ts b/src/v2/providers/storage.ts index 3428576bf..cf88cdc51 100644 --- a/src/v2/providers/storage.ts +++ b/src/v2/providers/storage.ts @@ -553,6 +553,27 @@ export function onOperation( func.run = handler; + Object.defineProperty(func, "__trigger", { + get: () => { + const baseOpts = options.optionsToTriggerAnnotations(options.getGlobalOptions()); + const specificOpts = options.optionsToTriggerAnnotations(opts); + + return { + platform: "gcfv2", + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + eventTrigger: { + eventType, + resource: bucket, // TODO(colerogers): replace with 'bucket,' eventually + }, + }; + }, + }); + // TypeScript doesn't recognize defineProperty as adding a property and complains // that __endpoint doesn't exist. We can either cast to any and lose all type safety // or we can just assign a meaningless value before calling defineProperty. diff --git a/src/v2/providers/tasks.ts b/src/v2/providers/tasks.ts index f6e63b210..d5974b722 100644 --- a/src/v2/providers/tasks.ts +++ b/src/v2/providers/tasks.ts @@ -207,8 +207,29 @@ export function onTaskDispatched( const fixedLen = (req: Request) => handler(req); const func: any = wrapTraceContext(onDispatchHandler(fixedLen)); - const globalOpts = options.getGlobalOptions(); - const baseOpts = options.optionsToEndpoint(globalOpts); + Object.defineProperty(func, "__trigger", { + get: () => { + const baseOpts = options.optionsToTriggerAnnotations(options.getGlobalOptions()); + // global options calls region a scalar and https allows it to be an array, + // but optionsToTriggerAnnotations handles both cases. + const specificOpts = options.optionsToTriggerAnnotations(opts as options.GlobalOptions); + const taskQueueTrigger: Record = {}; + copyIfPresent(taskQueueTrigger, opts, "retryConfig", "rateLimits"); + convertIfPresent(taskQueueTrigger, opts, "invoker", "invoker", convertInvoker); + return { + platform: "gcfv2", + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + taskQueueTrigger, + }; + }, + }); + + const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); // global options calls region a scalar and https allows it to be an array, // but optionsToManifestEndpoint handles both cases. const specificOpts = options.optionsToEndpoint(opts as options.GlobalOptions);