diff --git a/spec/v2.spec.ts b/spec/v2.spec.ts index ff55c23..896b139 100644 --- a/spec/v2.spec.ts +++ b/spec/v2.spec.ts @@ -522,6 +522,19 @@ describe('v2', () => { expect(cloudEvent.data.val()).deep.equal(dataVal); }); + + it('should accept json data', () => { + const referenceOptions = { + ref: 'foo/bar', + instance: 'instance-1', + }; + const cloudFn = database.onValueCreated(referenceOptions, handler); + const cloudFnWrap = wrapV2(cloudFn); + const dataVal = { snapshot: 'override' }; + const cloudEvent = cloudFnWrap({ data: dataVal }).cloudEvent; + + expect(cloudEvent.data.val()).deep.equal(dataVal); + }); }); describe('database.onValueDeleted()', () => { @@ -563,6 +576,19 @@ describe('v2', () => { expect(cloudEvent.data.val()).deep.equal(dataVal); }); + + it('should accept json data', () => { + const referenceOptions = { + ref: 'foo/bar', + instance: 'instance-1', + }; + const cloudFn = database.onValueDeleted(referenceOptions, handler); + const cloudFnWrap = wrapV2(cloudFn); + const dataVal = { snapshot: 'override' }; + const cloudEvent = cloudFnWrap({ data: dataVal }).cloudEvent; + + expect(cloudEvent.data.val()).deep.equal(dataVal); + }); }); describe('database.onValueUpdated()', () => { @@ -610,6 +636,23 @@ describe('v2', () => { expect(cloudEvent.data.before.val()).deep.equal(beforeDataVal); expect(cloudEvent.data.after.val()).deep.equal(afterDataVal); }); + + it('should accept json data', () => { + const referenceOptions = { + ref: 'foo/bar', + instance: 'instance-1', + }; + const cloudFn = database.onValueUpdated(referenceOptions, handler); + const cloudFnWrap = wrapV2(cloudFn); + const afterDataVal = { snapshot: 'after' }; + const beforeDataVal = { snapshot: 'before' }; + const data = { before: beforeDataVal, after: afterDataVal }; + + const cloudEvent = cloudFnWrap({ data }).cloudEvent; + + expect(cloudEvent.data.before.val()).deep.equal(beforeDataVal); + expect(cloudEvent.data.after.val()).deep.equal(afterDataVal); + }); }); describe('database.onValueWritten()', () => { @@ -657,6 +700,24 @@ describe('v2', () => { expect(cloudEvent.data.before.val()).deep.equal(beforeDataVal); expect(cloudEvent.data.after.val()).deep.equal(afterDataVal); }); + + it('should accept json data', () => { + const referenceOptions = { + ref: 'foo/bar', + instance: 'instance-1', + }; + const cloudFn = database.onValueWritten(referenceOptions, handler); + const cloudFnWrap = wrapV2(cloudFn); + const afterDataVal = { snapshot: 'after' }; + + const beforeDataVal = { snapshot: 'before' }; + + const data = { before: beforeDataVal, after: afterDataVal }; + const cloudEvent = cloudFnWrap({ data }).cloudEvent; + + expect(cloudEvent.data.before.val()).deep.equal(beforeDataVal); + expect(cloudEvent.data.after.val()).deep.equal(afterDataVal); + }); }); }); diff --git a/src/cloudevent/generate.ts b/src/cloudevent/generate.ts index 908648b..bc5d76d 100644 --- a/src/cloudevent/generate.ts +++ b/src/cloudevent/generate.ts @@ -1,7 +1,12 @@ -import { CloudEvent } from 'firebase-functions/v2'; -import { CloudFunction } from 'firebase-functions/v2'; +import { + CloudEvent, + CloudFunction, + database, + pubsub, +} from 'firebase-functions/v2'; import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './mocks/partials'; -import { DeepPartial, MockCloudEventAbstractFactory } from './types'; +import { DeepPartial } from './types'; +import { Change } from 'firebase-functions'; import merge from 'ts-deepmerge'; /** @@ -17,9 +22,7 @@ export function generateCombinedCloudEvent< cloudFunction, cloudEventPartial ); - return cloudEventPartial - ? (merge(generatedCloudEvent, cloudEventPartial) as EventType) - : generatedCloudEvent; + return mergeCloudEvents(generatedCloudEvent, cloudEventPartial); } export function generateMockCloudEvent>( @@ -37,3 +40,59 @@ export function generateMockCloudEvent>( // No matches were found return null; } + +const IMMUTABLE_DATA_TYPES = [database.DataSnapshot, Change, pubsub.Message]; + +function mergeCloudEvents>( + generatedCloudEvent: EventType, + cloudEventPartial: DeepPartial +) { + /** + * There are several CloudEvent.data types that can not be overridden with json. + * In these circumstances, we generate the CloudEvent.data given the user supplies + * in the DeepPartial. + * + * Because we have already extracted the user supplied data, we don't want to overwrite + * the CloudEvent.data with an incompatible type. + * + * An example of this is a user supplying JSON for the data of the DatabaseEvents. + * The returned CloudEvent should be returning DataSnapshot that uses the supplied json, + * NOT the supplied JSON. + */ + if (shouldDeleteUserSuppliedData(generatedCloudEvent, cloudEventPartial)) { + delete cloudEventPartial.data; + } + return cloudEventPartial + ? (merge(generatedCloudEvent, cloudEventPartial) as EventType) + : generatedCloudEvent; +} + +function shouldDeleteUserSuppliedData>( + generatedCloudEvent: EventType, + cloudEventPartial: DeepPartial +) { + // Don't attempt to delete the data if there is no data. + if (cloudEventPartial?.data === undefined) { + return false; + } + // If the user intentionally provides one of the IMMUTABLE DataTypes, DON'T delete it! + if ( + IMMUTABLE_DATA_TYPES.some((type) => cloudEventPartial?.data instanceof type) + ) { + return false; + } + + /** If the generated CloudEvent.data is an IMMUTABLE DataTypes, then use the generated data and + * delete the user supplied CloudEvent.data. + */ + if ( + IMMUTABLE_DATA_TYPES.some( + (type) => generatedCloudEvent?.data instanceof type + ) + ) { + return true; + } + + // Otherwise, don't delete the data and allow ts-merge to handle merging the data. + return false; +} diff --git a/src/cloudevent/mocks/database/helpers.ts b/src/cloudevent/mocks/database/helpers.ts index 5b89983..b4855c8 100644 --- a/src/cloudevent/mocks/database/helpers.ts +++ b/src/cloudevent/mocks/database/helpers.ts @@ -6,10 +6,46 @@ import { } from '../../../providers/database'; import { getBaseCloudEvent } from '../helpers'; import { Change } from 'firebase-functions'; +import { makeDataSnapshot } from '../../../providers/database'; + +type ChangeLike = { + before: database.DataSnapshot | object; + after: database.DataSnapshot | object; +}; + +function getOrCreateDataSnapshot( + data: database.DataSnapshot | object, + ref: string +) { + if (data instanceof database.DataSnapshot) { + return data; + } + if (data instanceof Object) { + return makeDataSnapshot(data, ref); + } + return exampleDataSnapshot(ref); +} + +function getOrCreateDataSnapshotChange( + data: DeepPartial | ChangeLike>, + ref: string +) { + if (data instanceof Change) { + return data; + } + if (data instanceof Object && data?.before && data?.after) { + const beforeDataSnapshot = getOrCreateDataSnapshot(data!.before, ref); + const afterDataSnapshot = getOrCreateDataSnapshot(data!.after, ref); + return new Change(beforeDataSnapshot, afterDataSnapshot); + } + return exampleDataSnapshotChange(ref); +} export function getDatabaseSnapshotCloudEvent( cloudFunction: CloudFunction>, - cloudEventPartial?: DeepPartial> + cloudEventPartial?: DeepPartial< + database.DatabaseEvent + > ) { const { instance, @@ -19,9 +55,7 @@ export function getDatabaseSnapshotCloudEvent( params, } = getCommonDatabaseFields(cloudFunction, cloudEventPartial); - const data = - (cloudEventPartial?.data as database.DataSnapshot) || - exampleDataSnapshot(ref); + const data = getOrCreateDataSnapshot(cloudEventPartial?.data, ref); return { // Spread common fields @@ -43,7 +77,7 @@ export function getDatabaseChangeSnapshotCloudEvent( database.DatabaseEvent> >, cloudEventPartial?: DeepPartial< - database.DatabaseEvent> + database.DatabaseEvent | ChangeLike> > ): database.DatabaseEvent> { const { @@ -54,9 +88,7 @@ export function getDatabaseChangeSnapshotCloudEvent( params, } = getCommonDatabaseFields(cloudFunction, cloudEventPartial); - const data = - (cloudEventPartial?.data as Change) || - exampleDataSnapshotChange(ref); + const data = getOrCreateDataSnapshotChange(cloudEventPartial?.data, ref); return { // Spread common fields diff --git a/src/v2.ts b/src/v2.ts index 3140c3b..9dde6fc 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -29,7 +29,7 @@ import { DeepPartial } from './cloudevent/types'; * It will subsequently invoke the cloud function it wraps with the provided {@link CloudEvent} */ export type WrappedV2Function> = ( - cloudEventPartial?: DeepPartial + cloudEventPartial?: DeepPartial ) => any | Promise; /**