From bcfeea512194cdc8a7d7a7ba7ed9b97507eb5e05 Mon Sep 17 00:00:00 2001 From: Tyler Stark Date: Wed, 3 Aug 2022 14:37:27 -0500 Subject: [PATCH 1/4] Update Rtdb test sdk to allow json for CloudEvent This commit updates the test sdk api for the Database CloudEventPartial to support end-users submitting JSON that gets transformed into `DataSnapshot` instead of forcing users to import and use `Change` and `DataSnapshot` to mock out the CloudEvent resopnses. --- spec/v2.spec.ts | 61 ++++++++++++++++++++++ src/cloudevent/generate.ts | 66 ++++++++++++++++++++++-- src/cloudevent/mocks/database/helpers.ts | 43 ++++++++++++--- src/v2.ts | 2 +- 4 files changed, 158 insertions(+), 14 deletions(-) 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..8518793 100644 --- a/src/cloudevent/generate.ts +++ b/src/cloudevent/generate.ts @@ -1,7 +1,9 @@ -import { CloudEvent } from 'firebase-functions/v2'; +import { CloudEvent, pubsub } from 'firebase-functions/v2'; import { CloudFunction } from 'firebase-functions/v2'; import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './mocks/partials'; -import { DeepPartial, MockCloudEventAbstractFactory } from './types'; +import { DeepPartial } from './types'; +import { database } from 'firebase-functions/v2'; +import { Change } from 'firebase-functions'; import merge from 'ts-deepmerge'; /** @@ -17,9 +19,7 @@ export function generateCombinedCloudEvent< cloudFunction, cloudEventPartial ); - return cloudEventPartial - ? (merge(generatedCloudEvent, cloudEventPartial) as EventType) - : generatedCloudEvent; + return mergeCloudEvents(generatedCloudEvent, cloudEventPartial); } export function generateMockCloudEvent>( @@ -37,3 +37,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 PoJson. + * 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..1b356f8 100644 --- a/src/cloudevent/mocks/database/helpers.ts +++ b/src/cloudevent/mocks/database/helpers.ts @@ -6,10 +6,41 @@ import { } from '../../../providers/database'; import { getBaseCloudEvent } from '../helpers'; import { Change } from 'firebase-functions'; +import { makeDataSnapshot } from '../../../providers/database'; + +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: Change | any, + 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 +50,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 +72,7 @@ export function getDatabaseChangeSnapshotCloudEvent( database.DatabaseEvent> >, cloudEventPartial?: DeepPartial< - database.DatabaseEvent> + database.DatabaseEvent | object> > ): database.DatabaseEvent> { const { @@ -54,9 +83,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; /** From 1481939ca362796ef9ef55ee037b03f9cf00b077 Mon Sep 17 00:00:00 2001 From: Tyler Stark Date: Thu, 4 Aug 2022 16:09:10 -0500 Subject: [PATCH 2/4] Updates per PR feedback --- src/cloudevent/generate.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/cloudevent/generate.ts b/src/cloudevent/generate.ts index 8518793..bc5d76d 100644 --- a/src/cloudevent/generate.ts +++ b/src/cloudevent/generate.ts @@ -1,8 +1,11 @@ -import { CloudEvent, pubsub } 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 } from './types'; -import { database } from 'firebase-functions/v2'; import { Change } from 'firebase-functions'; import merge from 'ts-deepmerge'; @@ -45,7 +48,7 @@ function mergeCloudEvents>( cloudEventPartial: DeepPartial ) { /** - * There are several CloudEvent.data types that can not be overridden with PoJson. + * 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. * @@ -72,7 +75,7 @@ function shouldDeleteUserSuppliedData>( if (cloudEventPartial?.data === undefined) { return false; } - /** If the user intentionally provides one of the IMMUTABLE DataTypes, DON'T delete it! */ + // If the user intentionally provides one of the IMMUTABLE DataTypes, DON'T delete it! if ( IMMUTABLE_DATA_TYPES.some((type) => cloudEventPartial?.data instanceof type) ) { From eb3d89b3e23e38af6cf0061935d4c98246143a30 Mon Sep 17 00:00:00 2001 From: Tyler Stark Date: Thu, 4 Aug 2022 18:02:45 -0500 Subject: [PATCH 3/4] Update src/cloudevent/mocks/database/helpers.ts Co-authored-by: Daniel Lee --- src/cloudevent/mocks/database/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cloudevent/mocks/database/helpers.ts b/src/cloudevent/mocks/database/helpers.ts index 1b356f8..89b574e 100644 --- a/src/cloudevent/mocks/database/helpers.ts +++ b/src/cloudevent/mocks/database/helpers.ts @@ -9,7 +9,7 @@ import { Change } from 'firebase-functions'; import { makeDataSnapshot } from '../../../providers/database'; function getOrCreateDataSnapshot( - data: database.DataSnapshot | object, + data: database.DataSnapshot | Object, ref: string ) { if (data instanceof database.DataSnapshot) { From 77b831b2351a78ed58ea1a912a94660cba296442 Mon Sep 17 00:00:00 2001 From: Tyler Stark Date: Fri, 5 Aug 2022 10:29:08 -0500 Subject: [PATCH 4/4] Added ChangeLike type, and reverted an Object to object (lint errror) --- src/cloudevent/mocks/database/helpers.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/cloudevent/mocks/database/helpers.ts b/src/cloudevent/mocks/database/helpers.ts index 89b574e..b4855c8 100644 --- a/src/cloudevent/mocks/database/helpers.ts +++ b/src/cloudevent/mocks/database/helpers.ts @@ -8,8 +8,13 @@ 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, + data: database.DataSnapshot | object, ref: string ) { if (data instanceof database.DataSnapshot) { @@ -22,15 +27,15 @@ function getOrCreateDataSnapshot( } function getOrCreateDataSnapshotChange( - data: Change | any, + 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); + const beforeDataSnapshot = getOrCreateDataSnapshot(data!.before, ref); + const afterDataSnapshot = getOrCreateDataSnapshot(data!.after, ref); return new Change(beforeDataSnapshot, afterDataSnapshot); } return exampleDataSnapshotChange(ref); @@ -72,7 +77,7 @@ export function getDatabaseChangeSnapshotCloudEvent( database.DatabaseEvent> >, cloudEventPartial?: DeepPartial< - database.DatabaseEvent | object> + database.DatabaseEvent | ChangeLike> > ): database.DatabaseEvent> { const {