Skip to content

Commit

Permalink
refactor: updated OnCallWithNestContextRequest to use single object
Browse files Browse the repository at this point in the history
BREAKING CHANGE: updated all onCall and functions to now use a single request object instead of multiple parameters
  • Loading branch information
dereekb committed Jun 4, 2022
1 parent 29c1940 commit 2ef4002
Show file tree
Hide file tree
Showing 31 changed files with 387 additions and 239 deletions.
3 changes: 2 additions & 1 deletion apps/demo-api/src/app/function/auth/init.user.function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { onEventWithDemoNestContext } from '../function';
*/
export const initUserOnCreate = onEventWithDemoNestContext<UserRecord>((withNest) =>
functions.auth.user().onCreate(
withNest(async (nest, data: UserRecord) => {
withNest(async (request) => {
const { nest, auth, data } = request;
const uid = data.uid;

if (uid) {
Expand Down
3 changes: 2 additions & 1 deletion apps/demo-api/src/app/function/guestbook/guestbook.crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { CreateGuestbookParams } from '@dereekb/demo-firebase';
import { onCallCreateModelResultWithDocs } from '@dereekb/firebase';
import { DemoCreateModelfunction } from '../function';

export const createGuestbook: DemoCreateModelfunction<CreateGuestbookParams> = async (nest, data, context) => {
export const createGuestbook: DemoCreateModelfunction<CreateGuestbookParams> = async (request) => {
const { nest, auth, data } = request;
const createGuestbook = await nest.guestbookActions.createGuestbook(data);
const guestbook = await createGuestbook();
return onCallCreateModelResultWithDocs(guestbook);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { UpdateGuestbookEntryParams } from '@dereekb/demo-firebase';
import { DemoUpdateModelfunction } from '../function';
import { guestbookEntryForUser } from './guestbook.util';

export const updateGuestbookEntry: DemoUpdateModelfunction<UpdateGuestbookEntryParams> = async (nest, data, context) => {
export const updateGuestbookEntry: DemoUpdateModelfunction<UpdateGuestbookEntryParams> = async (request) => {
const { nest, auth, data } = request;
const guestbookEntryUpdateEntry = await nest.guestbookActions.updateGuestbookEntry(data);

const uid = context.auth.uid;
const uid = auth.uid;
const { guestbook: guestbookId } = guestbookEntryUpdateEntry.params;

const guestbookEntryDocument = guestbookEntryForUser(nest, guestbookId, uid);
Expand Down
15 changes: 6 additions & 9 deletions apps/demo-api/src/app/function/profile/profile.set.username.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@ import { ProfileDocument, SetProfileUsernameParams } from '@dereekb/demo-firebas
import { inAuthContext } from '@dereekb/firebase-server';
import { onCallWithDemoNestContext } from '../function';
import { userHasNoProfileError } from '../../common';
import { profileForUser } from './profile.util';
import { profileForUserRequest } from './profile.util';

export const profileSetUsername = onCallWithDemoNestContext(
inAuthContext(async (nest, data: SetProfileUsernameParams, context) => {
export const profileSetUsername = onCallWithDemoNestContext<SetProfileUsernameParams>(
inAuthContext(async (request) => {
const { nest, auth, data } = request;
const setProfileUsername = await nest.profileActions.setProfileUsername(data);

const params = setProfileUsername.params;
const uid = params.uid ?? context.auth?.uid!;

const profileDocument: ProfileDocument = profileForUser(nest, uid);
const profileDocument: ProfileDocument = await profileForUserRequest(request);
const exists = await profileDocument.accessor.exists();

if (!exists) {
throw userHasNoProfileError(uid);
throw userHasNoProfileError(auth.uid);
}

await setProfileUsername(profileDocument);
Expand Down
28 changes: 4 additions & 24 deletions apps/demo-api/src/app/function/profile/profile.update.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
import { ProfileDocument, UpdateProfileParams } from '@dereekb/demo-firebase';
import { DemoUpdateModelfunction } from '../function';
import { profileForUser } from './profile.util';
import { profileForUserRequest } from './profile.util';

export const updateProfile: DemoUpdateModelfunction<UpdateProfileParams> = async (nest, data, context) => {
export const updateProfile: DemoUpdateModelfunction<UpdateProfileParams> = async (request) => {
const { nest, auth, data } = request;
const updateProfile = await nest.profileActions.updateProfile(data);

const uid = context.auth.uid;
let profileDocument: ProfileDocument;

if (updateProfile.params.key != null) {
profileDocument = await nest.useModel('profile', {
context,
key: updateProfile.params.key,
roles: 'read',
use: (x) => x.document
});

// Alternative way using model() chain
/*
profileDocument = await nest.model(context)('profile')(updateProfile.params.key)('read')((x) => {
return x.document;
});
*/
} else {
profileDocument = profileForUser(nest, uid);
}

const profileDocument: ProfileDocument = await profileForUserRequest(request);
await updateProfile(profileDocument);
};
30 changes: 29 additions & 1 deletion apps/demo-api/src/app/function/profile/profile.util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
import { ProfileDocument } from '@dereekb/demo-firebase';
import { ProfileDocument, ProfileParams } from '@dereekb/demo-firebase';
import { FirebaseAuthUserId } from '@dereekb/firebase';
import { NestContextCallableRequestWithAuth } from '@dereekb/firebase-server';
import { DemoApiNestContext } from '../function';

export function profileForUser(nest: DemoApiNestContext, uid: FirebaseAuthUserId): ProfileDocument {
const profileFirestoreCollection = nest.demoFirestoreCollections.profileCollection;
const profileDocument = profileFirestoreCollection.documentAccessor().loadDocumentForPath(uid);
return profileDocument;
}

export async function profileForUserRequest(request: NestContextCallableRequestWithAuth<DemoApiNestContext, ProfileParams>): Promise<ProfileDocument> {
const { nest, data: params, auth } = request;
const profileFirestoreCollection = nest.demoFirestoreCollections.profileCollection;

let profileDocument: ProfileDocument;

if (params.key != null) {
profileDocument = await nest.useModel('profile', {
request,
key: params.key,
roles: 'owner',
use: (x) => x.document
});

// Alternative way using model() chain
/*
profileDocument = await nest.model(context)('profile')(updateProfile.params.key)('read')((x) => {
return x.document;
});
*/
} else {
profileDocument = profileForUser(nest, auth.uid);
}

return profileDocument;
}
6 changes: 3 additions & 3 deletions packages/firebase-server/src/lib/function/context.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as functions from 'firebase-functions';
import { unauthenticatedContextHasNoAuthData } from './error';

export type CallableContextWithAuthData = Omit<functions.https.CallableContext, 'auth'> & Required<Pick<functions.https.CallableContext, 'auth'>>;
export type CallableContextWithAuthData<R extends functions.https.CallableContext = functions.https.CallableContext> = Omit<R, 'auth'> & Required<Pick<R, 'auth'>>;

export function isContextWithAuthData(context: functions.https.CallableContext): context is CallableContextWithAuthData {
export function isContextWithAuthData<R extends functions.https.CallableContext>(context: functions.https.CallableContext): context is CallableContextWithAuthData<R> {
return Boolean(context.auth !== null && context.auth?.uid);
}

export function assertIsContextWithAuthData(context: functions.https.CallableContext): asserts context is CallableContextWithAuthData {
export function assertIsContextWithAuthData<R extends functions.https.CallableContext>(context: functions.https.CallableContext): asserts context is CallableContextWithAuthData<R> {
if (!isContextWithAuthData(context)) {
throw unauthenticatedContextHasNoAuthData();
}
Expand Down
46 changes: 46 additions & 0 deletions packages/firebase-server/src/lib/nest/function/call.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Configurable, PromiseOrValue } from '@dereekb/util';
import { CallableRequest } from 'firebase-functions/lib/common/providers/https';
import { NestApplicationContextRequest, NestContextCallableRequest, NestContextCallableRequestWithAuth } from './nest';
import { isContextWithAuthData } from '../../function/context';
import { unauthenticatedContextHasNoUidError } from '../../function/error';
import { MakeNestContext } from '../nest.provider';

// MARK: Application
export type OnCallWithNestApplicationRequest<I> = NestApplicationContextRequest<CallableRequest<I>>;

/**
* Runnable function that is passed an INestApplicationContext in addition to the usual data/context provided by firebase.
*/
export type OnCallWithNestApplication<I = unknown, O = unknown> = (request: OnCallWithNestApplicationRequest<I>) => PromiseOrValue<O>;

// MARK: Context
export type OnCallWithNestContextRequest<N, I> = NestContextCallableRequest<N, I>;

/**
* Runnable function that is passed an arbitrary nest context object in addition to the usual data/context provided by firebase.
*/
export type OnCallWithNestContext<N, I = unknown, O = unknown> = (request: OnCallWithNestContextRequest<N, I>) => PromiseOrValue<O>;

export function setNestContextOnRequest<N, I>(makeNestContext: MakeNestContext<N>, request: OnCallWithNestApplicationRequest<I>): OnCallWithNestContextRequest<N, I> {
(request as unknown as Configurable<OnCallWithNestContextRequest<N, I>>).nest = makeNestContext(request.nestApplication);
return request as unknown as OnCallWithNestContextRequest<N, I>;
}

// MARK: Context With Auth
export type OnCallWithAuthorizedNestContext<N, I = unknown, O = unknown> = (request: NestContextCallableRequestWithAuth<N, I>) => PromiseOrValue<O>;

/**
* Creates an OnCallWithNestContext wrapper that validates the input CallableContext to assert the context has auth data before entering the function.
*
* @param fn
* @returns
*/
export function inAuthContext<N, I, O>(fn: OnCallWithAuthorizedNestContext<N, I, O>): OnCallWithNestContext<N, I, O> {
return (request) => {
if (isContextWithAuthData(request)) {
return fn(request);
} else {
throw unauthenticatedContextHasNoUidError();
}
};
}
38 changes: 36 additions & 2 deletions packages/firebase-server/src/lib/nest/function/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,40 @@
import { TransformAndValidateFunctionResultFactory, TransformAndValidateObjectFactory } from '@dereekb/model';
import { firebaseServerActionsTransformContext, FirebaseServerActionsTransformContext } from './transform';
import { ValidationError } from 'class-validator';
import { toTransformAndValidateFunctionResultFactory, TransformAndValidateFunctionResultFactory, transformAndValidateObjectFactory, TransformAndValidateObjectFactory } from '@dereekb/model';
import { HttpException, ValidationPipe } from '@nestjs/common';
import { https } from 'firebase-functions';

// MARK: Transform Context
/**
* Context used for transforming content.
*/
export interface FirebaseServerActionsTransformContext {
readonly firebaseServerActionTransformFactory: TransformAndValidateObjectFactory;
readonly firebaseServerActionTransformFunctionFactory: TransformAndValidateFunctionResultFactory;
}

export function firebaseServerActionsTransformContext(): FirebaseServerActionsTransformContext {
const firebaseServerActionTransformFactory = firebaseServerActionsTransformFactory();
const firebaseServerActionTransformFunctionFactory = toTransformAndValidateFunctionResultFactory(firebaseServerActionTransformFactory);

return {
firebaseServerActionTransformFactory,
firebaseServerActionTransformFunctionFactory
};
}

export function firebaseServerActionsTransformFactory(): TransformAndValidateObjectFactory {
const nestValidationExceptionFactory = new ValidationPipe().createExceptionFactory();

return transformAndValidateObjectFactory({
handleValidationError: (validationError: ValidationError[]) => {
const nestError = nestValidationExceptionFactory(validationError);
const details = (nestError as HttpException).getResponse();
throw new https.HttpsError('invalid-argument', 'Parameters validation check failed.', details);
}
});
}

// MARK: Action Context
/**
* Context used for building FirebaseServerActions. It contains references to reusable factories.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/firebase-server/src/lib/nest/function/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './v1';
export * from './v2';
export * from './context';
export * from './transform';
export * from './call';
export * from './nest';
37 changes: 37 additions & 0 deletions packages/firebase-server/src/lib/nest/function/nest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { INestApplicationContext } from '@nestjs/common';
import { CallableRequest } from 'firebase-functions/lib/common/providers/https';
import { CallableContextWithAuthData } from '../../function/context';

export type NestApplicationContextRequest<R> = R & {
readonly nestApplication: INestApplicationContext;
};

export type NestRef<N> = {
readonly nest: N;
};

export type NestContextRequest<N, R> = R & {
readonly nest: N;
};

export function injectNestIntoRequest<N, R>(nest: N, request: R): NestContextRequest<N, R> {
return {
...request,
nest
};
}

export function injectNestApplicationContextIntoRequest<R>(nestContext: INestApplicationContext, request: R): NestApplicationContextRequest<R> {
return {
...request,
nestApplication: nestContext
};
}

// MARK: Types
export type NestContextCallableRequest<N, I> = NestContextRequest<N, CallableRequest<I>>;

/**
* NestContextCallableRequest that has valid auth attached to it.
*/
export type NestContextCallableRequestWithAuth<N, I> = CallableContextWithAuthData<NestContextCallableRequest<N, I>>;
34 changes: 0 additions & 34 deletions packages/firebase-server/src/lib/nest/function/transform.ts

This file was deleted.

30 changes: 14 additions & 16 deletions packages/firebase-server/src/lib/nest/function/v1/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ import * as functions from 'firebase-functions';
import { INestApplicationContext } from '@nestjs/common';
import { RunnableHttpFunction } from '../../../function/type';
import { MakeNestContext, NestApplicationFunctionFactory, NestApplicationPromiseGetter } from '../../nest.provider';
import { PromiseOrValue } from '@dereekb/util';
import { OnCallWithNestApplication, OnCallWithNestApplicationRequest, OnCallWithNestContext, setNestContextOnRequest } from '../call';

export function makeOnCallWithNestApplicationRequest<I>(nestApplication: INestApplicationContext, data: I, context: functions.https.CallableContext): OnCallWithNestApplicationRequest<I> {
return {
...context,
nestApplication,
data
};
}

// MARK: Nest
export type NestApplicationRunnableHttpFunctionFactory<I> = NestApplicationFunctionFactory<RunnableHttpFunction<I>>;

/**
* Runnable function that is passed an INestApplicationContext in addition to the usual data/context provided by firebase.
*/
export type OnCallWithNestApplication<I = unknown, O = unknown> = (nest: INestApplicationContext, data: I, context: functions.https.CallableContext) => PromiseOrValue<O>;

/**
* Factory function for generating a NestApplicationFunctionFactory for a HttpsFunctions/Runnable firebase function.
*/
export type OnCallWithNestApplicationFactory = <I, O>(fn: OnCallWithNestApplication<I, O>) => NestApplicationRunnableHttpFunctionFactory<I>;
export type OnCallWithNestApplicationFactory = <I = unknown, O = unknown>(fn: OnCallWithNestApplication<I, O>) => NestApplicationRunnableHttpFunctionFactory<I>;

/**
* Creates a factory for generating OnCallWithNestApplication functions.
Expand All @@ -25,19 +28,14 @@ export type OnCallWithNestApplicationFactory = <I, O>(fn: OnCallWithNestApplicat
*/
export function onCallWithNestApplicationFactory(): OnCallWithNestApplicationFactory {
return <I, O>(fn: OnCallWithNestApplication<I, O>) => {
return (nestAppPromiseGetter: NestApplicationPromiseGetter) => functions.https.onCall((data: I, context: functions.https.CallableContext) => nestAppPromiseGetter().then((x) => fn(x, data, context)));
return (nestAppPromiseGetter: NestApplicationPromiseGetter) => functions.https.onCall((data: I, context: functions.https.CallableContext) => nestAppPromiseGetter().then((x) => fn(makeOnCallWithNestApplicationRequest(x, data, context))));
};
}

/**
* Runnable function that is passed an arbitrary nest context object in addition to the usual data/context provided by firebase.
*/
export type OnCallWithNestContext<C, I = unknown, O = unknown> = (nestContext: C, data: I, context: functions.https.CallableContext) => PromiseOrValue<O>;

/**
* Factory function for generating HttpsFunctions/Runnable firebase function that returns the value from the input OnCallWithNestContext function.
*/
export type OnCallWithNestContextFactory<C> = <I, O>(fn: OnCallWithNestContext<C, I, O>) => NestApplicationRunnableHttpFunctionFactory<I>;
export type OnCallWithNestContextFactory<N> = <I = unknown, O = unknown>(fn: OnCallWithNestContext<N, I, O>) => NestApplicationRunnableHttpFunctionFactory<I>;

/**
* Creates a factory for generating OnCallWithNestContext functions with a nest context object that is generated by the input function.
Expand All @@ -46,6 +44,6 @@ export type OnCallWithNestContextFactory<C> = <I, O>(fn: OnCallWithNestContext<C
* @param makeNestContext
* @returns
*/
export function onCallWithNestContextFactory<C>(appFactory: OnCallWithNestApplicationFactory, makeNestContext: MakeNestContext<C>): OnCallWithNestContextFactory<C> {
return <I, O>(fn: OnCallWithNestContext<C, I, O>) => appFactory<I, O>((nest, data, context) => fn(makeNestContext(nest), data, context));
export function onCallWithNestContextFactory<N>(appFactory: OnCallWithNestApplicationFactory, makeNestContext: MakeNestContext<N>): OnCallWithNestContextFactory<N> {
return <I, O>(fn: OnCallWithNestContext<N, I, O>) => appFactory<I, O>((request) => fn(setNestContextOnRequest(makeNestContext, request)));
}
Loading

0 comments on commit 2ef4002

Please sign in to comment.