From 1ea2d7d4c852449f34279eeedfadd2d69c1e7f2b Mon Sep 17 00:00:00 2001 From: Derek Burgman Date: Mon, 9 May 2022 21:36:42 -0500 Subject: [PATCH] feat: added jestFunctionFixture - added appFirestoreModuleMetadata for easier configuration of an app's FirestoreCollections-type provider and module - added ForwardFunction - added describeCloudFunctionTest --- apps/demo-api/src/app/app.ts | 8 ++- .../src/app/common/firebase/action.module.ts | 6 +- .../app/common/firebase/firebase.module.ts | 6 +- .../app/common/firebase/firestore.module.ts | 25 ++------ .../demo-api/src/app/common/firebase/index.ts | 2 + .../guestbook/guestbook.action.server.ts | 11 ++-- .../model/guestbook/guestbook.module.ts | 4 +- .../model/profile/profile.action.server.ts | 24 +++++++- .../common/model/profile/profile.module.ts | 4 +- .../guestbook/guestbookentry.function.spec.ts | 17 ++---- .../guestbook/guestbookentry.update.ts | 5 +- .../function/profile/profile.function.spec.ts | 38 +++++++++--- .../function/profile/profile.set.username.ts | 7 +-- .../app/function/profile/profile.update.ts | 13 ++++ .../src/app/function/profile/profile.util.ts | 9 +++ apps/demo-api/src/main.ts | 3 +- .../src/lib/guestbook/guestbook.api.ts | 17 +++--- .../src/lib/profile/profile.api.ts | 40 +++++++++--- apps/demo-firebase/src/lib/profile/profile.ts | 5 ++ .../guestbook.entry.popup.component.ts | 1 + .../store/guestbook.entry.document.store.ts | 6 +- .../src/lib/firebase/firebase.nest.ts | 61 ++++++++++++++++++- .../firebase-server/src/lib/function/index.ts | 1 + .../firebase-server/src/lib/function/type.ts | 6 ++ .../src/lib/nest/function/nest.ts | 7 ++- .../test/firebase/firebase.admin.function.ts | 4 +- .../firebase.admin.nest.function.context.ts | 39 ++++++++++++ .../firebase/firebase.admin.nest.function.ts | 20 +++++- .../src/test/firebase/firebase.admin.nest.ts | 4 +- .../src/test/firebase/firebase.admin.ts | 24 +++++--- .../src/test/firebase/index.ts | 1 + .../src/lib/function/function.forward.spec.ts | 17 ++++++ .../util/src/lib/function/function.forward.ts | 19 ++++++ packages/util/src/lib/function/index.ts | 1 + packages/util/src/lib/index.ts | 1 + packages/util/src/test/index.ts | 1 + packages/util/src/test/jest.function.ts | 18 ++++++ 37 files changed, 368 insertions(+), 107 deletions(-) create mode 100644 apps/demo-api/src/app/function/profile/profile.update.ts create mode 100644 apps/demo-api/src/app/function/profile/profile.util.ts create mode 100644 packages/firebase-server/src/lib/function/type.ts create mode 100644 packages/firebase-server/src/test/firebase/firebase.admin.nest.function.context.ts create mode 100644 packages/util/src/lib/function/function.forward.spec.ts create mode 100644 packages/util/src/lib/function/function.forward.ts create mode 100644 packages/util/src/lib/function/index.ts create mode 100644 packages/util/src/test/jest.function.ts diff --git a/apps/demo-api/src/app/app.ts b/apps/demo-api/src/app/app.ts index 0c137c8c2..b267e4398 100644 --- a/apps/demo-api/src/app/app.ts +++ b/apps/demo-api/src/app/app.ts @@ -1,8 +1,9 @@ -import { guestbookEntryUpdateKey, profileSetUsernameKey } from '@dereekb/demo-firebase'; +import { updateProfileKey, updateGuestbookEntryKey, profileSetUsernameKey } from '@dereekb/demo-firebase'; import { NestAppPromiseGetter, nestServerInstance } from '@dereekb/firebase-server'; import { DemoApiAppModule } from './app.module'; import { profileSetUsername, initUserOnCreate } from './function'; -import { guestbookEntryUpdateEntry } from './function/guestbook'; +import { updateGuestbookEntry } from './function/guestbook'; +import { updateProfile } from './function/profile/profile.update'; export const { initNestServer @@ -23,7 +24,8 @@ export function demoAppFunctions(nest: NestAppPromiseGetter) { // API Calls // Profile [profileSetUsernameKey]: profileSetUsername(nest), + [updateProfileKey]: updateProfile(nest), // Guestbook - [guestbookEntryUpdateKey]: guestbookEntryUpdateEntry(nest) + [updateGuestbookEntryKey]: updateGuestbookEntry(nest) }; } diff --git a/apps/demo-api/src/app/common/firebase/action.module.ts b/apps/demo-api/src/app/common/firebase/action.module.ts index 9e9e30d08..0b91d368a 100644 --- a/apps/demo-api/src/app/common/firebase/action.module.ts +++ b/apps/demo-api/src/app/common/firebase/action.module.ts @@ -1,4 +1,4 @@ -import { AppFirestoreModule } from './firestore.module'; +import { DemoApiFirestoreModule } from './firestore.module'; import { DemoFirestoreCollections } from "@dereekb/demo-firebase"; import { firebaseServerActionsContext } from '@dereekb/firebase-server'; import { Module } from "@nestjs/common"; @@ -12,7 +12,7 @@ const demoFirebaseServerActionsContextFactory = (collections: DemoFirestoreColle } @Module({ - imports: [AppFirestoreModule], + imports: [DemoApiFirestoreModule], providers: [{ provide: DemoFirebaseServerActionsContext, useFactory: demoFirebaseServerActionsContextFactory, @@ -20,4 +20,4 @@ const demoFirebaseServerActionsContextFactory = (collections: DemoFirestoreColle }], exports: [DemoFirebaseServerActionsContext] }) -export class AppActionModule { } +export class DemoApiActionModule { } diff --git a/apps/demo-api/src/app/common/firebase/firebase.module.ts b/apps/demo-api/src/app/common/firebase/firebase.module.ts index 85d4bdb2a..b22873851 100644 --- a/apps/demo-api/src/app/common/firebase/firebase.module.ts +++ b/apps/demo-api/src/app/common/firebase/firebase.module.ts @@ -1,8 +1,8 @@ import { Module } from "@nestjs/common"; -import { AppFirestoreModule } from './firestore.module'; -import { AppActionModule } from './action.module'; +import { DemoApiFirestoreModule } from './firestore.module'; +import { DemoApiActionModule } from './action.module'; @Module({ - imports: [AppFirestoreModule, AppActionModule] + imports: [DemoApiFirestoreModule, DemoApiActionModule] }) export class DemoApiFirebaseModule { } diff --git a/apps/demo-api/src/app/common/firebase/firestore.module.ts b/apps/demo-api/src/app/common/firebase/firestore.module.ts index c7a042f65..bc19ee6d6 100644 --- a/apps/demo-api/src/app/common/firebase/firestore.module.ts +++ b/apps/demo-api/src/app/common/firebase/firestore.module.ts @@ -1,22 +1,9 @@ import { DemoFirestoreCollections, makeDemoFirestoreCollections } from "@dereekb/demo-firebase"; -import { Firestore, FirestoreContext } from "@dereekb/firebase"; -import { FirebaseServerFirestoreModule, FIRESTORE_CONTEXT_TOKEN, FIRESTORE_TOKEN, googleCloudFirestoreContextFactory } from "@dereekb/firebase-server"; +import { appFirestoreModuleMetadata } from "@dereekb/firebase-server"; import { Module } from "@nestjs/common"; -const demoFirestoreContextFactory = (firestore: Firestore) => googleCloudFirestoreContextFactory(firestore); -const demoFirestoreCollectionsFactory = (context: FirestoreContext) => makeDemoFirestoreCollections(context); - -@Module({ - imports: [FirebaseServerFirestoreModule], - exports: [FirebaseServerFirestoreModule, DemoFirestoreCollections, FIRESTORE_CONTEXT_TOKEN], - providers: [{ - provide: FIRESTORE_CONTEXT_TOKEN, - useFactory: demoFirestoreContextFactory, - inject: [FIRESTORE_TOKEN] - }, { - provide: DemoFirestoreCollections, - useFactory: demoFirestoreCollectionsFactory, - inject: [FIRESTORE_CONTEXT_TOKEN] - }] -}) -export class AppFirestoreModule { } +@Module(appFirestoreModuleMetadata({ + provide: DemoFirestoreCollections, + useFactory: makeDemoFirestoreCollections +})) +export class DemoApiFirestoreModule { } diff --git a/apps/demo-api/src/app/common/firebase/index.ts b/apps/demo-api/src/app/common/firebase/index.ts index 8f3f4b47b..bbf20f420 100644 --- a/apps/demo-api/src/app/common/firebase/index.ts +++ b/apps/demo-api/src/app/common/firebase/index.ts @@ -1,2 +1,4 @@ +export * from './action.context'; +export * from './action.module'; export * from './firebase.module'; export * from './firestore.module'; diff --git a/apps/demo-api/src/app/common/model/guestbook/guestbook.action.server.ts b/apps/demo-api/src/app/common/model/guestbook/guestbook.action.server.ts index b080b539d..19ce8d926 100644 --- a/apps/demo-api/src/app/common/model/guestbook/guestbook.action.server.ts +++ b/apps/demo-api/src/app/common/model/guestbook/guestbook.action.server.ts @@ -1,5 +1,5 @@ import { FirebaseServerActionsContext } from "@dereekb/firebase-server"; -import { GuestbookFirestoreCollections, UpdateGuestbookEntryParams, AsyncGuestbookEntryUpdateAction, GuestbookEntryDocument } from "@dereekb/demo-firebase"; +import { GuestbookFirestoreCollections, UpdateGuestbookEntryParams, AsyncGuestbookEntryUpdateAction, GuestbookEntryDocument, GuestbookEntry } from "@dereekb/demo-firebase"; /** * FirebaseServerActionsContextt required for GuestbookServerActions. @@ -10,7 +10,7 @@ export interface GuestbookServerActionsContext extends FirebaseServerActionsCont * Server-only guestbook actions. */ export abstract class GuestbookServerActions { - abstract guestbookEntryUpdateEntry(params: UpdateGuestbookEntryParams): AsyncGuestbookEntryUpdateAction; + abstract updateGuestbookEntry(params: UpdateGuestbookEntryParams): AsyncGuestbookEntryUpdateAction; } /** @@ -18,14 +18,14 @@ export abstract class GuestbookServerActions { */ export function guestbookServerActions(context: GuestbookServerActionsContext): GuestbookServerActions { return { - guestbookEntryUpdateEntry: guestbookEntryUpdateEntryFactory(context) + updateGuestbookEntry: guestbookEntryUpdateEntryFactory(context) }; } // MARK: Actions export function guestbookEntryUpdateEntryFactory({ firebaseServerActionTransformFunctionFactory, guestbookFirestoreCollection, guestbookEntryCollectionFactory }: GuestbookServerActionsContext) { return firebaseServerActionTransformFunctionFactory(UpdateGuestbookEntryParams, async (params) => { - const { message, signed } = params; + const { message, signed, published } = params; return async (document: GuestbookEntryDocument) => { const documentRef = document.documentRef; @@ -43,9 +43,10 @@ export function guestbookEntryUpdateEntryFactory({ firebaseServerActionTransform } else { const documentInTransaction = guestbookEntryCollectionFactory(parentGuestbook).documentAccessorForTransaction(transaction).loadDocument(documentRef); - const set = { + const set: Partial = { message, signed, + published, updatedAt: new Date() // update the updated at time }; diff --git a/apps/demo-api/src/app/common/model/guestbook/guestbook.module.ts b/apps/demo-api/src/app/common/model/guestbook/guestbook.module.ts index 9bdb41c6c..207d0420f 100644 --- a/apps/demo-api/src/app/common/model/guestbook/guestbook.module.ts +++ b/apps/demo-api/src/app/common/model/guestbook/guestbook.module.ts @@ -1,12 +1,12 @@ import { Module } from "@nestjs/common"; import { DemoFirebaseServerActionsContext } from "../../firebase/action.context"; -import { AppActionModule } from "../../firebase/action.module"; +import { DemoApiActionModule } from "../../firebase/action.module"; import { guestbookServerActions, GuestbookServerActions } from "./guestbook.action.server"; export const guestbookServerActionsFactory = (context: DemoFirebaseServerActionsContext) => guestbookServerActions(context) @Module({ - imports: [AppActionModule], + imports: [DemoApiActionModule], providers: [{ provide: GuestbookServerActions, useFactory: guestbookServerActionsFactory, diff --git a/apps/demo-api/src/app/common/model/profile/profile.action.server.ts b/apps/demo-api/src/app/common/model/profile/profile.action.server.ts index 60c973290..6c6aa01e0 100644 --- a/apps/demo-api/src/app/common/model/profile/profile.action.server.ts +++ b/apps/demo-api/src/app/common/model/profile/profile.action.server.ts @@ -1,5 +1,5 @@ import { FirebaseServerActionsContext } from "@dereekb/firebase-server"; -import { AsyncProfileUpdateAction, ProfileDocument, ProfileFirestoreCollections, profileWithUsername, SetProfileUsernameParams } from "@dereekb/demo-firebase"; +import { AsyncProfileUpdateAction, ProfileDocument, ProfileFirestoreCollections, profileWithUsername, SetProfileUsernameParams, UpdateProfileParams } from "@dereekb/demo-firebase"; import { Maybe } from "@dereekb/util"; /** @@ -12,6 +12,7 @@ export interface ProfileServerActionsContext extends FirebaseServerActionsContex */ export abstract class ProfileServerActions { abstract initProfileForUid(uid: string): Promise; + abstract updateProfile(params: UpdateProfileParams): AsyncProfileUpdateAction; abstract setProfileUsername(params: SetProfileUsernameParams): AsyncProfileUpdateAction; } @@ -21,6 +22,7 @@ export abstract class ProfileServerActions { export function profileServerActions(context: ProfileServerActionsContext): ProfileServerActions { return { initProfileForUid: initProfileForUidFactory(context), + updateProfile: updateProfileFactory(context), setProfileUsername: setProfileUsernameFactory(context) }; } @@ -84,7 +86,7 @@ export function setProfileUsernameFactory({ firebaseServerActionTransformFunctio const profilePrivateDataDocument = profilePrivateDataCollectionFactory(document).loadDocumentForTransaction(transaction); // update the username - await documentInTransaction.accessor.update({ username }); + await documentInTransaction.accessor.set({ username }, { merge: true }); // update the data on the accessor const profilePrivateData = profilePrivateDataDocument; @@ -101,3 +103,21 @@ export function setProfileUsernameFactory({ firebaseServerActionTransformFunctio }; }); } + + +export function updateProfileFactory({ firebaseServerActionTransformFunctionFactory, profileFirestoreCollection }: ProfileServerActionsContext) { + return firebaseServerActionTransformFunctionFactory(UpdateProfileParams, async (params) => { + const { bio } = params; + + return async (document: ProfileDocument) => { + const documentRef = document.documentRef; + + await profileFirestoreCollection.firestoreContext.runTransaction(async (transaction) => { + const profile = profileFirestoreCollection.documentAccessorForTransaction(transaction).loadDocument(documentRef); + profile.accessor.set({ bio }, { merge: true }) + }); + + return document; + }; + }); +} diff --git a/apps/demo-api/src/app/common/model/profile/profile.module.ts b/apps/demo-api/src/app/common/model/profile/profile.module.ts index 6c432f3d3..4ed526d0d 100644 --- a/apps/demo-api/src/app/common/model/profile/profile.module.ts +++ b/apps/demo-api/src/app/common/model/profile/profile.module.ts @@ -1,12 +1,12 @@ import { Module } from "@nestjs/common"; import { DemoFirebaseServerActionsContext } from "../../firebase/action.context"; -import { AppActionModule } from "../../firebase/action.module"; +import { DemoApiActionModule } from "../../firebase/action.module"; import { profileServerActions, ProfileServerActions } from "./profile.action.server"; export const profileServerActionsFactory = (context: DemoFirebaseServerActionsContext) => profileServerActions(context) @Module({ - imports: [AppActionModule], + imports: [DemoApiActionModule], providers: [{ provide: ProfileServerActions, useFactory: profileServerActionsFactory, diff --git a/apps/demo-api/src/app/function/guestbook/guestbookentry.function.spec.ts b/apps/demo-api/src/app/function/guestbook/guestbookentry.function.spec.ts index e394fa703..5f904607f 100644 --- a/apps/demo-api/src/app/function/guestbook/guestbookentry.function.spec.ts +++ b/apps/demo-api/src/app/function/guestbook/guestbookentry.function.spec.ts @@ -1,19 +1,12 @@ -import { guestbookEntryUpdateEntry } from './guestbookentry.update'; +import { updateGuestbookEntry } from './guestbookentry.update'; import { GuestbookEntry, UpdateGuestbookEntryParams } from '@dereekb/demo-firebase'; import { demoGuestbookEntryContext, DemoApiFunctionContextFixture, demoApiFunctionContextFactory, demoAuthorizedUserContext, demoGuestbookContext } from '../../../test/fixture'; -import { WrappedCloudFunction } from '@dereekb/firebase-server'; import { isDate, isValid } from 'date-fns'; +import { describeCloudFunctionTest } from '@dereekb/firebase-server'; demoApiFunctionContextFactory((f: DemoApiFunctionContextFixture) => { - describe('guestbookEntryUpdateEntry', () => { - - let guestbookEntryUpdateEntryCloudFn: WrappedCloudFunction; - - beforeEach(() => { - const guestbookEntryUpdateEntryFn = guestbookEntryUpdateEntry(f.nestAppPromiseGetter); - guestbookEntryUpdateEntryCloudFn = f.wrapCloudFunction(guestbookEntryUpdateEntryFn); - }); + describeCloudFunctionTest('updateGuestbookEntry', { f, fn: updateGuestbookEntry }, (updateGuestbookEntryCloudFn) => { demoAuthorizedUserContext(f, (u) => { @@ -39,7 +32,7 @@ demoApiFunctionContextFactory((f: DemoApiFunctionContextFixture) => { signed }; - await u.callCloudFunction(guestbookEntryUpdateEntryCloudFn, params); + await u.callCloudFunction(updateGuestbookEntryCloudFn, params); exists = await userGuestbookEntry.accessor.exists(); expect(exists).toBe(true); @@ -77,7 +70,7 @@ demoApiFunctionContextFactory((f: DemoApiFunctionContextFixture) => { published: true }; - await u.callCloudFunction(guestbookEntryUpdateEntryCloudFn, params); + await u.callCloudFunction(updateGuestbookEntryCloudFn, params); exists = await userGuestbookEntry.accessor.exists(); expect(exists).toBe(true); diff --git a/apps/demo-api/src/app/function/guestbook/guestbookentry.update.ts b/apps/demo-api/src/app/function/guestbook/guestbookentry.update.ts index 97d8765fd..de6749906 100644 --- a/apps/demo-api/src/app/function/guestbook/guestbookentry.update.ts +++ b/apps/demo-api/src/app/function/guestbook/guestbookentry.update.ts @@ -3,8 +3,8 @@ import { inAuthContext } from '@dereekb/firebase-server'; import { onCallWithDemoNestContext } from '../function'; import { guestbookEntryForUser } from './guestbook.util'; -export const guestbookEntryUpdateEntry = onCallWithDemoNestContext(inAuthContext(async (nest, data: UpdateGuestbookEntryParams, context) => { - const guestbookEntryUpdateEntry = await nest.guestbookActions.guestbookEntryUpdateEntry(data); +export const updateGuestbookEntry = onCallWithDemoNestContext(inAuthContext(async (nest, data: UpdateGuestbookEntryParams, context) => { + const guestbookEntryUpdateEntry = await nest.guestbookActions.updateGuestbookEntry(data); const uid = context.auth.uid; const { guestbook: guestbookId } = guestbookEntryUpdateEntry.params; @@ -12,5 +12,4 @@ export const guestbookEntryUpdateEntry = onCallWithDemoNestContext(inAuthContext const guestbookEntryDocument = guestbookEntryForUser(nest, guestbookId, uid); await guestbookEntryUpdateEntry(guestbookEntryDocument); - return { success: true }; })); diff --git a/apps/demo-api/src/app/function/profile/profile.function.spec.ts b/apps/demo-api/src/app/function/profile/profile.function.spec.ts index 6a1d4408c..8f0668f7c 100644 --- a/apps/demo-api/src/app/function/profile/profile.function.spec.ts +++ b/apps/demo-api/src/app/function/profile/profile.function.spec.ts @@ -1,6 +1,8 @@ +import { updateProfile } from './profile.update'; import { profileSetUsername } from './profile.set.username'; -import { SetProfileUsernameParams } from '@dereekb/demo-firebase'; +import { SetProfileUsernameParams, UpdateProfileParams } from '@dereekb/demo-firebase'; import { DemoApiFunctionContextFixture, demoApiFunctionContextFactory, demoAuthorizedUserContext } from '../../../test/fixture'; +import { describeCloudFunctionTest } from '@dereekb/firebase-server'; /** * NOTES: @@ -14,8 +16,10 @@ import { DemoApiFunctionContextFixture, demoApiFunctionContextFactory, demoAutho // Every test is done within its own context; the firestore/auth/etc. is empty between each test since under the hood our test app name changes. demoApiFunctionContextFactory((f: DemoApiFunctionContextFixture) => { - // jest describe - describe('profileSetUsername', () => { + // describeCloudFunctionTest wraps a jest describe along with the following: + // - Build our profileSetUsername function using our testing context instances's Nest App for each test, and the profileSetUsername factory. + // - wrap the function to make it a usable function and exposed as profileSetUsernameCloudFn + describeCloudFunctionTest('profileSetUsername', { f, fn: profileSetUsername }, (profileSetUsernameCloudFn) => { // with our DemoApiFunctionContextFixture, we can easily create a new user for this test case. demoAuthorizedUserContext(f, (u) => { @@ -24,13 +28,6 @@ demoApiFunctionContextFactory((f: DemoApiFunctionContextFixture) => { it('should set the profile username.', async () => { const username = 'username'; - - // Build our profileSetUsername function using our testing context's Nest App, and the profileSetUsername factory. - const profileSetUsernameFn = profileSetUsername(f.nestAppPromiseGetter); - - // wrap the function to make it a usable function. - const profileSetUsernameCloudFn = f.wrapCloudFunction(profileSetUsernameFn); - const params: SetProfileUsernameParams = { username }; @@ -74,4 +71,25 @@ demoApiFunctionContextFactory((f: DemoApiFunctionContextFixture) => { }); + // describe tests for updateProfile + describeCloudFunctionTest('updateProfile', { f, fn: updateProfile }, (updateProfileCloudFn) => { + + demoAuthorizedUserContext(f, (u) => { + + it(`should update the user's profile.`, async () => { + const bio = 'test bio'; + const params: UpdateProfileParams = { + bio + }; + + await u.callCloudFunction(updateProfileCloudFn, params); + + const profileData = await u.instance.loadUserProfile().snapshotData(); + expect(profileData?.bio).toBe(bio); + }); + + }); + + }); + }); diff --git a/apps/demo-api/src/app/function/profile/profile.set.username.ts b/apps/demo-api/src/app/function/profile/profile.set.username.ts index d11da143c..6de9dedf0 100644 --- a/apps/demo-api/src/app/function/profile/profile.set.username.ts +++ b/apps/demo-api/src/app/function/profile/profile.set.username.ts @@ -2,18 +2,16 @@ import { ProfileDocument, SetProfileUsernameParams } from '@dereekb/demo-firebas import { inAuthContext } from '@dereekb/firebase-server'; import { onCallWithDemoNestContext } from '../function'; import { userHasNoProfileError } from '../../common/model/profile/profile.error'; +import { profileForUser } from './profile.util'; export const profileSetUsername = onCallWithDemoNestContext(inAuthContext(async (nest, data: SetProfileUsernameParams, context) => { const setProfileUsername = await nest.profileActions.setProfileUsername(data); - const profileFirestoreCollection = nest.demoFirestoreCollections.profileFirestoreCollection; const params = setProfileUsername.params; - - let profileDocument: ProfileDocument; const uid = params.uid ?? context.auth?.uid!; - profileDocument = await profileFirestoreCollection.documentAccessor().loadDocumentForPath(uid); + const profileDocument: ProfileDocument = profileForUser(nest, uid); const exists = await profileDocument.accessor.exists(); if (!exists) { @@ -21,5 +19,4 @@ export const profileSetUsername = onCallWithDemoNestContext(inAuthContext(async } await setProfileUsername(profileDocument); - return { success: true }; })); diff --git a/apps/demo-api/src/app/function/profile/profile.update.ts b/apps/demo-api/src/app/function/profile/profile.update.ts new file mode 100644 index 000000000..768f1b9cf --- /dev/null +++ b/apps/demo-api/src/app/function/profile/profile.update.ts @@ -0,0 +1,13 @@ +import { ProfileDocument, UpdateProfileParams } from '@dereekb/demo-firebase'; +import { inAuthContext } from '@dereekb/firebase-server'; +import { onCallWithDemoNestContext } from '../function'; +import { profileForUser } from './profile.util'; + +export const updateProfile = onCallWithDemoNestContext(inAuthContext(async (nest, data: UpdateProfileParams, context) => { + const updateProfile = await nest.profileActions.updateProfile(data); + + const uid = updateProfile.params.uid ?? context.auth.uid; + + const profileDocument: ProfileDocument = profileForUser(nest, uid); + await updateProfile(profileDocument); +})); diff --git a/apps/demo-api/src/app/function/profile/profile.util.ts b/apps/demo-api/src/app/function/profile/profile.util.ts new file mode 100644 index 000000000..ad903a365 --- /dev/null +++ b/apps/demo-api/src/app/function/profile/profile.util.ts @@ -0,0 +1,9 @@ +import { ProfileDocument } from '@dereekb/demo-firebase'; +import { FirebaseAuthUserId } from '@dereekb/firebase'; +import { DemoApiNestContext } from '../function'; + +export function profileForUser(nest: DemoApiNestContext, uid: FirebaseAuthUserId): ProfileDocument { + const profileFirestoreCollection = nest.demoFirestoreCollections.profileFirestoreCollection; + const profileDocument = profileFirestoreCollection.documentAccessor().loadDocumentForPath(uid); + return profileDocument; +} diff --git a/apps/demo-api/src/main.ts b/apps/demo-api/src/main.ts index 080159a6e..40547233e 100644 --- a/apps/demo-api/src/main.ts +++ b/apps/demo-api/src/main.ts @@ -12,5 +12,6 @@ export const api = functions.https.onRequest(server); export const { initUserOnCreate, profileSetUsername, - guestbookEntryUpdateEntry + updateProfile, + updateGuestbookEntry } = demoAppFunctions(nest); diff --git a/apps/demo-firebase/src/lib/guestbook/guestbook.api.ts b/apps/demo-firebase/src/lib/guestbook/guestbook.api.ts index 4b4634632..f21c9ef93 100644 --- a/apps/demo-firebase/src/lib/guestbook/guestbook.api.ts +++ b/apps/demo-firebase/src/lib/guestbook/guestbook.api.ts @@ -1,4 +1,3 @@ -import { GuestbookEntry } from './guestbook'; import { Expose } from "class-transformer"; import { FirebaseFunctionMap, firebaseFunctionMapFactory, FirebaseFunctionMapFunction, FirebaseFunctionTypeConfigMap } from "@dereekb/firebase"; import { IsOptional, IsNotEmpty, IsString, MaxLength, IsBoolean } from "class-validator"; @@ -39,22 +38,22 @@ export class UpdateGuestbookEntryParams extends GuestbookEntryParams { } -export const guestbookEntryUpdateKey = 'guestbookEntryUpdateEntry'; -export const guestbookEntryDeleteKey = 'guestbookEntryDeleteEntry'; +export const updateGuestbookEntryKey = 'updateGuestbookEntry'; +export const deleteGuestbookEntryKey = 'deleteGuestbookEntry'; export type GuestbookFunctionTypeMap = { - [guestbookEntryUpdateKey]: [UpdateGuestbookEntryParams, GuestbookEntry] - [guestbookEntryDeleteKey]: [GuestbookEntryParams, GuestbookEntry] + [updateGuestbookEntryKey]: [UpdateGuestbookEntryParams, void] + [deleteGuestbookEntryKey]: [GuestbookEntryParams, void] } export const guestbookFunctionTypeConfigMap: FirebaseFunctionTypeConfigMap = { - [guestbookEntryUpdateKey]: null, - [guestbookEntryDeleteKey]: null + [updateGuestbookEntryKey]: null, + [deleteGuestbookEntryKey]: null } export abstract class GuestbookFunctions implements FirebaseFunctionMap { - [guestbookEntryUpdateKey]: FirebaseFunctionMapFunction; - [guestbookEntryDeleteKey]: FirebaseFunctionMapFunction; + [updateGuestbookEntryKey]: FirebaseFunctionMapFunction; + [deleteGuestbookEntryKey]: FirebaseFunctionMapFunction; } export const guestbookFunctionMap = firebaseFunctionMapFactory(guestbookFunctionTypeConfigMap); diff --git a/apps/demo-firebase/src/lib/profile/profile.api.ts b/apps/demo-firebase/src/lib/profile/profile.api.ts index 71a72ec4c..bc74b3972 100644 --- a/apps/demo-firebase/src/lib/profile/profile.api.ts +++ b/apps/demo-firebase/src/lib/profile/profile.api.ts @@ -1,15 +1,10 @@ -import { Profile } from './profile'; import { Expose } from "class-transformer"; import { FirebaseFunctionMap, firebaseFunctionMapFactory, FirebaseFunctionMapFunction, FirebaseFunctionTypeConfigMap } from "@dereekb/firebase"; import { IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator"; -export class SetProfileUsernameParams { +export const PROFILE_BIO_MAX_LENGTH = 200; - @Expose() - @IsNotEmpty() - @IsString() - @MaxLength(20) - username!: string; +export class ProfileParams { // MARK: Admin Only /** @@ -22,30 +17,55 @@ export class SetProfileUsernameParams { } +export class SetProfileUsernameParams extends ProfileParams { + + @Expose() + @IsNotEmpty() + @IsString() + @MaxLength(20) + username!: string; + +} + +export class UpdateProfileParams extends ProfileParams { + + @Expose() + @IsOptional() + @IsNotEmpty() + @IsString() + @MaxLength(PROFILE_BIO_MAX_LENGTH) + bio?: string; + +} + /** * We set the key here to allow both the functions server and the type map/client access this shared key. */ export const profileSetUsernameKey = 'profileSetUsername'; +export const updateProfileKey = 'updateProfile'; /** * This is our FirebaseFunctionTypeMap for Profile. It defines all the functions that are available. */ export type ProfileFunctionTypeMap = { - [profileSetUsernameKey]: [SetProfileUsernameParams, Profile] + [profileSetUsernameKey]: [SetProfileUsernameParams, void], + [updateProfileKey]: [UpdateProfileParams, void] } /** * This is the configuration map. It is */ export const profileFunctionTypeConfigMap: FirebaseFunctionTypeConfigMap = { - [profileSetUsernameKey]: null + [profileSetUsernameKey]: null, + [updateProfileKey]: null } /** * Declared as an abstract class so we can inject it into our Angular app using this token. */ export abstract class ProfileFunctions implements FirebaseFunctionMap { - [profileSetUsernameKey]: FirebaseFunctionMapFunction; + [profileSetUsernameKey]: FirebaseFunctionMapFunction; + [updateProfileKey]: FirebaseFunctionMapFunction; } /** diff --git a/apps/demo-firebase/src/lib/profile/profile.ts b/apps/demo-firebase/src/lib/profile/profile.ts index def613de2..671dbd784 100644 --- a/apps/demo-firebase/src/lib/profile/profile.ts +++ b/apps/demo-firebase/src/lib/profile/profile.ts @@ -11,6 +11,10 @@ export interface Profile extends UserRelatedById { * Unique username. */ username: string; + /** + * Profile biography + */ + bio?: string; /** * Last date the profile was updated at. */ @@ -26,6 +30,7 @@ export const profileCollectionPath = 'profile'; export const profileConverter = makeSnapshotConverterFunctions({ fields: { username: firestoreString({}), + bio: firestoreString({}), updatedAt: firestoreDate({ saveDefaultAsNow: true }) } }); diff --git a/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.entry.popup.component.ts b/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.entry.popup.component.ts index abc8055ff..67fe6561e 100644 --- a/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.entry.popup.component.ts +++ b/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.entry.popup.component.ts @@ -19,6 +19,7 @@ export interface DemoGuestbookEntryPopupComponentConfig {

