From bb500c33ac42697128b2ec2c329197255829d17a Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 22 Nov 2021 10:50:39 -0800 Subject: [PATCH 1/3] Annotate function triggers with __endpoint property (#999) In addition to annotating function triggers with `__trigger` property, we add `__endpoint` annotation. This property will be used by the to-be-developed functions runtime to generate/declare deployment manifest that the CLI will use to deploy the function. There are lots of code duplication between the utility functions for annotating the `__trigger` and `__endpoint` properties. I didn't try to refactor the common code since I expect that we will favor `__endpoint` property in the future. --- spec/v1/cloud-functions.spec.ts | 103 +++++++++++- spec/v1/providers/https.spec.ts | 32 +++- spec/v2/providers/helpers.ts | 19 +++ spec/v2/providers/https.spec.ts | 62 ++++++- spec/v2/providers/pubsub.spec.ts | 66 +++++++- spec/v2/providers/storage.spec.ts | 267 +++++++++++++++++++++++++++++- src/cloud-functions.ts | 92 +++++++++- src/common/manifest.ts | 79 +++++++++ src/handler-builder.ts | 2 + src/providers/https.ts | 31 +++- src/v2/core.ts | 5 +- src/v2/options.ts | 50 +++++- src/v2/providers/https.ts | 48 +++++- src/v2/providers/pubsub.ts | 26 ++- src/v2/providers/storage.ts | 37 ++++- 15 files changed, 885 insertions(+), 34 deletions(-) create mode 100644 src/common/manifest.ts diff --git a/spec/v1/cloud-functions.spec.ts b/spec/v1/cloud-functions.spec.ts index 05e80bd21..5c861f3d6 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 __trigger on the returned CloudFunction', () => { + it('should put a __trigger/__endpoint on the returned CloudFunction', () => { const cf = makeCloudFunction({ provider: 'mock.provider', eventType: 'mock.event', @@ -49,6 +49,7 @@ describe('makeCloudFunction', () => { triggerResource: () => 'resource', handler: () => null, }); + expect(cf.__trigger).to.deep.equal({ eventTrigger: { eventType: 'mock.provider.mock.event', @@ -56,10 +57,22 @@ describe('makeCloudFunction', () => { service: 'service', }, }); + + expect(cf.__endpoint).to.deep.equal({ + platform: 'gcfv1', + eventTrigger: { + eventType: 'mock.provider.mock.event', + eventFilters: { + resource: 'resource', + }, + retry: false, + }, + }); }); - it('should have legacy event type in __trigger 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', @@ -67,6 +80,92 @@ describe('makeCloudFunction', () => { service: 'service', }, }); + + expect(cf.__endpoint).to.deep.equal({ + platform: 'gcfv1', + eventTrigger: { + eventType: 'providers/provider/eventTypes/event', + eventFilters: { + resource: 'resource', + }, + retry: false, + }, + }); + }); + + it('should include converted options in __endpoint', () => { + const cf = makeCloudFunction({ + provider: 'mock.provider', + eventType: 'mock.event', + service: 'service', + triggerResource: () => 'resource', + handler: () => null, + options: { + timeoutSeconds: 10, + regions: ['us-central1'], + memory: '128MB', + serviceAccount: 'foo@google.com', + }, + }); + + expect(cf.__endpoint).to.deep.equal({ + platform: 'gcfv1', + timeoutSeconds: 10, + region: ['us-central1'], + availableMemoryMb: 128, + serviceAccountEmail: 'foo@google.com', + eventTrigger: { + eventType: 'mock.provider.mock.event', + eventFilters: { + resource: 'resource', + }, + retry: false, + }, + }); + }); + + it('should set retry given failure policy in __endpoint', () => { + const cf = makeCloudFunction({ + provider: 'mock.provider', + eventType: 'mock.event', + service: 'service', + triggerResource: () => 'resource', + handler: () => null, + options: { failurePolicy: { retry: {} } }, + }); + + expect(cf.__endpoint).to.deep.equal({ + platform: 'gcfv1', + eventTrigger: { + eventType: 'mock.provider.mock.event', + eventFilters: { + resource: 'resource', + }, + retry: true, + }, + }); + }); + + it('should setup a scheduleTrigger in __endpoint given a schedule', () => { + const schedule = { + schedule: 'every 5 minutes', + retryConfig: { retryCount: 3 }, + timeZone: 'America/New_York', + }; + const cf = makeCloudFunction({ + provider: 'mock.provider', + eventType: 'mock.event', + service: 'service', + triggerResource: () => 'resource', + handler: () => null, + options: { + schedule, + }, + }); + expect(cf.__endpoint).to.deep.equal({ + platform: 'gcfv1', + scheduleTrigger: schedule, + }); }); it('should construct the right context for event', () => { diff --git a/spec/v1/providers/https.spec.ts b/spec/v1/providers/https.spec.ts index 2fbddcd26..ddc9c670d 100644 --- a/spec/v1/providers/https.spec.ts +++ b/spec/v1/providers/https.spec.ts @@ -22,7 +22,7 @@ import { expect } from 'chai'; import * as express from 'express'; -import * as _ from 'lodash'; + import * as functions from '../../../src/index'; import * as https from '../../../src/providers/https'; import { @@ -94,11 +94,15 @@ function runHandler( describe('CloudHttpsBuilder', () => { describe('#onRequest', () => { - it('should return a Trigger with appropriate values', () => { + it('should return a trigger/endpoint with appropriate values', () => { const result = https.onRequest((req, resp) => { resp.send(200); }); expect(result.__trigger).to.deep.equal({ httpsTrigger: {} }); + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv1', + httpsTrigger: {}, + }); }); it('should allow both region and runtime options to be set', () => { @@ -115,24 +119,31 @@ describe('CloudHttpsBuilder', () => { 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); + expect(fn.__endpoint.httpsTrigger.invoker).to.deep.equal(['private']); }); }); }); describe('handler namespace', () => { describe('#onRequest', () => { - it('should return an empty trigger', () => { + it('should return an empty trigger and endpoint', () => { const result = functions.handler.https.onRequest((req, res) => { res.send(200); }); expect(result.__trigger).to.deep.equal({}); + expect(result.__endpoint).to.deep.equal({}); }); }); describe('#onCall', () => { - it('should return an empty trigger', () => { + it('should return an empty trigger and endpoint', () => { const result = functions.handler.https.onCall(() => null); expect(result.__trigger).to.deep.equal({}); + expect(result.__endpoint).to.deep.equal({}); }); }); @@ -145,14 +156,21 @@ describe('handler namespace', () => { }); describe('#onCall', () => { - it('should return a Trigger with appropriate values', () => { + it('should return a trigger/endpoint with appropriate values', () => { const result = https.onCall((data) => { return 'response'; }); + expect(result.__trigger).to.deep.equal({ httpsTrigger: {}, labels: { 'deployment-callable': 'true' }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv1', + callableTrigger: {}, + labels: {}, + }); }); it('should allow both region and runtime options to be set', () => { @@ -167,6 +185,10 @@ describe('#onCall', () => { 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); }); it('has a .run method', () => { diff --git a/spec/v2/providers/helpers.ts b/spec/v2/providers/helpers.ts index a654a4944..a499be69a 100644 --- a/spec/v2/providers/helpers.ts +++ b/spec/v2/providers/helpers.ts @@ -33,3 +33,22 @@ export const FULL_TRIGGER = { hello: 'world', }, }; + +export const FULL_ENDPOINT = { + 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', + labels: { + hello: 'world', + }, +}; diff --git a/spec/v2/providers/https.spec.ts b/spec/v2/providers/https.spec.ts index ef9efbc29..30ad53088 100644 --- a/spec/v2/providers/https.spec.ts +++ b/spec/v2/providers/https.spec.ts @@ -7,7 +7,7 @@ import { expectedResponseHeaders, MockRequest, } from '../../fixtures/mockrequest'; -import { FULL_OPTIONS, FULL_TRIGGER } from './helpers'; +import { FULL_ENDPOINT, FULL_OPTIONS, FULL_TRIGGER } from './helpers'; /** * RunHandlerResult contains the data from an express.Response. @@ -82,10 +82,11 @@ describe('onRequest', () => { delete process.env.GCLOUD_PROJECT; }); - it('should return a minimal trigger with appropriate values', () => { + it('should return a minimal trigger/endpoint with appropriate values', () => { const result = https.onRequest((req, res) => { res.send(200); }); + expect(result.__trigger).to.deep.equal({ apiVersion: 2, platform: 'gcfv2', @@ -94,9 +95,15 @@ describe('onRequest', () => { }, labels: {}, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + httpsTrigger: {}, + labels: {}, + }); }); - it('should create a complex trigger with appropriate values', () => { + it('should create a complex trigger/endpoint with appropriate values', () => { const result = https.onRequest( { ...FULL_OPTIONS, @@ -107,6 +114,7 @@ describe('onRequest', () => { res.send(200); } ); + expect(result.__trigger).to.deep.equal({ ...FULL_TRIGGER, httpsTrigger: { @@ -115,6 +123,14 @@ describe('onRequest', () => { }, regions: ['us-west1', 'us-central1'], }); + + expect(result.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + httpsTrigger: { + invoker: ['service-account1@', 'service-account2@'], + }, + region: ['us-west1', 'us-central1'], + }); }); it('should merge options and globalOptions', () => { @@ -148,6 +164,17 @@ describe('onRequest', () => { regions: ['us-west1', 'us-central1'], labels: {}, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + httpsTrigger: { + invoker: ['private'], + }, + concurrency: 20, + minInstances: 3, + region: ['us-west1', 'us-central1'], + labels: {}, + }); }); it('should be an express handler', async () => { @@ -209,8 +236,9 @@ describe('onCall', () => { delete process.env.GCLOUD_PROJECT; }); - it('should return a minimal trigger with appropriate values', () => { + it('should return a minimal trigger/endpoint with appropriate values', () => { const result = https.onCall((request) => 42); + expect(result.__trigger).to.deep.equal({ apiVersion: 2, platform: 'gcfv2', @@ -221,10 +249,17 @@ describe('onCall', () => { 'deployment-callable': 'true', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + callableTrigger: {}, + }); }); - it('should create a complex trigger with appropriate values', () => { + it('should create a complex trigger/endpoint with appropriate values', () => { const result = https.onCall(FULL_OPTIONS, (request) => 42); + expect(result.__trigger).to.deep.equal({ ...FULL_TRIGGER, httpsTrigger: { @@ -235,6 +270,14 @@ describe('onCall', () => { 'deployment-callable': 'true', }, }); + + expect(result.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + callableTrigger: {}, + labels: { + ...FULL_ENDPOINT.labels, + }, + }); }); it('should merge options and globalOptions', () => { @@ -265,6 +308,15 @@ describe('onCall', () => { 'deployment-callable': 'true', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + callableTrigger: {}, + concurrency: 20, + minInstances: 3, + region: ['us-west1', 'us-central1'], + labels: {}, + }); }); it('has a .run method', () => { diff --git a/spec/v2/providers/pubsub.spec.ts b/spec/v2/providers/pubsub.spec.ts index 4fc338614..f48e10f72 100644 --- a/spec/v2/providers/pubsub.spec.ts +++ b/spec/v2/providers/pubsub.spec.ts @@ -3,13 +3,21 @@ import { expect } from 'chai'; import { CloudEvent } from '../../../src/v2/core'; import * as options from '../../../src/v2/options'; import * as pubsub from '../../../src/v2/providers/pubsub'; -import { FULL_OPTIONS, FULL_TRIGGER } from './helpers'; +import { FULL_ENDPOINT, FULL_OPTIONS, FULL_TRIGGER } from './helpers'; const EVENT_TRIGGER = { eventType: 'google.cloud.pubsub.topic.v1.messagePublished', resource: 'projects/aProject/topics/topic', }; +const ENDPOINT_EVENT_TRIGGER = { + eventType: 'google.cloud.pubsub.topic.v1.messagePublished', + eventFilters: { + topic: 'topic', + }, + retry: false, +}; + describe('onMessagePublished', () => { beforeEach(() => { options.setGlobalOptions({}); @@ -20,25 +28,38 @@ describe('onMessagePublished', () => { delete process.env.GCLOUD_PROJECT; }); - it('should return a minimal trigger with appropriate values', () => { + it('should return a minimal trigger/endpoint with appropriate values', () => { const result = pubsub.onMessagePublished('topic', () => 42); + expect(result.__trigger).to.deep.equal({ apiVersion: 2, platform: 'gcfv2', eventTrigger: EVENT_TRIGGER, labels: {}, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + eventTrigger: ENDPOINT_EVENT_TRIGGER, + labels: {}, + }); }); - it('should create a complex trigger with appropriate values', () => { + 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, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); }); it('should merge options and globalOptions', () => { @@ -66,6 +87,45 @@ describe('onMessagePublished', () => { labels: {}, eventTrigger: EVENT_TRIGGER, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + concurrency: 20, + minInstances: 3, + region: ['us-west1'], + labels: {}, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); + }); + + it('should convert retry option if appropriate', () => { + const result = pubsub.onMessagePublished( + { + topic: 'topic', + region: 'us-west1', + minInstances: 3, + retry: true, + }, + () => 42 + ); + + expect(result.__trigger).to.deep.equal({ + apiVersion: 2, + platform: 'gcfv2', + minInstances: 3, + regions: ['us-west1'], + labels: {}, + eventTrigger: EVENT_TRIGGER, + failurePolicy: { retry: true }, + }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + minInstances: 3, + region: ['us-west1'], + labels: {}, + eventTrigger: { ...ENDPOINT_EVENT_TRIGGER, retry: true }, + }); }); it('should have a .run method', () => { diff --git a/spec/v2/providers/storage.spec.ts b/spec/v2/providers/storage.spec.ts index 601b5c024..8c2e25576 100644 --- a/spec/v2/providers/storage.spec.ts +++ b/spec/v2/providers/storage.spec.ts @@ -10,6 +10,14 @@ const EVENT_TRIGGER = { resource: 'some-bucket', }; +const ENDPOINT_EVENT_TRIGGER = { + eventType: 'event-type', + eventFilters: { + bucket: 'some-bucket', + }, + retry: false, +}; + describe('v2/storage', () => { describe('getOptsAndBucket', () => { it('should return the default bucket with empty opts', () => { @@ -68,7 +76,7 @@ describe('v2/storage', () => { configStub.restore(); }); - it('should create a minimal trigger with bucket', () => { + it('should create a minimal trigger/endpoint with bucket', () => { const result = storage.onOperation('event-type', 'some-bucket', () => 42); expect(result.__trigger).to.deep.equal({ @@ -76,9 +84,15 @@ describe('v2/storage', () => { labels: {}, eventTrigger: EVENT_TRIGGER, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); }); - it('should create a minimal trigger with opts', () => { + it('should create a minimal trigger/endpoint with opts', () => { configStub.returns({ storageBucket: 'default-bucket' }); const result = storage.onOperation( @@ -96,6 +110,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_EVENT_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + region: ['us-west1'], + }); }); it('should create a minimal trigger with bucket with opts and bucket', () => { @@ -110,9 +136,15 @@ describe('v2/storage', () => { labels: {}, eventTrigger: EVENT_TRIGGER, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); }); - it('should create a complex trigger with appropriate values', () => { + it('should create a complex trigger/endpoint with appropriate values', () => { const result = storage.onOperation( 'event-type', { @@ -139,6 +171,26 @@ describe('v2/storage', () => { }, eventTrigger: EVENT_TRIGGER, }); + + expect(result.__endpoint).to.deep.equal({ + 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', + labels: { + hello: 'world', + }, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); }); it('should merge options and globalOptions', () => { @@ -166,6 +218,15 @@ describe('v2/storage', () => { labels: {}, eventTrigger: EVENT_TRIGGER, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + concurrency: 20, + minInstances: 3, + region: ['us-west1'], + labels: {}, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); }); }); @@ -174,6 +235,10 @@ describe('v2/storage', () => { ...EVENT_TRIGGER, eventType: storage.archivedEvent, }; + const ENDPOINT_ARCHIVED_TRIGGER = { + ...ENDPOINT_EVENT_TRIGGER, + eventType: storage.archivedEvent, + }; let configStub: sinon.SinonStub; beforeEach(() => { @@ -197,6 +262,17 @@ describe('v2/storage', () => { resource: 'default-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_ARCHIVED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + }); }); it('should accept bucket and handler', () => { @@ -210,6 +286,17 @@ describe('v2/storage', () => { resource: 'my-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_ARCHIVED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + }); }); it('should accept opts and handler', () => { @@ -227,6 +314,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_ARCHIVED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + region: ['us-west1'], + }); }); it('should accept opts and handler, default bucket', () => { @@ -243,6 +342,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_ARCHIVED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + region: ['us-west1'], + }); }); }); @@ -251,6 +362,10 @@ describe('v2/storage', () => { ...EVENT_TRIGGER, eventType: storage.finalizedEvent, }; + const ENDPOINT_FINALIZED_TRIGGER = { + ...ENDPOINT_EVENT_TRIGGER, + eventType: storage.finalizedEvent, + }; let configStub: sinon.SinonStub; beforeEach(() => { @@ -274,6 +389,17 @@ describe('v2/storage', () => { resource: 'default-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_FINALIZED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + }); }); it('should accept bucket and handler', () => { @@ -287,6 +413,17 @@ describe('v2/storage', () => { resource: 'my-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_FINALIZED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + }); }); it('should accept opts and handler', () => { @@ -304,6 +441,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_FINALIZED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + region: ['us-west1'], + }); }); it('should accept opts and handler, default bucket', () => { @@ -323,6 +472,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_FINALIZED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + region: ['us-west1'], + }); }); }); @@ -331,6 +492,10 @@ describe('v2/storage', () => { ...EVENT_TRIGGER, eventType: storage.deletedEvent, }; + const ENDPOINT_DELETED_TRIGGER = { + ...ENDPOINT_EVENT_TRIGGER, + eventType: storage.deletedEvent, + }; let configStub: sinon.SinonStub; beforeEach(() => { @@ -355,6 +520,17 @@ describe('v2/storage', () => { }, }); + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_DELETED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + }); + configStub.restore(); }); @@ -369,6 +545,17 @@ describe('v2/storage', () => { resource: 'my-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_DELETED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + }); }); it('should accept opts and handler', () => { @@ -386,6 +573,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_DELETED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + region: ['us-west1'], + }); }); it('should accept opts and handler, default bucket', () => { @@ -402,6 +601,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_DELETED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + region: ['us-west1'], + }); }); }); @@ -410,6 +621,10 @@ describe('v2/storage', () => { ...EVENT_TRIGGER, eventType: storage.metadataUpdatedEvent, }; + const ENDPOINT_METADATA_TRIGGER = { + ...ENDPOINT_EVENT_TRIGGER, + eventType: storage.metadataUpdatedEvent, + }; let configStub: sinon.SinonStub; beforeEach(() => { @@ -434,6 +649,17 @@ describe('v2/storage', () => { }, }); + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_METADATA_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + }); + configStub.restore(); }); @@ -448,6 +674,17 @@ describe('v2/storage', () => { resource: 'my-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_METADATA_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + }); }); it('should accept opts and handler', () => { @@ -465,6 +702,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_METADATA_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + region: ['us-west1'], + }); }); it('should accept opts and handler, default bucket', () => { @@ -484,6 +733,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_METADATA_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + region: ['us-west1'], + }); }); }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 688c4eb78..5f7823c2f 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -37,6 +37,7 @@ import { durationFromSeconds, serviceAccountFromShorthand, } from './common/encoding'; +import { ManifestEndpoint } from './common/manifest'; /** @hidden */ const WILDCARD_REGEX = new RegExp('{[^/{}]*}', 'g'); @@ -283,6 +284,14 @@ export interface TriggerAnnotated { }; } +/** + * @hidden + * EndpointAnnotated is used to generate the manifest that conforms to the container contract. + */ +export interface EndpointAnnotated { + __endpoint: ManifestEndpoint; +} + /** * A Runnable has a `run` method which directly invokes the user-defined * function - useful for unit testing. @@ -301,6 +310,7 @@ export interface Runnable { * arguments. */ export type HttpsFunction = TriggerAnnotated & + EndpointAnnotated & ((req: Request, resp: Response) => void | Promise); /** @@ -312,6 +322,7 @@ export type HttpsFunction = TriggerAnnotated & */ export type CloudFunction = Runnable & TriggerAnnotated & + EndpointAnnotated & ((input: any, context?: any) => PromiseLike | any); /** @hidden */ @@ -322,9 +333,9 @@ export interface MakeCloudFunctionArgs { dataConstructor?: (raw: Event) => EventData; eventType: string; handler?: (data: EventData, context: EventContext) => PromiseLike | any; - labels?: { [key: string]: any }; + labels?: Record; legacyEventType?: string; - options?: { [key: string]: any }; + options?: DeploymentOptions; /* * TODO: should remove `provider` and require a fully qualified `eventType` * once all providers have migrated to new format. @@ -432,6 +443,37 @@ export function makeCloudFunction({ }, }); + Object.defineProperty(cloudFunction, '__endpoint', { + get: () => { + if (triggerResource() == null) { + return undefined; + } + + const endpoint: ManifestEndpoint = { + platform: 'gcfv1', + ...optionsToEndpoint(options), + }; + + if (options.schedule) { + endpoint.scheduleTrigger = options.schedule; + } else { + endpoint.eventTrigger = { + eventType: legacyEventType || provider + '.' + eventType, + eventFilters: { + resource: triggerResource(), + }, + retry: !!options.failurePolicy, + }; + } + + if (Object.keys(labels).length > 0) { + endpoint.labels = { ...endpoint.labels, ...labels }; + } + + return endpoint; + }, + }); + cloudFunction.run = handler || contextOnlyHandler; return cloudFunction; } @@ -545,3 +587,49 @@ export function optionsToTrigger(options: DeploymentOptions) { return trigger; } + +export function optionsToEndpoint( + options: DeploymentOptions +): ManifestEndpoint { + const endpoint: ManifestEndpoint = {}; + copyIfPresent( + endpoint, + options, + 'minInstances', + 'maxInstances', + 'ingressSettings', + 'labels', + 'timeoutSeconds' + ); + convertIfPresent(endpoint, options, 'region', 'regions'); + convertIfPresent( + endpoint, + options, + 'serviceAccountEmail', + 'serviceAccount', + (sa) => sa + ); + if (options.vpcConnector) { + const vpc: ManifestEndpoint['vpc'] = { connector: options.vpcConnector }; + convertIfPresent( + vpc, + options, + 'egressSettings', + 'vpcConnectorEgressSettings' + ); + endpoint.vpc = vpc; + } + convertIfPresent(endpoint, options, 'availableMemoryMb', 'memory', (mem) => { + const memoryLookup = { + '128MB': 128, + '256MB': 256, + '512MB': 512, + '1GB': 1024, + '2GB': 2048, + '4GB': 4096, + '8GB': 8192, + }; + return memoryLookup[mem]; + }); + return endpoint; +} diff --git a/src/common/manifest.ts b/src/common/manifest.ts new file mode 100644 index 000000000..02784b68c --- /dev/null +++ b/src/common/manifest.ts @@ -0,0 +1,79 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/** + * @internal + * An definition of a function as appears in the Manifest. + */ +export interface ManifestEndpoint { + entryPoint?: string; + region?: string[]; + platform?: string; + availableMemoryMb?: number; + maxInstances?: number; + minInstances?: number; + concurrency?: number; + serviceAccountEmail?: string; + timeoutSeconds?: number; + vpc?: { + connector: string; + egressSettings?: string; + }; + labels?: Record; + ingressSettings?: string; + environmentVariables?: Record; + + httpsTrigger?: { + invoker?: string[]; + }; + + callableTrigger?: {}; + + eventTrigger?: { + eventFilters: Record; + eventType: string; + retry: boolean; + region?: string; + serviceAccountEmail?: string; + }; + + scheduleTrigger?: { + schedule?: string; + timezone?: string; + retryConfig?: { + retryCount?: number; + maxRetryDuration?: string; + minBackoffDuration?: string; + maxBackoffDuration?: string; + maxDoublings?: number; + }; + }; +} + +/** + * @internal + * An definition of a function deployment as appears in the Manifest. + **/ +export interface ManifestBackend { + specVersion: 'v1alpha1'; + requiredAPIs: Record; + endpoints: Record; +} diff --git a/src/handler-builder.ts b/src/handler-builder.ts index 7ebf78f15..d1286f275 100644 --- a/src/handler-builder.ts +++ b/src/handler-builder.ts @@ -70,6 +70,7 @@ export class HandlerBuilder { ): HttpsFunction => { const func = https._onRequestWithOptions(handler, {}); func.__trigger = {}; + func.__endpoint = {}; return func; }, onCall: ( @@ -80,6 +81,7 @@ export class HandlerBuilder { ): HttpsFunction => { const func = https._onCallWithOptions(handler, {}); func.__trigger = {}; + func.__endpoint = {}; return func; }, /** @hidden */ diff --git a/src/providers/https.ts b/src/providers/https.ts index ecefd6c29..9017095fd 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -22,12 +22,13 @@ import * as express from 'express'; -import { HttpsFunction, optionsToTrigger, Runnable } from '../cloud-functions'; import { - convertIfPresent, - convertInvoker, - copyIfPresent, -} from '../common/encoding'; + HttpsFunction, + optionsToEndpoint, + optionsToTrigger, + Runnable, +} from '../cloud-functions'; +import { convertIfPresent, convertInvoker } from '../common/encoding'; import { CallableContext, FunctionsErrorCode, @@ -167,6 +168,19 @@ export function _onRequestWithOptions( convertInvoker ); // TODO parse the options + + cloudFunction.__endpoint = { + platform: 'gcfv1', + ...optionsToEndpoint(options), + httpsTrigger: {}, + }; + convertIfPresent( + cloudFunction.__endpoint.httpsTrigger, + options, + 'invoker', + 'invoker', + convertInvoker + ); return cloudFunction; } @@ -195,6 +209,13 @@ export function _onCallWithOptions( }; func.__trigger.labels['deployment-callable'] = 'true'; + func.__endpoint = { + platform: 'gcfv1', + labels: {}, + ...optionsToEndpoint(options), + callableTrigger: {}, + }; + func.run = handler; return func; diff --git a/src/v2/core.ts b/src/v2/core.ts index d471c107b..c790be7a8 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -20,6 +20,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import { ManifestEndpoint } from '../common/manifest'; + /** @internal */ export interface TriggerAnnotation { concurrency?: number; @@ -91,7 +93,8 @@ export interface CloudEvent { export interface CloudFunction { (raw: CloudEvent): any | Promise; - __trigger: unknown; + __trigger?: unknown; + __endpoint: ManifestEndpoint; run(event: CloudEvent): any | Promise; } diff --git a/src/v2/options.ts b/src/v2/options.ts index 076cb8532..6591554ad 100644 --- a/src/v2/options.ts +++ b/src/v2/options.ts @@ -21,14 +21,16 @@ // SOFTWARE. import { + convertIfPresent, + copyIfPresent, durationFromSeconds, serviceAccountFromShorthand, } from '../common/encoding'; -import { convertIfPresent, copyIfPresent } from '../common/encoding'; import * as logger from '../logger'; import { TriggerAnnotation } from './core'; import { declaredParams } from './params'; import { ParamSpec } from './params/types'; +import { ManifestEndpoint } from '../common/manifest'; /** * List of all regions supported by Cloud Functions v2 @@ -279,6 +281,52 @@ export function optionsToTriggerAnnotations( return annotation; } +/** + * Apply GlobalOptions to endpoint manifest. + * @internal + */ +export function optionsToEndpoint( + opts: GlobalOptions | EventHandlerOptions +): ManifestEndpoint { + const endpoint: ManifestEndpoint = {}; + copyIfPresent( + endpoint, + opts, + 'concurrency', + 'minInstances', + 'maxInstances', + 'ingressSettings', + 'labels', + 'timeoutSeconds' + ); + convertIfPresent(endpoint, opts, 'serviceAccountEmail', 'serviceAccount'); + if (opts.vpcConnector) { + const vpc: ManifestEndpoint['vpc'] = { connector: opts.vpcConnector }; + convertIfPresent(vpc, opts, 'egressSettings', 'vpcConnectorEgressSettings'); + endpoint.vpc = vpc; + } + convertIfPresent(endpoint, opts, 'availableMemoryMb', 'memory', (mem) => { + const memoryLookup = { + '128MB': 128, + '256MB': 256, + '512MB': 512, + '1GB': 1024, + '2GB': 2048, + '4GB': 4096, + '8GB': 8192, + }; + return memoryLookup[mem]; + }); + convertIfPresent(endpoint, opts, 'region', 'region', (region) => { + if (typeof region === 'string') { + return [region]; + } + return region; + }); + + return endpoint; +} + /** * @hidden */ diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index 43556a1ba..292c7cf0e 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -28,6 +28,7 @@ import { copyIfPresent, } from '../../common/encoding'; +import * as options from '../options'; import { CallableRequest, FunctionsErrorCode, @@ -39,7 +40,7 @@ import { TaskRequest, TaskRetryConfig, } from '../../common/providers/https'; -import * as options from '../options'; +import { ManifestEndpoint } from '../../common/manifest'; export { Request, @@ -74,7 +75,10 @@ export interface TaskQueueOptions extends options.GlobalOptions { export type HttpsFunction = (( req: Request, res: express.Response -) => void | Promise) & { __trigger: unknown }; +) => void | Promise) & { + __trigger?: unknown; + __endpoint: ManifestEndpoint; +}; export interface CallableFunction extends HttpsFunction { run(data: CallableRequest): Return; } @@ -126,6 +130,7 @@ export function onRequest( }); }; } + Object.defineProperty(handler, '__trigger', { get: () => { const baseOpts = options.optionsToTriggerAnnotations( @@ -161,6 +166,30 @@ export function onRequest( 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. + const specificOpts = options.optionsToEndpoint(opts as options.GlobalOptions); + const endpoint: Partial = { + platform: 'gcfv2', + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + httpsTrigger: {}, + }; + convertIfPresent( + endpoint.httpsTrigger, + opts, + 'invoker', + 'invoker', + convertInvoker + ); + (handler as HttpsFunction).__endpoint = endpoint; + return handler as HttpsFunction; } @@ -222,6 +251,21 @@ export function onCall>( }, }); + 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); + func.__endpoint = { + platform: 'gcfv2', + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + callableTrigger: {}, + }; + func.run = handler; return func; } diff --git a/src/v2/providers/pubsub.ts b/src/v2/providers/pubsub.ts index cca25b294..81c4bca68 100644 --- a/src/v2/providers/pubsub.ts +++ b/src/v2/providers/pubsub.ts @@ -1,5 +1,7 @@ -import { CloudEvent, CloudFunction } from '../core'; import * as options from '../options'; +import { CloudEvent, CloudFunction } from '../core'; +import { copyIfPresent } from '../../common/encoding'; +import { ManifestEndpoint } from '../../common/manifest'; /** * Interface representing a Google Cloud Pub/Sub message. @@ -133,7 +135,7 @@ export function onMessagePublished( func.run = handler; - // TypeScript doesn't recongize defineProperty as adding a property and complains + // TypeScript doesn't recognize defineProperty as adding a property and complains // that __trigger 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. func.__trigger = 'silence the transpiler'; @@ -164,5 +166,25 @@ export function onMessagePublished( }, }); + const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); + const specificOpts = options.optionsToEndpoint(opts); + + const endpoint: ManifestEndpoint = { + platform: 'gcfv2', + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + eventTrigger: { + eventType: 'google.cloud.pubsub.topic.v1.messagePublished', + eventFilters: { topic }, + retry: false, + }, + }; + copyIfPresent(endpoint.eventTrigger, opts, 'retry', 'retry'); + func.__endpoint = endpoint; + return func; } diff --git a/src/v2/providers/storage.ts b/src/v2/providers/storage.ts index e58405820..c092d25a2 100644 --- a/src/v2/providers/storage.ts +++ b/src/v2/providers/storage.ts @@ -20,9 +20,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import * as options from '../options'; import { firebaseConfig } from '../../config'; import { CloudEvent, CloudFunction } from '../core'; -import * as options from '../options'; +import { copyIfPresent } from '../../common/encoding'; +import { ManifestEndpoint } from '../../common/manifest'; /** * An object within Google Cloud Storage. @@ -313,10 +315,11 @@ export function onOperation( func.run = handler; - // TypeScript doesn't recongize defineProperty as adding a property and complains - // that __trigger doesn't exist. We can either cast to any and lose all type safety + // 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. func.__trigger = 'silence the transpiler'; + func.__endpoint = {} as ManifestEndpoint; Object.defineProperty(func, '__trigger', { get: () => { @@ -341,6 +344,34 @@ export function onOperation( }, }); + // SDK may attempt to read FIREBASE_CONFIG env var to fetch the default bucket name. + // To prevent runtime errors when FIREBASE_CONFIG env var is missing, we use getters. + Object.defineProperty(func, '__endpoint', { + get: () => { + const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); + const specificOpts = options.optionsToEndpoint(opts); + + const endpoint: ManifestEndpoint = { + platform: 'gcfv2', + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + eventTrigger: { + eventType: eventType, + eventFilters: { + bucket, + }, + retry: false, + }, + }; + copyIfPresent(endpoint.eventTrigger, opts, 'retry', 'retry'); + return endpoint; + }, + }); + return func; } From 5f2fa9041c5a80e384c18d0075c732b23368e698 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 22 Nov 2021 13:17:12 -0800 Subject: [PATCH 2/3] Add missing import. --- src/providers/https.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/https.ts b/src/providers/https.ts index 9017095fd..1b5cfaa4d 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -28,7 +28,7 @@ import { optionsToTrigger, Runnable, } from '../cloud-functions'; -import { convertIfPresent, convertInvoker } from '../common/encoding'; +import {convertIfPresent, convertInvoker, copyIfPresent} from '../common/encoding'; import { CallableContext, FunctionsErrorCode, From 9a2cd47d8e9ec5232c1fab5ecce4e6cc761359d4 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 9 Dec 2021 21:18:32 -0800 Subject: [PATCH 3/3] More of annotate endpoint property for functions (#1009) Follows up https://github.com/firebase/firebase-functions/pull/999 to annotate each funuctions with `__endpoint` property. Highlight of changes: * Extend unit test coverage for all v1 providers * Add `__endpoint` annotation to v1 task queue functions * Add `__requiredAPIs` annotation to task queue and scheduler functions * Explicitly set `__endpoint` to undefined in the handler namespace * No SDK-level label setting in the __endpoint annotation. --- spec/v1/cloud-functions.spec.ts | 5 + spec/v1/providers/analytics.spec.ts | 22 +- spec/v1/providers/auth.spec.ts | 85 ++++++-- spec/v1/providers/database.spec.ts | 184 +++++++++++----- spec/v1/providers/firestore.spec.ts | 72 ++++--- spec/v1/providers/https.spec.ts | 44 +++- spec/v1/providers/pubsub.spec.ts | 100 ++++++++- spec/v1/providers/remoteConfig.spec.ts | 43 ++-- spec/v1/providers/storage.spec.ts | 278 +++++++++++++++---------- spec/v1/providers/testLab.spec.ts | 21 +- spec/v2/providers/https.spec.ts | 5 +- src/cloud-functions.ts | 26 ++- src/common/manifest.ts | 10 +- src/handler-builder.ts | 70 ++++--- src/providers/https.ts | 31 ++- src/v2/providers/pubsub.ts | 5 - src/v2/providers/storage.ts | 11 +- 17 files changed, 711 insertions(+), 301 deletions(-) diff --git a/spec/v1/cloud-functions.spec.ts b/spec/v1/cloud-functions.spec.ts index 5c861f3d6..c894bfa23 100644 --- a/spec/v1/cloud-functions.spec.ts +++ b/spec/v1/cloud-functions.spec.ts @@ -67,6 +67,7 @@ describe('makeCloudFunction', () => { }, retry: false, }, + labels: {}, }); }); @@ -90,6 +91,7 @@ describe('makeCloudFunction', () => { }, retry: false, }, + labels: {}, }); }); @@ -121,6 +123,7 @@ describe('makeCloudFunction', () => { }, retry: false, }, + labels: {}, }); }); @@ -143,6 +146,7 @@ describe('makeCloudFunction', () => { }, retry: true, }, + labels: {}, }); }); @@ -165,6 +169,7 @@ describe('makeCloudFunction', () => { expect(cf.__endpoint).to.deep.equal({ platform: 'gcfv1', scheduleTrigger: schedule, + labels: {}, }); }); diff --git a/spec/v1/providers/analytics.spec.ts b/spec/v1/providers/analytics.spec.ts index d92244439..24eb0a008 100644 --- a/spec/v1/providers/analytics.spec.ts +++ b/spec/v1/providers/analytics.spec.ts @@ -50,10 +50,14 @@ describe('Analytics Functions', () => { 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); }); describe('#onLog', () => { - it('should return a TriggerDefinition with appropriate values', () => { + it('should return a trigger/endpoint with appropriate values', () => { const cloudFunction = analytics.event('first_open').onLog(() => null); expect(cloudFunction.__trigger).to.deep.equal({ @@ -64,6 +68,19 @@ describe('Analytics Functions', () => { service: 'app-measurement.com', }, }); + + expect(cloudFunction.__endpoint).to.deep.equal({ + platform: 'gcfv1', + eventTrigger: { + eventFilters: { + resource: 'projects/project1/events/first_open', + }, + eventType: + 'providers/google.firebase.analytics/eventTypes/event.log', + retry: false, + }, + labels: {}, + }); }); }); @@ -305,11 +322,12 @@ describe('Analytics Functions', () => { describe('handler namespace', () => { describe('#onLog', () => { - it('should return an empty trigger', () => { + it('should return an empty trigger/endpoint', () => { const cloudFunction = functions.handler.analytics.event.onLog( () => null ); expect(cloudFunction.__trigger).to.deep.equal({}); + expect(cloudFunction.__endpoint).to.undefined; }); it('should handle an event with the appropriate fields', () => { diff --git a/spec/v1/providers/auth.spec.ts b/spec/v1/providers/auth.spec.ts index e22052573..6168cf767 100644 --- a/spec/v1/providers/auth.spec.ts +++ b/spec/v1/providers/auth.spec.ts @@ -51,12 +51,38 @@ 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 { + platform: 'gcfv1', + eventTrigger: { + eventFilters: { + resource: `projects/${project}`, + }, + eventType: `providers/firebase.auth/eventTypes/${eventType}`, + retry: false, + }, + labels: {}, + }; + } + const handler = (user: firebase.auth.UserRecord) => { return Promise.resolve(); }; + const project = 'project1'; + before(() => { - process.env.GCLOUD_PROJECT = 'project1'; + process.env.GCLOUD_PROJECT = project; }); after(() => { @@ -76,31 +102,37 @@ describe('Auth Functions', () => { 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); }); describe('#onCreate', () => { - it('should return a TriggerDefinition with appropriate values', () => { + it('should return a trigger/endpoint with appropriate values', () => { const cloudFunction = auth.user().onCreate(() => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/firebase.auth/eventTypes/user.create', - resource: 'projects/project1', - service: 'firebaseauth.googleapis.com', - }, - }); + + expect(cloudFunction.__trigger).to.deep.equal( + expectedTrigger(project, 'user.create') + ); + + expect(cloudFunction.__endpoint).to.deep.equal( + expectedEndpoint(project, 'user.create') + ); }); }); describe('#onDelete', () => { - it('should return a TriggerDefinition with appropriate values', () => { + it('should return a trigger/endpoint with appropriate values', () => { const cloudFunction = auth.user().onDelete(handler); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/firebase.auth/eventTypes/user.delete', - resource: 'projects/project1', - service: 'firebaseauth.googleapis.com', - }, - }); + + expect(cloudFunction.__trigger).to.deep.equal( + expectedTrigger(project, 'user.delete') + ); + + expect(cloudFunction.__endpoint).to.deep.equal( + expectedEndpoint(project, 'user.delete') + ); }); }); @@ -198,6 +230,11 @@ describe('Auth Functions', () => { const cloudFunction = functions.handler.auth.user.onCreate(() => null); expect(cloudFunction.__trigger).to.deep.equal({}); }); + + it('should return an empty endpoint', () => { + const cloudFunction = functions.handler.auth.user.onCreate(() => null); + expect(cloudFunction.__endpoint).to.be.undefined; + }); }); describe('#onDelete', () => { @@ -206,13 +243,15 @@ describe('Auth Functions', () => { ); it('should return an empty trigger', () => { - const handler = (user: firebase.auth.UserRecord) => { - return Promise.resolve(); - }; - const cloudFunction = functions.handler.auth.user.onDelete(handler); + const cloudFunction = functions.handler.auth.user.onDelete(() => null); expect(cloudFunction.__trigger).to.deep.equal({}); }); + it('should return an empty endpoint', () => { + const cloudFunction = functions.handler.auth.user.onDelete(() => null); + expect(cloudFunction.__endpoint).to.be.undefined; + }); + it('should handle wire format as of v5.0.0 of firebase-admin', () => { return cloudFunctionDelete(event.data, event.context).then( (data: any) => { @@ -237,6 +276,10 @@ describe('Auth Functions', () => { 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); + }); + it('should not throw when #run is called', () => { const cf = auth.user().onCreate(() => null); expect(cf.run).to.not.throw(Error); diff --git a/spec/v1/providers/database.spec.ts b/spec/v1/providers/database.spec.ts index d49c27854..f932dd871 100644 --- a/spec/v1/providers/database.spec.ts +++ b/spec/v1/providers/database.spec.ts @@ -31,6 +31,30 @@ 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 { + platform: 'gcfv1', + eventTrigger: { + eventFilters: { + resource, + }, + eventType: `providers/google.firebase.database/eventTypes/${eventType}`, + retry: false, + }, + labels: {}, + }; + } + before(() => { (config as any).firebaseConfigCache = { databaseURL: 'https://subdomain.apse.firebasedatabase.app', @@ -56,21 +80,29 @@ describe('Database Functions', () => { 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); }); describe('#onWrite()', () => { - it('should return "ref.write" as the event type', () => { - const eventType = database.ref('foo').onWrite(() => null).__trigger - .eventTrigger.eventType; - expect(eventType).to.eq( - 'providers/google.firebase.database/eventTypes/ref.write' + 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' + ) ); - }); - it('should construct a proper resource path', () => { - const resource = database.ref('foo').onWrite(() => null).__trigger - .eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); + expect(func.__endpoint).to.deep.equal( + expectedEndpoint( + 'projects/_/instances/subdomain/refs/foo', + 'ref.write' + ) + ); }); it('should let developers choose a database instance', () => { @@ -78,8 +110,14 @@ describe('Database Functions', () => { .instance('custom') .ref('foo') .onWrite(() => null); - const resource = func.__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + + 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') + ); }); it('should return a handler that emits events with a proper DataSnapshot', () => { @@ -107,18 +145,22 @@ describe('Database Functions', () => { }); describe('#onCreate()', () => { - it('should return "ref.create" as the event type', () => { - const eventType = database.ref('foo').onCreate(() => null).__trigger - .eventTrigger.eventType; - expect(eventType).to.eq( - 'providers/google.firebase.database/eventTypes/ref.create' + 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' + ) ); - }); - it('should construct a proper resource path', () => { - const resource = database.ref('foo').onCreate(() => null).__trigger - .eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); + expect(func.__endpoint).to.deep.equal( + expectedEndpoint( + 'projects/_/instances/subdomain/refs/foo', + 'ref.create' + ) + ); }); it('should let developers choose a database instance', () => { @@ -126,8 +168,14 @@ describe('Database Functions', () => { .instance('custom') .ref('foo') .onCreate(() => null); - const resource = func.__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + + 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') + ); }); it('should return a handler that emits events with a proper DataSnapshot', () => { @@ -156,18 +204,22 @@ describe('Database Functions', () => { }); describe('#onUpdate()', () => { - it('should return "ref.update" as the event type', () => { - const eventType = database.ref('foo').onUpdate(() => null).__trigger - .eventTrigger.eventType; - expect(eventType).to.eq( - 'providers/google.firebase.database/eventTypes/ref.update' + 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' + ) ); - }); - it('should construct a proper resource path', () => { - const resource = database.ref('foo').onUpdate(() => null).__trigger - .eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); + expect(func.__endpoint).to.deep.equal( + expectedEndpoint( + 'projects/_/instances/subdomain/refs/foo', + 'ref.update' + ) + ); }); it('should let developers choose a database instance', () => { @@ -175,8 +227,14 @@ describe('Database Functions', () => { .instance('custom') .ref('foo') .onUpdate(() => null); - const resource = func.__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + + 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') + ); }); it('should return a handler that emits events with a proper DataSnapshot', () => { @@ -205,18 +263,22 @@ describe('Database Functions', () => { }); describe('#onDelete()', () => { - it('should return "ref.delete" as the event type', () => { - const eventType = database.ref('foo').onDelete(() => null).__trigger - .eventTrigger.eventType; - expect(eventType).to.eq( - 'providers/google.firebase.database/eventTypes/ref.delete' + 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' + ) ); - }); - it('should construct a proper resource path', () => { - const resource = database.ref('foo').onDelete(() => null).__trigger - .eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); + expect(func.__endpoint).to.deep.equal( + expectedEndpoint( + 'projects/_/instances/subdomain/refs/foo', + 'ref.delete' + ) + ); }); it('should let developers choose a database instance', () => { @@ -224,8 +286,14 @@ describe('Database Functions', () => { .instance('custom') .ref('foo') .onDelete(() => null); - const resource = func.__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + + 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') + ); }); it('should return a handler that emits events with a proper DataSnapshot', () => { @@ -259,6 +327,7 @@ describe('Database Functions', () => { it('correctly sets trigger to {}', () => { const cf = functions.handler.database.ref.onWrite(() => null); expect(cf.__trigger).to.deep.equal({}); + expect(cf.__endpoint).to.be.undefined; }); it('should be able to use the instance entry point', () => { @@ -266,6 +335,7 @@ describe('Database Functions', () => { () => null ); expect(func.__trigger).to.deep.equal({}); + expect(func.__endpoint).to.be.undefined; }); it('should return a handler that emits events with a proper DataSnapshot', () => { @@ -296,7 +366,8 @@ describe('Database Functions', () => { describe('#onCreate()', () => { it('correctly sets trigger to {}', () => { const cf = functions.handler.database.ref.onCreate(() => null); - return expect(cf.__trigger).to.deep.equal({}); + expect(cf.__trigger).to.deep.equal({}); + expect(cf.__endpoint).to.be.undefined; }); it('should be able to use the instance entry point', () => { @@ -304,6 +375,7 @@ describe('Database Functions', () => { () => null ); expect(func.__trigger).to.deep.equal({}); + expect(func.__endpoint).to.be.undefined; }); it('should return a handler that emits events with a proper DataSnapshot', () => { @@ -333,7 +405,8 @@ describe('Database Functions', () => { describe('#onUpdate()', () => { it('correctly sets trigger to {}', () => { const cf = functions.handler.database.ref.onUpdate(() => null); - return expect(cf.__trigger).to.deep.equal({}); + expect(cf.__trigger).to.deep.equal({}); + expect(cf.__endpoint).to.be.undefined; }); it('should be able to use the instance entry point', () => { @@ -341,6 +414,7 @@ describe('Database Functions', () => { () => null ); expect(func.__trigger).to.deep.equal({}); + expect(func.__endpoint).to.be.undefined; }); it('should return a handler that emits events with a proper DataSnapshot', () => { @@ -370,7 +444,8 @@ describe('Database Functions', () => { describe('#onDelete()', () => { it('correctly sets trigger to {}', () => { const cf = functions.handler.database.ref.onDelete(() => null); - return expect(cf.__trigger).to.deep.equal({}); + expect(cf.__trigger).to.deep.equal({}); + expect(cf.__endpoint).to.be.undefined; }); it('should be able to use the instance entry point', () => { @@ -378,6 +453,7 @@ describe('Database Functions', () => { () => null ); expect(func.__trigger).to.deep.equal({}); + expect(func.__endpoint).to.be.undefined; }); it('should return a handler that emits events with a proper DataSnapshot', () => { @@ -419,6 +495,12 @@ describe('Database Functions', () => { ).to.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); diff --git a/spec/v1/providers/firestore.spec.ts b/spec/v1/providers/firestore.spec.ts index 25cffaf6c..3f9f07fbe 100644 --- a/spec/v1/providers/firestore.spec.ts +++ b/spec/v1/providers/firestore.spec.ts @@ -103,6 +103,20 @@ describe('Firestore Functions', () => { }; } + function expectedEndpoint(resource: string, eventType: string) { + return { + platform: 'gcfv1', + eventTrigger: { + eventFilters: { + resource, + }, + eventType: `providers/cloud.firestore/eventTypes/${eventType}`, + retry: false, + }, + labels: {}, + }; + } + before(() => { process.env.GCLOUD_PROJECT = 'project1'; }); @@ -117,9 +131,14 @@ describe('Firestore Functions', () => { const cloudFunction = firestore .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') + ); }); it('should allow custom namespaces', () => { @@ -129,9 +148,14 @@ describe('Firestore Functions', () => { .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') + ); }); it('should allow custom databases', () => { @@ -140,9 +164,14 @@ describe('Firestore Functions', () => { .database('myDB') .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') + ); }); it('should allow both custom database and namespace', () => { @@ -153,9 +182,14 @@ describe('Firestore Functions', () => { .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') + ); }); it('should allow both region and runtime options to be set', () => { @@ -171,36 +205,10 @@ describe('Firestore Functions', () => { 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'); - }); - - it('onCreate should have the "document.create" eventType', () => { - const resource = - 'projects/project1/databases/(default)/documents/users/{uid}'; - const eventType = firestore.document('users/{uid}').onCreate(() => null) - .__trigger.eventTrigger.eventType; - expect(eventType).to.eq( - expectedTrigger(resource, 'document.create').eventTrigger.eventType - ); - }); - it('onUpdate should have the "document.update" eventType', () => { - const resource = - 'projects/project1/databases/(default)/documents/users/{uid}'; - const eventType = firestore.document('users/{uid}').onUpdate(() => null) - .__trigger.eventTrigger.eventType; - expect(eventType).to.eq( - expectedTrigger(resource, 'document.update').eventTrigger.eventType - ); - }); - - it('onDelete should have the "document.delete" eventType', () => { - const resource = - 'projects/project1/databases/(default)/documents/users/{uid}'; - const eventType = firestore.document('users/{uid}').onDelete(() => null) - .__trigger.eventTrigger.eventType; - expect(eventType).to.eq( - expectedTrigger(resource, 'document.delete').eventTrigger.eventType - ); + 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); }); }); @@ -217,6 +225,12 @@ describe('Firestore Functions', () => { ).to.throw(Error); }); + it('should throw when endpoint is accessed', () => { + expect( + () => firestore.document('input').onCreate(() => null).__endpoint + ).to.throw(Error); + }); + it('should not throw when #run is called', () => { const cf = firestore.document('input').onCreate(() => null); expect(cf.run).to.not.throw(Error); diff --git a/spec/v1/providers/https.spec.ts b/spec/v1/providers/https.spec.ts index ddc9c670d..22e6d52fd 100644 --- a/spec/v1/providers/https.spec.ts +++ b/spec/v1/providers/https.spec.ts @@ -94,7 +94,7 @@ function runHandler( describe('CloudHttpsBuilder', () => { describe('#onRequest', () => { - it('should return a trigger/endpoint with appropriate values', () => { + it('should return a trigger with appropriate values', () => { const result = https.onRequest((req, resp) => { resp.send(200); }); @@ -130,20 +130,20 @@ describe('CloudHttpsBuilder', () => { describe('handler namespace', () => { describe('#onRequest', () => { - it('should return an empty trigger and endpoint', () => { + it('should return an empty trigger', () => { const result = functions.handler.https.onRequest((req, res) => { res.send(200); }); expect(result.__trigger).to.deep.equal({}); - expect(result.__endpoint).to.deep.equal({}); + expect(result.__endpoint).to.be.undefined; }); }); describe('#onCall', () => { - it('should return an empty trigger and endpoint', () => { + it('should return an empty trigger', () => { const result = functions.handler.https.onCall(() => null); expect(result.__trigger).to.deep.equal({}); - expect(result.__endpoint).to.deep.equal({}); + expect(result.__endpoint).to.be.undefined; }); }); @@ -151,6 +151,7 @@ describe('handler namespace', () => { it('should return an empty trigger', () => { const result = functions.handler.https.taskQueue.onEnqueue(() => null); expect(result.__trigger).to.deep.equal({}); + expect(result.__endpoint).to.be.undefined; }); }); }); @@ -231,7 +232,7 @@ describe('#onCall', () => { }); describe('#onEnqueue', () => { - it('should return a Trigger with appropriate values', () => { + it('should return a trigger/endpoint with appropriate values', () => { const result = https .taskQueue({ rateLimits: { @@ -248,6 +249,7 @@ describe('#onEnqueue', () => { invoker: 'private', }) .onDispatch(() => {}); + expect(result.__trigger).to.deep.equal({ taskQueueTrigger: { rateLimits: { @@ -264,6 +266,24 @@ describe('#onEnqueue', () => { invoker: ['private'], }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv1', + taskQueueTrigger: { + rateLimits: { + maxBurstSize: 20, + maxConcurrentDispatches: 30, + maxDispatchesPerSecond: 40, + }, + retryConfig: { + maxAttempts: 5, + maxBackoffSeconds: 20, + maxDoublings: 3, + minBackoffSeconds: 5, + }, + invoker: ['private'], + }, + }); }); it('should allow both region and runtime options to be set', () => { @@ -286,6 +306,18 @@ describe('#onEnqueue', () => { }, }, }); + + expect(fn.__endpoint).to.deep.equal({ + platform: 'gcfv1', + region: ['us-east1'], + availableMemoryMb: 256, + timeoutSeconds: 90, + taskQueueTrigger: { + retryConfig: { + maxAttempts: 5, + }, + }, + }); }); it('has a .run method', async () => { diff --git a/spec/v1/providers/pubsub.spec.ts b/spec/v1/providers/pubsub.spec.ts index 1ab8c7ba4..5547515ab 100644 --- a/spec/v1/providers/pubsub.spec.ts +++ b/spec/v1/providers/pubsub.spec.ts @@ -85,12 +85,17 @@ describe('Pubsub Functions', () => { 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); }); describe('#onPublish', () => { - it('should return a TriggerDefinition with appropriate values', () => { + it('should return a trigger/endpoint with appropriate values', () => { // 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', @@ -98,6 +103,18 @@ describe('Pubsub Functions', () => { service: 'pubsub.googleapis.com', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv1', + eventTrigger: { + eventType: 'google.pubsub.topic.publish', + eventFilters: { + resource: 'projects/project1/topics/toppy', + }, + retry: false, + }, + labels: {}, + }); }); it('should throw with improperly formatted topics', () => { @@ -143,27 +160,38 @@ describe('Pubsub Functions', () => { }); describe('#schedule', () => { - it('should return a trigger with schedule', () => { + it('should return a trigger/endpoint with schedule', () => { const result = pubsub .schedule('every 5 minutes') .onRun((context) => null); + expect(result.__trigger.schedule).to.deep.equal({ schedule: 'every 5 minutes', }); + + expect(result.__endpoint.scheduleTrigger).to.deep.equal({ + schedule: 'every 5 minutes', + }); }); - it('should return a trigger with schedule and timeZone when one is chosen', () => { + it('should return a trigger/endpoint with schedule and timeZone when one is chosen', () => { const result = pubsub .schedule('every 5 minutes') .timeZone('America/New_York') .onRun((context) => null); + expect(result.__trigger.schedule).to.deep.equal({ schedule: 'every 5 minutes', timeZone: 'America/New_York', }); + + expect(result.__endpoint.scheduleTrigger).to.deep.equal({ + schedule: 'every 5 minutes', + timeZone: 'America/New_York', + }); }); - it('should return a trigger with schedule and retry config when called with retryConfig', () => { + it('should return a trigger/endpoint with schedule and retry config when called with retryConfig', () => { const retryConfig = { retryCount: 3, maxRetryDuration: '10 minutes', @@ -175,6 +203,7 @@ describe('Pubsub Functions', () => { .schedule('every 5 minutes') .retryConfig(retryConfig) .onRun(() => null); + expect(result.__trigger.schedule).to.deep.equal({ schedule: 'every 5 minutes', retryConfig, @@ -182,10 +211,16 @@ describe('Pubsub Functions', () => { expect(result.__trigger.labels).to.deep.equal({ 'deployment-scheduled': 'true', }); + + expect(result.__endpoint.scheduleTrigger).to.deep.equal({ + schedule: 'every 5 minutes', + retryConfig, + }); + expect(result.__endpoint.labels).to.be.empty; }); it( - 'should return a trigger with schedule, timeZone and retry config' + + 'should return a trigger/endpoint with schedule, timeZone and retry config' + 'when called with retryConfig and timeout', () => { const retryConfig = { @@ -200,6 +235,7 @@ describe('Pubsub Functions', () => { .timeZone('America/New_York') .retryConfig(retryConfig) .onRun(() => null); + expect(result.__trigger.schedule).to.deep.equal({ schedule: 'every 5 minutes', retryConfig, @@ -208,10 +244,17 @@ describe('Pubsub Functions', () => { expect(result.__trigger.labels).to.deep.equal({ 'deployment-scheduled': 'true', }); + + expect(result.__endpoint.scheduleTrigger).to.deep.equal({ + schedule: 'every 5 minutes', + retryConfig, + timeZone: 'America/New_York', + }); + expect(result.__endpoint.labels).to.be.empty; } ); - it('should return an appropriate trigger when called with region and options', () => { + it('should return an appropriate trigger/endpoint when called with region and options', () => { const result = functions .region('us-east1') .runWith({ @@ -226,9 +269,16 @@ describe('Pubsub Functions', () => { 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({ + schedule: 'every 5 minutes', + }); + expect(result.__endpoint.region).to.deep.equal(['us-east1']); + expect(result.__endpoint.availableMemoryMb).to.deep.equal(256); + expect(result.__endpoint.timeoutSeconds).to.deep.equal(90); }); - it('should return an appropriate trigger when called with region, timeZone, and options', () => { + it('should return an appropriate trigger/endpoint when called with region, timeZone, and options', () => { const result = functions .region('us-east1') .runWith({ @@ -245,9 +295,17 @@ describe('Pubsub Functions', () => { 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({ + schedule: 'every 5 minutes', + timeZone: 'America/New_York', + }); + expect(result.__endpoint.region).to.deep.equal(['us-east1']); + expect(result.__endpoint.availableMemoryMb).to.deep.equal(256); + expect(result.__endpoint.timeoutSeconds).to.deep.equal(90); }); - it('should return an appropriate trigger when called with region, options and retryConfig', () => { + it('should return an appropriate trigger/endpoint when called with region, options and retryConfig', () => { const retryConfig = { retryCount: 3, maxRetryDuration: '10 minutes', @@ -274,9 +332,17 @@ describe('Pubsub Functions', () => { 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({ + schedule: 'every 5 minutes', + retryConfig, + }); + expect(result.__endpoint.region).to.deep.equal(['us-east1']); + expect(result.__endpoint.availableMemoryMb).to.deep.equal(256); + expect(result.__endpoint.timeoutSeconds).to.deep.equal(90); }); - it('should return an appropriate trigger when called with region, options, retryConfig, and timeZone', () => { + it('should return an appropriate trigger/endpoint when called with region, options, retryConfig, and timeZone', () => { const retryConfig = { retryCount: 3, maxRetryDuration: '10 minutes', @@ -305,6 +371,15 @@ describe('Pubsub Functions', () => { 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({ + schedule: 'every 5 minutes', + timeZone: 'America/New_York', + retryConfig, + }); + expect(result.__endpoint.region).to.deep.equal(['us-east1']); + expect(result.__endpoint.availableMemoryMb).to.deep.equal(256); + expect(result.__endpoint.timeoutSeconds).to.deep.equal(90); }); }); }); @@ -315,6 +390,7 @@ describe('Pubsub Functions', () => { it('should return an empty trigger', () => { const result = functions.handler.pubsub.topic.onPublish(() => null); expect(result.__trigger).to.deep.equal({}); + expect(result.__endpoint).to.be.undefined; }); it('should properly handle a new-style event', () => { @@ -406,6 +482,12 @@ describe('Pubsub Functions', () => { ).to.throw(Error); }); + it('should throw when endpoint is accessed', () => { + expect( + () => pubsub.topic('toppy').onPublish(() => null).__endpoint + ).to.throw(Error); + }); + it('should not throw when #run is called', () => { const cf = pubsub.topic('toppy').onPublish(() => null); expect(cf.run).to.not.throw(Error); diff --git a/spec/v1/providers/remoteConfig.spec.ts b/spec/v1/providers/remoteConfig.spec.ts index f7221cf99..f3fde2043 100644 --- a/spec/v1/providers/remoteConfig.spec.ts +++ b/spec/v1/providers/remoteConfig.spec.ts @@ -20,13 +20,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. import { expect } from 'chai'; -import * as _ from 'lodash'; import { CloudFunction, Event, EventContext, - TriggerAnnotated, } from '../../../src/cloud-functions'; import * as functions from '../../../src/index'; import * as remoteConfig from '../../../src/providers/remoteConfig'; @@ -47,18 +45,6 @@ describe('RemoteConfig Functions', () => { } describe('#onUpdate', () => { - function expectedTrigger(): TriggerAnnotated { - return { - __trigger: { - eventTrigger: { - resource: 'projects/project1', - eventType: 'google.firebase.remoteconfig.update', - service: 'firebaseremoteconfig.googleapis.com', - }, - }, - }; - } - before(() => { process.env.GCLOUD_PROJECT = 'project1'; }); @@ -69,9 +55,26 @@ describe('RemoteConfig Functions', () => { it('should have the correct trigger', () => { const cloudFunction = remoteConfig.onUpdate(() => null); - expect(cloudFunction.__trigger).to.deep.equal( - expectedTrigger().__trigger - ); + + 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({ + platform: 'gcfv1', + eventTrigger: { + eventType: 'google.firebase.remoteconfig.update', + eventFilters: { + resource: 'projects/project1', + }, + retry: false, + }, + labels: {}, + }); }); it('should allow both region and runtime options to be set', () => { @@ -86,6 +89,10 @@ describe('RemoteConfig Functions', () => { 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); }); }); @@ -135,7 +142,9 @@ describe('RemoteConfig Functions', () => { const cloudFunction = functions.handler.remoteConfig.onUpdate( () => null ); + expect(cloudFunction.__trigger).to.deep.equal({}); + expect(cloudFunction.__endpoint).to.be.undefined; }); it('should correctly unwrap the event', () => { diff --git a/spec/v1/providers/storage.spec.ts b/spec/v1/providers/storage.spec.ts index 02762630d..d96b131b9 100644 --- a/spec/v1/providers/storage.spec.ts +++ b/spec/v1/providers/storage.spec.ts @@ -28,9 +28,35 @@ import * as storage from '../../../src/providers/storage'; 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 { + platform: 'gcfv1', + eventTrigger: { + eventFilters: { + resource: `projects/_/buckets/${bucket}`, + }, + eventType: `google.storage.object.${eventType}`, + retry: false, + }, + labels: {}, + }; + } + + const defaultBucket = 'bucket'; + before(() => { (config as any).firebaseConfigCache = { - storageBucket: 'bucket', + storageBucket: defaultBucket, }; }); @@ -51,6 +77,10 @@ describe('Storage Functions', () => { 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); }); describe('#onArchive', () => { @@ -59,24 +89,26 @@ describe('Storage Functions', () => { .bucket('bucky') .object() .onArchive(() => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'google.storage.object.archive', - resource: 'projects/_/buckets/bucky', - service: 'storage.googleapis.com', - }, - }); + + 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({ - eventTrigger: { - eventType: 'google.storage.object.archive', - resource: 'projects/_/buckets/bucket', - service: 'storage.googleapis.com', - }, - }); + + expect(cloudFunction.__trigger).to.deep.equal( + expectedTrigger(defaultBucket, 'archive') + ); + + expect(cloudFunction.__endpoint).to.deep.equal( + expectedEndpoint(defaultBucket, 'archive') + ); }); it('should allow fully qualified bucket names', () => { @@ -85,13 +117,14 @@ describe('Storage Functions', () => { {} ); const result = subjectQualified.onArchive(() => null); - expect(result.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'google.storage.object.archive', - resource: 'projects/_/buckets/bucky', - service: 'storage.googleapis.com', - }, - }); + + 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', () => { @@ -102,6 +135,14 @@ describe('Storage Functions', () => { .object() .onArchive(() => null).__trigger ).to.throw(Error); + + expect( + () => + storage + .bucket('bad/bucket/format') + .object() + .onArchive(() => null).__endpoint + ).to.throw(Error); }); it('should not mess with media links using non-literal slashes', () => { @@ -139,24 +180,26 @@ describe('Storage Functions', () => { .bucket('bucky') .object() .onDelete(() => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'google.storage.object.delete', - resource: 'projects/_/buckets/bucky', - service: 'storage.googleapis.com', - }, - }); + + 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({ - eventTrigger: { - eventType: 'google.storage.object.delete', - resource: 'projects/_/buckets/bucket', - service: 'storage.googleapis.com', - }, - }); + + expect(cloudFunction.__trigger).to.deep.equal( + expectedTrigger(defaultBucket, 'delete') + ); + + expect(cloudFunction.__endpoint).to.deep.equal( + expectedEndpoint(defaultBucket, 'delete') + ); }); it('should allow fully qualified bucket names', () => { @@ -165,23 +208,25 @@ describe('Storage Functions', () => { {} ); const result = subjectQualified.onDelete(() => null); - expect(result.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'google.storage.object.delete', - resource: 'projects/_/buckets/bucky', - service: 'storage.googleapis.com', - }, - }); + + expect(result.__trigger).to.deep.equal( + expectedTrigger('bucky', 'delete') + ); + + expect(result.__endpoint).to.deep.equal( + expectedEndpoint('bucky', 'delete') + ); }); it('should throw with improperly formatted buckets', () => { - expect( - () => - storage - .bucket('bad/bucket/format') - .object() - .onDelete(() => null).__trigger - ).to.throw(Error); + const fn = storage + .bucket('bad/bucket/format') + .object() + .onDelete(() => null); + + expect(() => fn.__trigger).to.throw(Error); + + expect(() => fn.__endpoint).to.throw(Error); }); it('should not mess with media links using non-literal slashes', () => { @@ -219,24 +264,26 @@ describe('Storage Functions', () => { .bucket('bucky') .object() .onFinalize(() => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'google.storage.object.finalize', - resource: 'projects/_/buckets/bucky', - service: 'storage.googleapis.com', - }, - }); + + 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({ - eventTrigger: { - eventType: 'google.storage.object.finalize', - resource: 'projects/_/buckets/bucket', - service: 'storage.googleapis.com', - }, - }); + + expect(cloudFunction.__trigger).to.deep.equal( + expectedTrigger(defaultBucket, 'finalize') + ); + + expect(cloudFunction.__endpoint).to.deep.equal( + expectedEndpoint(defaultBucket, 'finalize') + ); }); it('should allow fully qualified bucket names', () => { @@ -245,23 +292,25 @@ describe('Storage Functions', () => { {} ); const result = subjectQualified.onFinalize(() => null); - expect(result.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'google.storage.object.finalize', - resource: 'projects/_/buckets/bucky', - service: 'storage.googleapis.com', - }, - }); + + expect(result.__trigger).to.deep.equal( + expectedTrigger('bucky', 'finalize') + ); + + expect(result.__endpoint).to.deep.equal( + expectedEndpoint('bucky', 'finalize') + ); }); it('should throw with improperly formatted buckets', () => { - expect( - () => - storage - .bucket('bad/bucket/format') - .object() - .onFinalize(() => null).__trigger - ).to.throw(Error); + const fn = storage + .bucket('bad/bucket/format') + .object() + .onFinalize(() => null); + + expect(() => fn.__trigger).to.throw(Error); + + expect(() => fn.__endpoint).to.throw(Error); }); it('should not mess with media links using non-literal slashes', () => { @@ -299,24 +348,26 @@ describe('Storage Functions', () => { .bucket('bucky') .object() .onMetadataUpdate(() => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'google.storage.object.metadataUpdate', - resource: 'projects/_/buckets/bucky', - service: 'storage.googleapis.com', - }, - }); + + 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({ - eventTrigger: { - eventType: 'google.storage.object.metadataUpdate', - resource: 'projects/_/buckets/bucket', - service: 'storage.googleapis.com', - }, - }); + + expect(cloudFunction.__trigger).to.deep.equal( + expectedTrigger(defaultBucket, 'metadataUpdate') + ); + + expect(cloudFunction.__endpoint).to.deep.equal( + expectedEndpoint(defaultBucket, 'metadataUpdate') + ); }); it('should allow fully qualified bucket names', () => { @@ -325,23 +376,24 @@ describe('Storage Functions', () => { {} ); const result = subjectQualified.onMetadataUpdate(() => null); - expect(result.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'google.storage.object.metadataUpdate', - resource: 'projects/_/buckets/bucky', - service: 'storage.googleapis.com', - }, - }); + + expect(result.__trigger).to.deep.equal( + expectedTrigger('bucky', 'metadataUpdate') + ); + + expect(result.__endpoint).to.deep.equal( + expectedEndpoint('bucky', 'metadataUpdate') + ); }); it('should throw with improperly formatted buckets', () => { - expect( - () => - storage - .bucket('bad/bucket/format') - .object() - .onMetadataUpdate(() => null).__trigger - ).to.throw(Error); + const fn = storage + .bucket('bad/bucket/format') + .object() + .onMetadataUpdate(() => null); + + expect(() => fn.__trigger).to.throw(Error); + expect(() => fn.__endpoint).to.throw(Error); }); it('should not mess with media links using non-literal slashes', () => { @@ -390,7 +442,9 @@ describe('Storage Functions', () => { const cloudFunction = functions.handler.storage.bucket.onArchive( () => null ); + expect(cloudFunction.__trigger).to.deep.equal({}); + expect(cloudFunction.__endpoint).to.be.undefined; }); it('should not mess with media links using non-literal slashes', () => { @@ -429,7 +483,9 @@ describe('Storage Functions', () => { const cloudFunction = functions.handler.storage.bucket.onDelete( () => null ); + expect(cloudFunction.__trigger).to.deep.equal({}); + expect(cloudFunction.__endpoint).to.be.undefined; }); it('should not mess with media links using non-literal slashes', () => { @@ -468,7 +524,9 @@ describe('Storage Functions', () => { const cloudFunction = functions.handler.storage.bucket.onFinalize( () => null ); + expect(cloudFunction.__trigger).to.deep.equal({}); + expect(cloudFunction.__endpoint).to.be.undefined; }); it('should not mess with media links using non-literal slashes', () => { @@ -507,7 +565,9 @@ describe('Storage Functions', () => { const cloudFunction = functions.handler.storage.bucket.onMetadataUpdate( () => null ); + expect(cloudFunction.__trigger).to.deep.equal({}); + expect(cloudFunction.__endpoint).to.be.undefined; }); it('should not mess with media links using non-literal slashes', () => { @@ -558,6 +618,12 @@ describe('Storage Functions', () => { ); }); + it('should throw when endpoint is accessed', () => { + expect(() => storage.object().onArchive(() => null).__endpoint).to.throw( + Error + ); + }); + it('should not throw when #run is called', () => { const cf = storage.object().onArchive(() => null); expect(cf.run).to.not.throw(Error); diff --git a/spec/v1/providers/testLab.spec.ts b/spec/v1/providers/testLab.spec.ts index 3dbe8c0f5..b3ba22d7f 100644 --- a/spec/v1/providers/testLab.spec.ts +++ b/spec/v1/providers/testLab.spec.ts @@ -35,8 +35,9 @@ describe('Test Lab Functions', () => { delete process.env.GCLOUD_PROJECT; }); - it('should return a TriggerDefinition with appropriate values', () => { + 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', @@ -44,6 +45,18 @@ describe('Test Lab Functions', () => { resource: 'projects/project1/testMatrices/{matrix}', }, }); + + expect(func.__endpoint).to.deep.equal({ + platform: 'gcfv1', + eventTrigger: { + eventType: 'google.testing.testMatrix.complete', + eventFilters: { + resource: 'projects/project1/testMatrices/{matrix}', + }, + retry: false, + }, + labels: {}, + }); }); it('should parse TestMatrix in "INVALID" state', () => { @@ -155,6 +168,12 @@ describe('Test Lab Functions', () => { () => 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/https.spec.ts b/spec/v2/providers/https.spec.ts index 30ad53088..5f0f7eb74 100644 --- a/spec/v2/providers/https.spec.ts +++ b/spec/v2/providers/https.spec.ts @@ -252,8 +252,8 @@ describe('onCall', () => { expect(result.__endpoint).to.deep.equal({ platform: 'gcfv2', - labels: {}, callableTrigger: {}, + labels: {}, }); }); @@ -274,9 +274,6 @@ describe('onCall', () => { expect(result.__endpoint).to.deep.equal({ ...FULL_ENDPOINT, callableTrigger: {}, - labels: { - ...FULL_ENDPOINT.labels, - }, }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 5f7823c2f..d2de0504c 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -37,7 +37,7 @@ import { durationFromSeconds, serviceAccountFromShorthand, } from './common/encoding'; -import { ManifestEndpoint } from './common/manifest'; +import { ManifestEndpoint, ManifestRequiredAPI } from './common/manifest'; /** @hidden */ const WILDCARD_REGEX = new RegExp('{[^/{}]*}', 'g'); @@ -290,6 +290,7 @@ export interface TriggerAnnotated { */ export interface EndpointAnnotated { __endpoint: ManifestEndpoint; + __requiredAPIs?: ManifestRequiredAPI[]; } /** @@ -466,14 +467,24 @@ export function makeCloudFunction({ }; } - if (Object.keys(labels).length > 0) { - endpoint.labels = { ...endpoint.labels, ...labels }; - } + // Note: We intentionally don't make use of labels args here. + // labels is used to pass SDK-defined labels to the trigger, which isn't + // something we will do in the container contract world. + endpoint.labels = { ...endpoint.labels }; return endpoint; }, }); + if (options.schedule) { + cloudFunction.__requiredAPIs = [ + { + api: 'cloudscheduler.googleapis.com', + reason: 'Needed for scheduled functions.', + }, + ]; + } + cloudFunction.run = handler || contextOnlyHandler; return cloudFunction; } @@ -609,15 +620,14 @@ export function optionsToEndpoint( 'serviceAccount', (sa) => sa ); - if (options.vpcConnector) { - const vpc: ManifestEndpoint['vpc'] = { connector: options.vpcConnector }; + if (options?.vpcConnector) { + endpoint.vpc = { connector: options.vpcConnector }; convertIfPresent( - vpc, + endpoint.vpc, options, 'egressSettings', 'vpcConnectorEgressSettings' ); - endpoint.vpc = vpc; } convertIfPresent(endpoint, options, 'availableMemoryMb', 'memory', (mem) => { const memoryLookup = { diff --git a/src/common/manifest.ts b/src/common/manifest.ts index 02784b68c..379e61a4f 100644 --- a/src/common/manifest.ts +++ b/src/common/manifest.ts @@ -68,12 +68,18 @@ export interface ManifestEndpoint { }; } +/* @internal */ +export interface ManifestRequiredAPI { + api: string; + reason: string; +} + /** * @internal * An definition of a function deployment as appears in the Manifest. **/ -export interface ManifestBackend { +export interface ManifestStack { specVersion: 'v1alpha1'; - requiredAPIs: Record; + requiredAPIs: ManifestRequiredAPI[]; endpoints: Record; } diff --git a/src/handler-builder.ts b/src/handler-builder.ts index d1286f275..c89f3c936 100644 --- a/src/handler-builder.ts +++ b/src/handler-builder.ts @@ -48,16 +48,16 @@ export class HandlerBuilder { /** * Create a handler for HTTPS events. - + * `onRequest` handles an HTTPS request and has the same signature as an Express app. * * @example * ```javascript * exports.myFunction = functions.handler.https.onRequest((req, res) => { ... }) * ``` - * + * * `onCall` declares a callable function for clients to call using a Firebase SDK. - * + * * @example * ```javascript * exports.myFunction = functions.handler.https.onCall((data, context) => { ... }) @@ -70,7 +70,8 @@ export class HandlerBuilder { ): HttpsFunction => { const func = https._onRequestWithOptions(handler, {}); func.__trigger = {}; - func.__endpoint = {}; + func.__endpoint = undefined; + func.__requiredAPIs = undefined; return func; }, onCall: ( @@ -81,7 +82,8 @@ export class HandlerBuilder { ): HttpsFunction => { const func = https._onCallWithOptions(handler, {}); func.__trigger = {}; - func.__endpoint = {}; + func.__endpoint = undefined; + func.__requiredAPIs = undefined; return func; }, /** @hidden */ @@ -96,6 +98,8 @@ export class HandlerBuilder { const builder = new https.TaskQueueBuilder(); const func = builder.onDispatch(handler); func.__trigger = {}; + func.__endpoint = undefined; + func.__requiredAPIs = undefined; return func; }, }; @@ -105,21 +109,21 @@ export class HandlerBuilder { /** * Create a handler for Firebase Realtime Database events. - * + * * `ref.onCreate` handles the creation of new data. - * + * * @example * ```javascript * exports.myFunction = functions.handler.database.ref.onCreate((snap, context) => { ... }) * ``` - * + * * `ref.onUpdate` handles updates to existing data. - * + * * @example * ```javascript * exports.myFunction = functions.handler.database.ref.onUpdate((change, context) => { ... }) * ``` - + * `ref.onDelete` handles the deletion of existing data. * * @example @@ -152,21 +156,21 @@ export class HandlerBuilder { /** * Create a handler for Cloud Firestore events. - * + * * `document.onCreate` handles the creation of new documents. - * + * * @example * ```javascript * exports.myFunction = functions.handler.firestore.document.onCreate((snap, context) => { ... }) * ``` - + * `document.onUpdate` handles updates to existing documents. * * @example * ```javascript * exports.myFunction = functions.handler.firestore.document.onUpdate((change, context) => { ... }) * ``` - + * `document.onDelete` handles the deletion of existing documents. * * @example @@ -174,7 +178,7 @@ export class HandlerBuilder { * exports.myFunction = functions.handler.firestore.document.onDelete((snap, context) => * { ... }) * ``` - + * `document.onWrite` handles the creation, update, or deletion of documents. * * @example @@ -203,7 +207,7 @@ export class HandlerBuilder { * Create a handler for Firebase Remote Config events. * `remoteConfig.onUpdate` handles events that update a Remote Config template. - + * @example * ```javascript * exports.myFunction = functions.handler.remoteConfig.onUpdate() => { ... }) @@ -224,9 +228,9 @@ export class HandlerBuilder { /** * Create a handler for Google Analytics events. - + * `event.onLog` handles the logging of Analytics conversion events. - + * @example * ```javascript * exports.myFunction = functions.handler.analytics.event.onLog((event) => { ... }) @@ -242,21 +246,21 @@ export class HandlerBuilder { /** * Create a handler for Cloud Storage for Firebase events. - * + * * `object.onArchive` handles the archiving of Storage objects. - * + * * @example * ```javascript * exports.myFunction = functions.handler.storage.object.onArchive((object) => { ... }) * ``` - + * `object.onDelete` handles the deletion of Storage objects. * * @example * ```javascript * exports.myFunction = functions.handler.storage.object.onDelete((object) => { ... }) * ``` - + * `object.onFinalize` handles the creation of Storage objects. * * @example @@ -264,7 +268,7 @@ export class HandlerBuilder { * exports.myFunction = functions.handler.storage.object.onFinalize((object) => * { ... }) * ``` - + * `object.onMetadataUpdate` handles changes to the metadata of existing Storage objects. * * @example @@ -287,16 +291,16 @@ export class HandlerBuilder { /** * Create a handler for Cloud Pub/Sub events. - * - * `topic.onPublish` handles messages published to a Pub/Sub topic from SDKs, Cloud Console, or gcloud CLI. - * + * + * `topic.onPublish` handles messages published to a Pub/Sub topic from SDKs, Cloud Console, or gcloud CLI. + * * @example * ```javascript * exports.myFunction = functions.handler.pubsub.topic.onPublish((message) => { ... }) * ``` - + * `schedule.onPublish` handles messages published to a Pub/Sub topic on a schedule. - * + * * @example * ```javascript * exports.myFunction = functions.handler.pubsub.schedule.onPublish((message) => { ... }) @@ -315,21 +319,21 @@ export class HandlerBuilder { /** * Create a handler for Firebase Authentication events. - * + * * `user.onCreate` handles the creation of users. - * + * * @example * ```javascript * exports.myFunction = functions.handler.auth.user.onCreate((user) => { ... }) * ``` - + * `user.onDelete` handles the deletion of users. * * @example * ```javascript * exports.myFunction = functions.handler.auth.user.onDelete((user => { ... }) * ``` - + */ get auth() { return { @@ -343,7 +347,7 @@ export class HandlerBuilder { * Create a handler for Firebase Test Lab events. * `testMatrix.onComplete` handles the completion of a test matrix. - + * @example * ```javascript * exports.myFunction = functions.handler.testLab.testMatrix.onComplete((testMatrix) => { ... }) diff --git a/src/providers/https.ts b/src/providers/https.ts index 1b5cfaa4d..8ab072e78 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -28,7 +28,12 @@ import { optionsToTrigger, Runnable, } from '../cloud-functions'; -import {convertIfPresent, convertInvoker, copyIfPresent} from '../common/encoding'; +import { + convertIfPresent, + convertInvoker, + copyIfPresent, +} from '../common/encoding'; +import { ManifestEndpoint, ManifestRequiredAPI } from '../common/manifest'; import { CallableContext, FunctionsErrorCode, @@ -97,6 +102,8 @@ export interface TaskQueueOptions { export interface TaskQueueFunction { (req: Request, res: express.Response): Promise; __trigger: unknown; + __endpoint: ManifestEndpoint; + __requiredAPIs?: ManifestRequiredAPI[]; run(data: any, context: TaskContext): void | Promise; } @@ -132,6 +139,28 @@ export class TaskQueueBuilder { convertInvoker ); + func.__endpoint = { + platform: 'gcfv1', + ...optionsToEndpoint(this.depOpts), + taskQueueTrigger: {}, + }; + copyIfPresent(func.__endpoint.taskQueueTrigger, this.tqOpts, 'retryConfig'); + copyIfPresent(func.__endpoint.taskQueueTrigger, this.tqOpts, 'rateLimits'); + convertIfPresent( + func.__endpoint.taskQueueTrigger, + this.tqOpts, + 'invoker', + 'invoker', + convertInvoker + ); + + func.__requiredAPIs = [ + { + api: 'cloudtasks.googleapis.com', + reason: 'Needed for v1 task queue functions', + }, + ]; + func.run = handler; return func; diff --git a/src/v2/providers/pubsub.ts b/src/v2/providers/pubsub.ts index 81c4bca68..a751f0cd2 100644 --- a/src/v2/providers/pubsub.ts +++ b/src/v2/providers/pubsub.ts @@ -135,11 +135,6 @@ export function onMessagePublished( func.run = handler; - // TypeScript doesn't recognize defineProperty as adding a property and complains - // that __trigger 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. - func.__trigger = 'silence the transpiler'; - Object.defineProperty(func, '__trigger', { get: () => { const baseOpts = options.optionsToTriggerAnnotations( diff --git a/src/v2/providers/storage.ts b/src/v2/providers/storage.ts index c092d25a2..e03115df0 100644 --- a/src/v2/providers/storage.ts +++ b/src/v2/providers/storage.ts @@ -315,12 +315,6 @@ export function onOperation( func.run = handler; - // 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. - func.__trigger = 'silence the transpiler'; - func.__endpoint = {} as ManifestEndpoint; - Object.defineProperty(func, '__trigger', { get: () => { const baseOpts = options.optionsToTriggerAnnotations( @@ -344,6 +338,11 @@ export function onOperation( }, }); + // 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. + func.__endpoint = {} as ManifestEndpoint; + // SDK may attempt to read FIREBASE_CONFIG env var to fetch the default bucket name. // To prevent runtime errors when FIREBASE_CONFIG env var is missing, we use getters. Object.defineProperty(func, '__endpoint', {