diff --git a/spec/v2.spec.ts b/spec/v2.spec.ts index 1101aa7..d9f13da 100644 --- a/spec/v2.spec.ts +++ b/spec/v2.spec.ts @@ -23,6 +23,7 @@ import { expect } from 'chai'; import { wrapV2 } from '../src/v2'; +import type { WrappedV2ScheduledFunction } from '../src/v2'; import { CloudFunction, @@ -35,6 +36,7 @@ import { eventarc, https, firestore, + scheduler, } from 'firebase-functions/v2'; import { defineString } from 'firebase-functions/params'; import { makeDataSnapshot } from '../src/providers/database'; @@ -1422,6 +1424,41 @@ describe('v2', () => { }); }); + describe('scheduler', () => { + describe('onSchedule()', () => { + it('should return correct data', async () => { + const scheduledFunction = scheduler.onSchedule( + 'every 5 minutes', + (_e) => {} + ); + + scheduledFunction.__endpoint = { + platform: 'gcfv2', + labels: {}, + scheduleTrigger: { + schedule: 'every 5 minutes', + }, + concurrency: 20, + minInstances: 3, + region: ['us-west1', 'us-central1'], + }; + + let wrappedFunction: WrappedV2ScheduledFunction; + + expect( + () => (wrappedFunction = wrapV2(scheduledFunction)) + ).not.to.throw(); + + const result = await wrappedFunction({ + scheduleTime: '2024-01-01T00:00:00Z', + jobName: 'test-job', + }); + + expect(result).to.be.undefined; + }); + }); + }); + describe('callable functions', () => { it('should return correct data', async () => { const cloudFunction = https.onCall(() => 'hello'); diff --git a/src/main.ts b/src/main.ts index 13a23bb..d4c009a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -80,9 +80,14 @@ export function wrap>( | WrappedFunction | WrappedV2Function | WrappedV2CallableFunction { + if (!cloudFunction) { + throw new Error('Cannot wrap: undefined cloud function'); + } + if (isV2CloudFunction(cloudFunction)) { - return wrapV2(cloudFunction as CloudFunctionV2); + return wrapV2(cloudFunction); } + return wrapV1( cloudFunction as HttpsFunctionOrCloudFunctionV1 ); @@ -93,12 +98,23 @@ export function wrap>( *
    *
  • V1 CloudFunction is sometimes a binary function *
  • V2 CloudFunction is always a unary function + *
  • V2 ScheduleFunction is a binary function (HttpsFunctionV2) + * *
  • V1 CloudFunction.run is always a binary function *
  • V2 CloudFunction.run is always a unary function + *
  • V2 ScheduleFunction.run is always a unary function + *
+ * * @return True iff the CloudFunction is a V2 function. */ function isV2CloudFunction>( - cloudFunction: any + cloudFunction: CloudFunctionV1 | CloudFunctionV2 | HttpsFunctionV2 ): cloudFunction is CloudFunctionV2 { - return cloudFunction.length === 1 && cloudFunction?.run?.length === 1; + if (!cloudFunction) { + return false; + } + + type CloudFunctionType = CloudFunctionV1 | CloudFunctionV2; + + return (cloudFunction as CloudFunctionType).run.length === 1; } diff --git a/src/v2.ts b/src/v2.ts index e3fef18..5880347 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -20,12 +20,20 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { CloudFunction, CloudEvent } from 'firebase-functions/v2'; -import { CallableFunction, CallableRequest } from 'firebase-functions/v2/https'; +import type { CloudFunction, CloudEvent } from 'firebase-functions/v2'; +import type { CallableFunction, CallableRequest } from 'firebase-functions/v2/https'; +import type { + ScheduledEvent, + ScheduleFunction, +} from 'firebase-functions/v2/scheduler'; import { generateCombinedCloudEvent } from './cloudevent/generate'; import { DeepPartial } from './cloudevent/types'; -import * as express from 'express'; + +type V2WrappableFunctions = + | CloudFunction + | CallableFunction + | ScheduleFunction; /** A function that can be called with test data and optional override values for {@link CloudEvent} * It will subsequently invoke the cloud function it wraps with the provided {@link CloudEvent} @@ -38,14 +46,24 @@ export type WrappedV2CallableFunction = ( data: CallableRequest ) => T | Promise; -function isCallableV2Function>( - cf: CloudFunction | CallableFunction +export type WrappedV2ScheduledFunction = ( + data: ScheduledEvent +) => void | Promise; + +function isCallableV2Function( + cf: V2WrappableFunctions ): cf is CallableFunction { - return !!cf?.__endpoint?.callableTrigger; + return !!cf.__endpoint?.callableTrigger; +} + +function isScheduledV2Function( + cf: V2WrappableFunctions +): cf is ScheduleFunction { + return !!cf.__endpoint?.scheduleTrigger; } function assertIsCloudFunction>( - cf: CloudFunction | CallableFunction + cf: V2WrappableFunctions ): asserts cf is CloudFunction { if (!('run' in cf) || !cf.run) { throw new Error( @@ -54,6 +72,14 @@ function assertIsCloudFunction>( } } +function assertIsCloudFunctionV2>( + cf: CloudFunction | CallableFunction +): asserts cf is CloudFunction { + if (cf?.__endpoint?.platform !== 'gcfv2') { + throw new Error('This function can only wrap V2 CloudFunctions.'); + } +} + /** * Takes a v2 cloud function to be tested, and returns a {@link WrappedV2Function} * which can be called in test code. @@ -70,19 +96,34 @@ export function wrapV2( cloudFunction: CallableFunction ): WrappedV2CallableFunction; +export function wrapV2( + cloudFunction: ScheduleFunction +): WrappedV2ScheduledFunction; + export function wrapV2>( - cloudFunction: CloudFunction | CallableFunction -): WrappedV2Function | WrappedV2CallableFunction { + cloudFunction: + | CloudFunction + | CallableFunction + | ScheduleFunction +): + | WrappedV2Function + | WrappedV2CallableFunction + | WrappedV2ScheduledFunction { + if (!cloudFunction) { + throw new Error('Cannot wrap: undefined cloud function'); + } + + assertIsCloudFunction(cloudFunction); + assertIsCloudFunctionV2(cloudFunction); + if (isCallableV2Function(cloudFunction)) { return (req: CallableRequest) => { return cloudFunction.run(req); }; } - assertIsCloudFunction(cloudFunction); - - if (cloudFunction?.__endpoint?.platform !== 'gcfv2') { - throw new Error('This function can only wrap V2 CloudFunctions.'); + if (isScheduledV2Function(cloudFunction)) { + return createScheduledWrapper(cloudFunction); } return (cloudEventPartial?: DeepPartial) => { @@ -93,3 +134,25 @@ export function wrapV2>( return cloudFunction.run(cloudEvent); }; } + +function createScheduledWrapper( + cloudFunction: ScheduleFunction +): WrappedV2ScheduledFunction { + return (options: ScheduledEvent) => { + _checkOptionValidity(['jobName', 'scheduleTime'], options); + return cloudFunction.run(options); + }; +} + +function _checkOptionValidity( + validFields: string[], + options: Record +) { + Object.keys(options).forEach((key) => { + if (validFields.indexOf(key) === -1) { + throw new Error( + `Options object ${JSON.stringify(options)} has invalid key "${key}"` + ); + } + }); +} \ No newline at end of file