+ ` diff --git a/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/store/guestbook.entry.document.store.ts b/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/store/guestbook.entry.document.store.ts index feddb8612..aa1cbfc83 100644 --- a/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/store/guestbook.entry.document.store.ts +++ b/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/store/guestbook.entry.document.store.ts @@ -2,7 +2,7 @@ import { first, Observable, shareReplay, from, switchMap } from 'rxjs'; import { Optional, Injectable } from "@angular/core"; import { LoadingState, loadingStateFromObs } from '@dereekb/rxjs'; import { AbstractDbxFirebaseDocumentWithParentStore } from "@dereekb/dbx-firebase"; -import { DemoFirestoreCollections, Guestbook, GuestbookDocument, GuestbookEntry, GuestbookEntryDocument, guestbookEntryUpdateKey, GuestbookFunctions, UpdateGuestbookEntryParams } from "@dereekb/demo-firebase"; +import { DemoFirestoreCollections, Guestbook, GuestbookDocument, GuestbookEntry, GuestbookEntryDocument, updateGuestbookEntryKey, GuestbookFunctions, UpdateGuestbookEntryParams } from "@dereekb/demo-firebase"; import { GuestbookDocumentStore } from "./guestbook.document.store"; @Injectable() @@ -16,11 +16,11 @@ export class GuestbookEntryDocumentStore extends AbstractDbxFirebaseDocumentWith } } - updateEntry(params: Omit): Observable> { + updateEntry(params: Omit): Observable> { return this.parent$.pipe( first(), switchMap((parent) => - loadingStateFromObs(from(this.guestbookFunctions[guestbookEntryUpdateKey]({ + loadingStateFromObs(from(this.guestbookFunctions[updateGuestbookEntryKey]({ ...params, guestbook: parent.id }))) diff --git a/packages/firebase-server/src/lib/firebase/firebase.nest.ts b/packages/firebase-server/src/lib/firebase/firebase.nest.ts index e9d18c01e..530067764 100644 --- a/packages/firebase-server/src/lib/firebase/firebase.nest.ts +++ b/packages/firebase-server/src/lib/firebase/firebase.nest.ts @@ -1,5 +1,9 @@ import * as admin from 'firebase-admin'; -import { FactoryProvider, InjectionToken, Module } from "@nestjs/common"; +import { FactoryProvider, InjectionToken, Module, ModuleMetadata, Provider } from "@nestjs/common"; +import { FirestoreContext } from '@dereekb/firebase'; +import { Firestore } from '@google-cloud/firestore'; +import { googleCloudFirestoreContextFactory } from '../firestore/firestore'; +import { ClassLikeType } from '@dereekb/util'; // MARK: Tokens /** @@ -37,3 +41,58 @@ export function firebaseServerAppTokenProvider(useFactory: () => admin.app.App): exports: [FIRESTORE_TOKEN] }) export class FirebaseServerFirestoreModule { } + +/** + * Nest provider module for firebase that includes the FirebaseServerFirestoreModule and provides a value for FIRESTORE_CONTEXT_TOKEN using the googleCloudFirestoreContextFactory. + */ +@Module({ + imports: [FirebaseServerFirestoreModule], + providers: [{ + provide: FIRESTORE_CONTEXT_TOKEN, + useFactory: googleCloudFirestoreContextFactory, + inject: [FIRESTORE_TOKEN] + }], + exports: [FirebaseServerFirestoreModule, FIRESTORE_CONTEXT_TOKEN] +}) +export class FirebaseServerFirestoreContextModule { } + +// MARK: AppFirestoreCollections +export type ProvideAppFirestoreCollectionsFactory = (context: FirestoreContext) => T; + +export interface ProvideAppFirestoreCollectionsConfig { + provide: ClassLikeType; + useFactory: ProvideAppFirestoreCollectionsFactory; +} + +/** + * Used to configure a Nestjs provider for a FirestoreCollections-type object that is initialized with a FirestoreContext. + * + * @param type + * @param useFactory + * @returns + */ +export function provideAppFirestoreCollections({ provide, useFactory }: ProvideAppFirestoreCollectionsConfig): [Provider] { + return [{ + provide, + useFactory, + inject: [FIRESTORE_CONTEXT_TOKEN] + }]; +} + +// MARK: app firestore module +export interface ProvideAppModuleMetadataConfig extends ProvideAppFirestoreCollectionsConfig, Pick { } + +/** + * Convenience function used to generate ModuleMetadata for an app's Firestore related modules and an appFirestoreCollection + * + * @param provide + * @param useFactory + * @returns + */ +export function appFirestoreModuleMetadata(config: ProvideAppModuleMetadataConfig): ModuleMetadata { + return { + imports: [FirebaseServerFirestoreContextModule, ...(config.imports ?? [])], + exports: [FirebaseServerFirestoreContextModule, config.provide, ...(config.exports ?? [])], + providers: [...provideAppFirestoreCollections(config), ...(config.providers ?? [])] + }; +} diff --git a/packages/firebase-server/src/lib/function/index.ts b/packages/firebase-server/src/lib/function/index.ts index d19f0ee17..f583e3508 100644 --- a/packages/firebase-server/src/lib/function/index.ts +++ b/packages/firebase-server/src/lib/function/index.ts @@ -1,3 +1,4 @@ export * from './context'; export * from './assert'; export * from './error'; +export * from './type'; diff --git a/packages/firebase-server/src/lib/function/type.ts b/packages/firebase-server/src/lib/function/type.ts new file mode 100644 index 000000000..5460c8498 --- /dev/null +++ b/packages/firebase-server/src/lib/function/type.ts @@ -0,0 +1,6 @@ +import { HttpsFunction, Runnable } from "firebase-functions"; + +/** + * Union of firebase-functions HttpsFunction and Runnable + */ +export type RunnableHttpFunction = HttpsFunction & Runnable; diff --git a/packages/firebase-server/src/lib/nest/function/nest.ts b/packages/firebase-server/src/lib/nest/function/nest.ts index 65c4d3f2e..8818a8c27 100644 --- a/packages/firebase-server/src/lib/nest/function/nest.ts +++ b/packages/firebase-server/src/lib/nest/function/nest.ts @@ -2,6 +2,7 @@ import { PromiseOrValue, Getter } from '@dereekb/util'; import { INestApplicationContext } from '@nestjs/common'; import * as functions from 'firebase-functions'; import { EventContext, HttpsFunction, Runnable } from 'firebase-functions'; +import { RunnableHttpFunction } from '../../function/type'; // MARK: Nest /** @@ -16,6 +17,8 @@ export type NestApplicationPromiseGetter = Getter = (nestAppPromiseGetter: NestApplicationPromiseGetter) => F; +export type NestApplicationRunnableHttpFunctionFactory = NestApplicationFunctionFactory>; + /** * Runnable function that is passed an INestApplicationContext in addition to the usual data/context provided by firebase. */ @@ -24,7 +27,7 @@ export type OnCallWithNestApplication = (nest: INestApplicatio /** * Factory function for generating a NestApplicationFunctionFactory for a HttpsFunctions/Runnable firebase function. */ -export type OnCallWithNestApplicationFactory = (fn: OnCallWithNestApplication) => NestApplicationFunctionFactory>; +export type OnCallWithNestApplicationFactory = (fn: OnCallWithNestApplication) => NestApplicationRunnableHttpFunctionFactory; /** * Creates a factory for generating OnCallWithNestApplication functions. @@ -46,7 +49,7 @@ export type OnCallWithNestContext = (nestContext: C, data: /** * Factory function for generating HttpsFunctions/Runnable firebase function that returns the value from the input OnCallWithNestContext function. */ -export type OnCallWithNestContextFactory = (fn: OnCallWithNestContext) => NestApplicationFunctionFactory>; +export type OnCallWithNestContextFactory = (fn: OnCallWithNestContext) => NestApplicationRunnableHttpFunctionFactory; /** * Getter for an INestApplicationContext promise. Nest should be initialized when the promise resolves. diff --git a/packages/firebase-server/src/test/firebase/firebase.admin.function.ts b/packages/firebase-server/src/test/firebase/firebase.admin.function.ts index a37407eb6..d8683d26c 100644 --- a/packages/firebase-server/src/test/firebase/firebase.admin.function.ts +++ b/packages/firebase-server/src/test/firebase/firebase.admin.function.ts @@ -4,7 +4,7 @@ import { Firestore } from '@google-cloud/firestore'; import { Auth } from 'firebase-admin/lib/auth/auth'; import { FeaturesList } from 'firebase-functions-test/lib/features'; import { TestFirestoreContext, TestFirestoreInstance } from '@dereekb/firebase'; -import { AbstractJestTestContextFixture, jestTestContextBuilder, JestTestContextFactory, Maybe } from "@dereekb/util"; +import { AbstractJestTestContextFixture, Getter, jestTestContextBuilder, JestTestContextFactory, JestTestContextFixture, Maybe } from "@dereekb/util"; import { applyFirebaseGCloudTestProjectIdToFirebaseConfigEnv, getGCloudTestProjectId, isAdminEnvironmentInitialized, rollNewGCloudProjectEnvironmentVariable } from './firebase'; import { FirebaseAdminTestContext, FirebaseAdminTestContextInstance, WrapCloudFunction } from './firebase.admin'; @@ -54,6 +54,8 @@ export interface FirebaseAdminFunctionTestConfig { export interface FirebaseAdminFunctionTestContext extends FirebaseAdminTestContext { } +export interface FullFirebaseAdminFunctionTestContext extends FirebaseAdminFunctionTestContext, JestTestContextFixture { } + export class FirebaseAdminFunctionTestContextFixture extends AbstractJestTestContextFixture implements FirebaseAdminFunctionTestContext { // MARK: FirebaseAdminTestContext (Forwarded) diff --git a/packages/firebase-server/src/test/firebase/firebase.admin.nest.function.context.ts b/packages/firebase-server/src/test/firebase/firebase.admin.nest.function.context.ts new file mode 100644 index 000000000..38f09cb95 --- /dev/null +++ b/packages/firebase-server/src/test/firebase/firebase.admin.nest.function.context.ts @@ -0,0 +1,39 @@ +import { useJestFunctionFixture } from "@dereekb/util"; +import { NestApplicationRunnableHttpFunctionFactory } from "../../lib/nest/function/nest"; +import { WrappedCloudFunction } from "./firebase.admin"; +import { FirebaseAdminFunctionNestTestContext, wrapCloudFunctionForNestTestsGetter } from "./firebase.admin.nest.function"; + +export interface CloudFunctionTestConfig { + f: FirebaseAdminFunctionNestTestContext; + fn: NestApplicationRunnableHttpFunctionFactory; +} + +/** + * Used to provide a test builder that exposes a WrappedCloudFunction using the input configuration. + * + * @param config + * @param buildTests + */ +export function cloudFunctionTest(config: CloudFunctionTestConfig, buildTests: (fn: WrappedCloudFunction) => void) { + const { f, fn } = config; + + useJestFunctionFixture>({ + fn: () => { + const x = wrapCloudFunctionForNestTestsGetter(f, fn)(); + return x; + } + }, buildTests); +} + +/** + * Convenience function that calls describe and cloudFunctionContext together. + * + * @param label + * @param config + * @param buildTests + */ +export function describeCloudFunctionTest(label: string, config: CloudFunctionTestConfig, buildTests: (fn: WrappedCloudFunction) => void) { + describe(label, () => { + cloudFunctionTest(config, buildTests); + }); +} diff --git a/packages/firebase-server/src/test/firebase/firebase.admin.nest.function.ts b/packages/firebase-server/src/test/firebase/firebase.admin.nest.function.ts index 4c741267b..d9b62b569 100644 --- a/packages/firebase-server/src/test/firebase/firebase.admin.nest.function.ts +++ b/packages/firebase-server/src/test/firebase/firebase.admin.nest.function.ts @@ -1,10 +1,16 @@ -import { JestBuildTestsWithContextFunction, JestTestContextFactory, JestTestContextFixture } from "@dereekb/util"; +import { Getter, JestBuildTestsWithContextFunction, JestTestContextFactory, JestTestContextFixture, useJestContextFixture } from "@dereekb/util"; import { firebaseAdminNestContextWithFixture, FirebaseAdminNestTestConfig, FirebaseAdminNestTestContext, FirebaseAdminNestTestContextFixture, FirebaseAdminNestTestContextInstance } from "./firebase.admin.nest"; import { FirebaseAdminFunctionTestContextInstance, firebaseAdminFunctionTestContextFactory } from "./firebase.admin.function"; -import { WrapCloudFunction } from './firebase.admin'; +import { FirebaseAdminCloudFunctionWrapper, WrapCloudFunction, wrapCloudFunctionForTests, WrappedCloudFunction } from './firebase.admin'; +import { NestApplicationRunnableHttpFunctionFactory } from "../../lib/nest/function/nest"; + +// MARK: Utility +export function wrapCloudFunctionForNestTestsGetter(wrapper: FirebaseAdminFunctionNestTestContext, fn: NestApplicationRunnableHttpFunctionFactory): Getter> { + return wrapCloudFunctionForTests(wrapper, () => fn(wrapper.nestAppPromiseGetter)); +} // MARK: FirebaseAdminFunction -export interface FirebaseAdminFunctionNestTestContext extends FirebaseAdminNestTestContext { +export interface FirebaseAdminFunctionNestTestContext extends FirebaseAdminNestTestContext, FirebaseAdminCloudFunctionWrapper { get wrapCloudFunction(): WrapCloudFunction; } @@ -19,6 +25,14 @@ export class FirebaseAdminFunctionNestTestContextFixture< return this.parent.instance.wrapCloudFunction; } + wrapCloudFunctionForNestTests(fn: NestApplicationRunnableHttpFunctionFactory): WrappedCloudFunction { + return this.wrapCloudFunctionForNestTestsGetter(fn)(); + } + + wrapCloudFunctionForNestTestsGetter(fn: NestApplicationRunnableHttpFunctionFactory): Getter> { + return wrapCloudFunctionForNestTestsGetter(this, fn); + } + } export class FirebaseAdminFunctionNestTestContextInstance< diff --git a/packages/firebase-server/src/test/firebase/firebase.admin.nest.ts b/packages/firebase-server/src/test/firebase/firebase.admin.nest.ts index 1afb5f59e..aa0f5d38e 100644 --- a/packages/firebase-server/src/test/firebase/firebase.admin.nest.ts +++ b/packages/firebase-server/src/test/firebase/firebase.admin.nest.ts @@ -1,5 +1,5 @@ import { AbstractChildJestTestContextFixture, ArrayOrValue, asArray, ClassType, Getter, JestBuildTestsWithContextFunction, JestTestContextFactory, JestTestContextFixture, useJestContextFixture } from "@dereekb/util"; -import { AbstractFirebaseAdminTestContextInstanceChild, firebaseAdminTestContextFactory, FirebaseAdminTestContextInstance } from './firebase.admin'; +import { AbstractFirebaseAdminTestContextInstanceChild, FirebaseAdminCloudFunctionWrapper, firebaseAdminTestContextFactory, FirebaseAdminTestContextInstance, wrapCloudFunctionForTests, WrapCloudFunctionInput, WrappedCloudFunction } from './firebase.admin'; import { Abstract, DynamicModule, INestApplicationContext, Provider, Type } from '@nestjs/common/interfaces'; import { NestAppPromiseGetter } from "../../lib/nest/app"; import { Test, TestingModule } from '@nestjs/testing'; @@ -11,6 +11,8 @@ export interface FirebaseAdminNestTestContext { get(typeOrToken: Type | Abstract | string | symbol, options?: { strict: boolean; }): TResult; } +export type FirebaseAdminNestTestContextFixtureType = FirebaseAdminNestTestContext & JestTestContextFixture; + export class FirebaseAdminNestTestContextFixture< PI extends FirebaseAdminTestContextInstance = FirebaseAdminTestContextInstance, PF extends JestTestContextFixture = JestTestContextFixture, diff --git a/packages/firebase-server/src/test/firebase/firebase.admin.ts b/packages/firebase-server/src/test/firebase/firebase.admin.ts index cb2bb2f29..bde885726 100644 --- a/packages/firebase-server/src/test/firebase/firebase.admin.ts +++ b/packages/firebase-server/src/test/firebase/firebase.admin.ts @@ -2,28 +2,38 @@ import * as admin from 'firebase-admin'; import { Firestore } from '@google-cloud/firestore'; import { Auth } from 'firebase-admin/lib/auth/auth'; import { JestTestFirestoreContextFactory, makeTestingFirestoreDrivers, TestFirestoreContext, TestFirestoreContextFixture, TestFirestoreInstance } from '@dereekb/firebase'; -import { AbstractJestTestContextFixture, cachedGetter, JestBuildTestsWithContextFunction, jestTestContextBuilder, JestTestContextFactory, JestTestContextFixture, useJestContextFixture } from "@dereekb/util"; +import { AbstractJestTestContextFixture, cachedGetter, Getter, JestBuildTestsWithContextFunction, jestTestContextBuilder, JestTestContextFactory, JestTestContextFixture, useJestContextFixture } from "@dereekb/util"; import { googleCloudFirestoreDrivers } from '../../lib/firestore/driver'; import { GoogleCloudTestFirestoreInstance } from '../firestore/firestore'; +import { CloudFunction as CloudFunctionV1 } from 'firebase-functions'; import { generateNewProjectId, isAdminEnvironmentInitialized } from './firebase'; import { wrap, WrappedFunction, WrappedScheduledFunction } from 'firebase-functions-test/lib/main'; export interface FirebaseAdminTestConfig { } export type WrapCloudFunction = typeof wrap; +export type WrapCloudFunctionInput = CloudFunctionV1; export type WrappedCloudFunction = WrappedScheduledFunction | WrappedFunction; -export interface FirebaseAdminTestContext { - readonly app: admin.app.App; - readonly auth: Auth; - readonly firestore: Firestore - readonly firestoreInstance: TestFirestoreInstance; - readonly firestoreContext: TestFirestoreContext; +export interface FirebaseAdminCloudFunctionWrapper { /** * Wrap function if available. If not in the right context/supported then this will throw an exception. */ get wrapCloudFunction(): WrapCloudFunction; + +} + +export function wrapCloudFunctionForTests = WrapCloudFunctionInput>(wrapper: FirebaseAdminCloudFunctionWrapper, getter: Getter): Getter> { + return () => wrapper.wrapCloudFunction(getter()); +} + +export interface FirebaseAdminTestContext extends FirebaseAdminCloudFunctionWrapper { + readonly app: admin.app.App; + readonly auth: Auth; + readonly firestore: Firestore + readonly firestoreInstance: TestFirestoreInstance; + readonly firestoreContext: TestFirestoreContext; } export class FirebaseAdminTestContextFixture extends AbstractJestTestContextFixture implements FirebaseAdminTestContext { diff --git a/packages/firebase-server/src/test/firebase/index.ts b/packages/firebase-server/src/test/firebase/index.ts index f603fc427..c94988aec 100644 --- a/packages/firebase-server/src/test/firebase/index.ts +++ b/packages/firebase-server/src/test/firebase/index.ts @@ -3,6 +3,7 @@ export * from './firebase.admin'; export * from './firebase.admin.nest'; export * from './firebase.admin.function'; export * from './firebase.admin.nest.function'; +export * from './firebase.admin.nest.function.context'; export * from './firebase.admin.auth'; export * from './firebase.admin.collection'; diff --git a/packages/util/src/lib/function/function.forward.spec.ts b/packages/util/src/lib/function/function.forward.spec.ts new file mode 100644 index 000000000..0ba1c45fd --- /dev/null +++ b/packages/util/src/lib/function/function.forward.spec.ts @@ -0,0 +1,17 @@ + +import { forwardFunction } from './function.forward'; + +describe('forwardFunction()', () => { + + it('should wrap a function.', () => { + const fn = (input: number) => (input + 1); + + const result = forwardFunction(() => fn); + expect(result).toBeDefined(); + + const value = 1; + const output = result(value); + expect(output).toBe(value + 1); + }); + +}); diff --git a/packages/util/src/lib/function/function.forward.ts b/packages/util/src/lib/function/function.forward.ts new file mode 100644 index 000000000..87af998ff --- /dev/null +++ b/packages/util/src/lib/function/function.forward.ts @@ -0,0 +1,19 @@ +import { Getter } from "../getter/getter"; + + +export type ForwardFunction O, O = any> = I; + +/** + * Wraps a Getter that returns a function. When the function is invoked, the getter retrieves the function then calls it with the input arguments. + * + * @param getter + * @returns + */ +export function forwardFunction O, O = any>(getter: Getter): ForwardFunction { + const fn = ((...args: any[]) => { + const forwardFn = getter(); + return forwardFn(...args); + }) as ForwardFunction; + + return fn; +} diff --git a/packages/util/src/lib/function/index.ts b/packages/util/src/lib/function/index.ts new file mode 100644 index 000000000..9092bbe2b --- /dev/null +++ b/packages/util/src/lib/function/index.ts @@ -0,0 +1 @@ +export * from './function.forward'; diff --git a/packages/util/src/lib/index.ts b/packages/util/src/lib/index.ts index dcbe88154..fda7160f9 100644 --- a/packages/util/src/lib/index.ts +++ b/packages/util/src/lib/index.ts @@ -3,6 +3,7 @@ export * from './assertion'; export * from './contact'; export * from './error'; export * from './filter'; +export * from './function'; export * from './getter'; export * from './iterable'; export * from './map'; diff --git a/packages/util/src/test/index.ts b/packages/util/src/test/index.ts index a59f052e9..61eec7175 100644 --- a/packages/util/src/test/index.ts +++ b/packages/util/src/test/index.ts @@ -1,2 +1,3 @@ export * from './jest'; export * from './jest.wrap'; +export * from './jest.function'; diff --git a/packages/util/src/test/jest.function.ts b/packages/util/src/test/jest.function.ts new file mode 100644 index 000000000..aebfd087a --- /dev/null +++ b/packages/util/src/test/jest.function.ts @@ -0,0 +1,18 @@ +import { forwardFunction } from "../lib/function/function.forward"; +import { Getter } from "../lib/getter/getter"; + +export interface UseJestFunctionFixture O, O = any> { + fn: Getter; +} + +export type JestFunctionFixtureBuildTests = (fn: I) => void; + +/** + * Creates a test context and jest configurations that provides a function to build tests based on the configuration. + */ +export function useJestFunctionFixture O, O = any>(config: UseJestFunctionFixture, buildTests: JestFunctionFixtureBuildTests): void { + const { fn } = config; + + const forward = forwardFunction(fn); + buildTests(forward); +}