Skip to content

Commit

Permalink
feat: added jestFunctionFixture
Browse files Browse the repository at this point in the history
- added appFirestoreModuleMetadata for easier configuration of an app's FirestoreCollections-type provider and module
- added ForwardFunction
- added describeCloudFunctionTest
  • Loading branch information
dereekb committed May 10, 2022
1 parent c89cc82 commit 1ea2d7d
Show file tree
Hide file tree
Showing 37 changed files with 368 additions and 107 deletions.
8 changes: 5 additions & 3 deletions apps/demo-api/src/app/app.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
};
}
6 changes: 3 additions & 3 deletions apps/demo-api/src/app/common/firebase/action.module.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -12,12 +12,12 @@ const demoFirebaseServerActionsContextFactory = (collections: DemoFirestoreColle
}

@Module({
imports: [AppFirestoreModule],
imports: [DemoApiFirestoreModule],
providers: [{
provide: DemoFirebaseServerActionsContext,
useFactory: demoFirebaseServerActionsContextFactory,
inject: [DemoFirestoreCollections]
}],
exports: [DemoFirebaseServerActionsContext]
})
export class AppActionModule { }
export class DemoApiActionModule { }
6 changes: 3 additions & 3 deletions apps/demo-api/src/app/common/firebase/firebase.module.ts
Original file line number Diff line number Diff line change
@@ -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 { }
25 changes: 6 additions & 19 deletions apps/demo-api/src/app/common/firebase/firestore.module.ts
Original file line number Diff line number Diff line change
@@ -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 { }
2 changes: 2 additions & 0 deletions apps/demo-api/src/app/common/firebase/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './action.context';
export * from './action.module';
export * from './firebase.module';
export * from './firestore.module';
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -10,22 +10,22 @@ export interface GuestbookServerActionsContext extends FirebaseServerActionsCont
* Server-only guestbook actions.
*/
export abstract class GuestbookServerActions {
abstract guestbookEntryUpdateEntry(params: UpdateGuestbookEntryParams): AsyncGuestbookEntryUpdateAction<UpdateGuestbookEntryParams>;
abstract updateGuestbookEntry(params: UpdateGuestbookEntryParams): AsyncGuestbookEntryUpdateAction<UpdateGuestbookEntryParams>;
}

/**
* Factory for generating GuestbookServerActions for a given context.
*/
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;
Expand All @@ -43,9 +43,10 @@ export function guestbookEntryUpdateEntryFactory({ firebaseServerActionTransform
} else {
const documentInTransaction = guestbookEntryCollectionFactory(parentGuestbook).documentAccessorForTransaction(transaction).loadDocument(documentRef);

const set = {
const set: Partial<GuestbookEntry> = {
message,
signed,
published,
updatedAt: new Date() // update the updated at time
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand All @@ -12,6 +12,7 @@ export interface ProfileServerActionsContext extends FirebaseServerActionsContex
*/
export abstract class ProfileServerActions {
abstract initProfileForUid(uid: string): Promise<ProfileDocument>;
abstract updateProfile(params: UpdateProfileParams): AsyncProfileUpdateAction<UpdateProfileParams>;
abstract setProfileUsername(params: SetProfileUsernameParams): AsyncProfileUpdateAction<SetProfileUsernameParams>;
}

Expand All @@ -21,6 +22,7 @@ export abstract class ProfileServerActions {
export function profileServerActions(context: ProfileServerActionsContext): ProfileServerActions {
return {
initProfileForUid: initProfileForUidFactory(context),
updateProfile: updateProfileFactory(context),
setProfileUsername: setProfileUsernameFactory(context)
};
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
};
});
}
4 changes: 2 additions & 2 deletions apps/demo-api/src/app/common/model/profile/profile.module.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UpdateGuestbookEntryParams>;

beforeEach(() => {
const guestbookEntryUpdateEntryFn = guestbookEntryUpdateEntry(f.nestAppPromiseGetter);
guestbookEntryUpdateEntryCloudFn = f.wrapCloudFunction(guestbookEntryUpdateEntryFn);
});
describeCloudFunctionTest('updateGuestbookEntry', { f, fn: updateGuestbookEntry }, (updateGuestbookEntryCloudFn) => {

demoAuthorizedUserContext(f, (u) => {

Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ 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;

const guestbookEntryDocument = guestbookEntryForUser(nest, guestbookId, uid);

await guestbookEntryUpdateEntry(guestbookEntryDocument);
return { success: true };
}));
38 changes: 28 additions & 10 deletions apps/demo-api/src/app/function/profile/profile.function.spec.ts
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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) => {
Expand All @@ -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
};
Expand Down Expand Up @@ -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);
});

});

});

});
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@ 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) {
throw userHasNoProfileError(uid);
}

await setProfileUsername(profileDocument);
return { success: true };
}));
13 changes: 13 additions & 0 deletions apps/demo-api/src/app/function/profile/profile.update.ts
Original file line number Diff line number Diff line change
@@ -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);
}));
9 changes: 9 additions & 0 deletions apps/demo-api/src/app/function/profile/profile.util.ts
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 2 additions & 1 deletion apps/demo-api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export const api = functions.https.onRequest(server);
export const {
initUserOnCreate,
profileSetUsername,
guestbookEntryUpdateEntry
updateProfile,
updateGuestbookEntry
} = demoAppFunctions(nest);
Loading

0 comments on commit 1ea2d7d

Please sign in to comment.