Skip to content

Commit

Permalink
feat: added firebaseServerDevFunctions()
Browse files Browse the repository at this point in the history
  • Loading branch information
dereekb committed Oct 4, 2022
1 parent 2114446 commit 375e3ac
Show file tree
Hide file tree
Showing 25 changed files with 458 additions and 16 deletions.
5 changes: 4 additions & 1 deletion apps/demo-api/src/app/api/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { Module } from '@nestjs/common';
import { DemoApiStripeModule } from './stripe/stripe.module';

@Module({
imports: [DemoApiStripeModule],
imports: [
// Stripe Module
DemoApiStripeModule
],
exports: []
})
export class DemoApiApiModule {}
16 changes: 5 additions & 11 deletions apps/demo-api/src/app/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { demoCreateModel, demoDeleteModel, demoUpdateModel } from './function/model/crud.functions';
import { profileSetUsernameKey } from '@dereekb/demo-firebase';
import { initEnvironmentForScheduleConfiguredFunctions, nestAppHasDevelopmentSchedulerEnabled, NestAppPromiseGetter, nestServerInstance } from '@dereekb/firebase-server';
import { NestAppPromiseGetter, nestServerInstance } from '@dereekb/firebase-server';
import { UPDATE_MODEL_APP_FUNCTION_KEY, DELETE_MODEL_APP_FUNCTION_KEY, CREATE_MODEL_APP_FUNCTION_KEY } from '@dereekb/firebase';
import { DemoApiAppModule } from './app.module';
import { profileSetUsername, initUserOnCreate } from './function';
Expand Down Expand Up @@ -40,14 +40,8 @@ export function allAppFunctions(nest: NestAppPromiseGetter) {
* @param nest
*/
export function allScheduledAppFunctions(nest: NestAppPromiseGetter) {
return initEnvironmentForScheduleConfiguredFunctions(
{
// Scheduled Functions
exampleSchedule: demoExampleUsageOfSchedule(nest)
},
{
// Only enable when the development scheduler is enabled
checkEnabled: nestAppHasDevelopmentSchedulerEnabled(nest)
}
);
return {
// Scheduled Functions
exampleSchedule: demoExampleUsageOfSchedule(nest)
};
}
12 changes: 12 additions & 0 deletions apps/demo-api/src/app/function/example/example.development.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { DemoDevelopmentExampleResult, DemoDevelopmentExampleParams } from '@dereekb/demo-firebase';
import { DemoDevelopmentFunction } from '../function';

export const exampleDevelopmentFunction: DemoDevelopmentFunction<DemoDevelopmentExampleParams, DemoDevelopmentExampleResult> = (request) => {
const { data } = request;

console.log(`exampleDevelopmentFunction() was called: ${data.message}`);

return {
message: data.message
};
};
9 changes: 8 additions & 1 deletion apps/demo-api/src/app/function/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
OnCallCreateModelMap,
onScheduleWithNestApplicationFactory,
onScheduleWithNestContextFactory,
OnScheduleWithNestContext
OnScheduleWithNestContext,
OnCallDevelopmentFunction,
OnCallDevelopmentFunctionMap
} from '@dereekb/firebase-server';
import { OnCallCreateModelResult } from '@dereekb/firebase';
import { ProfileServerActions, GuestbookServerActions, DemoApiAuthService, DemoFirebaseServerActionsContext } from '../common';
Expand Down Expand Up @@ -71,4 +73,9 @@ export type DemoOnCallUpdateModelMap = OnCallUpdateModelMap<DemoApiNestContext,
export type DemoDeleteModelFunction<I, O = void> = OnCallDeleteModelFunction<DemoApiNestContext, I, O>;
export type DemoOnCallDeleteModelMap = OnCallDeleteModelMap<DemoApiNestContext, DemoFirebaseModelTypes>;

// MARK: Schedule Functions
export type DemoScheduleFunction = OnScheduleWithNestContext<DemoApiNestContext>;

// MARK: Development Functions
export type DemoDevelopmentFunction<I = unknown, O = void> = OnCallDevelopmentFunction<DemoApiNestContext, I, O>;
export type DemoOnCallDevelopmentFunctionMap = OnCallDevelopmentFunctionMap<DemoApiNestContext>;
7 changes: 7 additions & 0 deletions apps/demo-api/src/app/function/model/development.functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { DEMO_APP_EXAMPLE_DEVELOPMENT_FUNCTION_KEY } from '@dereekb/demo-firebase';
import { exampleDevelopmentFunction } from '../example/example.development';
import { DemoOnCallDevelopmentFunctionMap } from '../function';

export const demoDevelopmentFunctionMap: DemoOnCallDevelopmentFunctionMap = {
[DEMO_APP_EXAMPLE_DEVELOPMENT_FUNCTION_KEY]: exampleDevelopmentFunction
};
2 changes: 2 additions & 0 deletions apps/demo-api/src/app/function/model/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './crud.functions';
export * from './schedule.functions';
export * from './development.functions';
15 changes: 13 additions & 2 deletions apps/demo-api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import 'reflect-metadata';
import { onRequest } from 'firebase-functions/v2/https';
import * as admin from 'firebase-admin';
import { allAppFunctions, allScheduledAppFunctions, initNestServer } from './app/app';
import { demoDevelopmentFunctionMap } from './app/function/model/development.functions';
import { firebaseServerDevFunctions } from '@dereekb/firebase-server';
import { onCallWithDemoNestContext } from './app/function/function';

const app = admin.initializeApp();

Expand All @@ -14,6 +17,14 @@ export const api = onRequest(server);
export const { initUserOnCreate, profileSetUsername, createModel, updateModel, deleteModel } = allAppFunctions(nest);

// Scheduled Functions
const scheduledFunctions = allScheduledAppFunctions(nest);
const allScheduledFunctions = allScheduledAppFunctions(nest);
export const { exampleSchedule } = allScheduledFunctions;

export const { exampleSchedule } = scheduledFunctions;
// Admin/Developer Functions
export const { dev } = firebaseServerDevFunctions({
enabled: true,
nest,
developerFunctionsMap: demoDevelopmentFunctionMap,
onCallFactory: onCallWithDemoNestContext,
allScheduledFunctions
});
32 changes: 32 additions & 0 deletions components/demo-firebase/src/lib/development/development.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DevelopmentFirebaseFunctionConfigMap, developmentFirebaseFunctionMapFactory } from '@dereekb/firebase';
import { Expose } from 'class-transformer';
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';

export const DEMO_DEVELOPMENT_EXAMPLE_MAX_MESSAGE_LENGTH = 200;

export class DemoDevelopmentExampleParams {
@Expose()
@IsNotEmpty()
@IsString()
@MaxLength(DEMO_DEVELOPMENT_EXAMPLE_MAX_MESSAGE_LENGTH)
message!: string;
}

export class DemoDevelopmentExampleResult {
message!: string;
}

export const DEMO_APP_EXAMPLE_DEVELOPMENT_FUNCTION_KEY = 'example';

export type DemoDevelopmentFunctionTypeMap = {
[DEMO_APP_EXAMPLE_DEVELOPMENT_FUNCTION_KEY]: [DemoDevelopmentExampleParams, DemoDevelopmentExampleResult];
};

export const demoDevelopmentFunctionsConfig: DevelopmentFirebaseFunctionConfigMap<DemoDevelopmentFunctionTypeMap> = {
[DEMO_APP_EXAMPLE_DEVELOPMENT_FUNCTION_KEY]: null
};

/**
* Used to generate our ProfileFunctionMap for a Functions instance.
*/
export const demoDevelopmentFunctionMap = developmentFirebaseFunctionMapFactory<DemoDevelopmentFunctionTypeMap>(demoDevelopmentFunctionsConfig);
1 change: 1 addition & 0 deletions components/demo-firebase/src/lib/development/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './development.api';
1 change: 1 addition & 0 deletions components/demo-firebase/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './auth';
export * from './development';
export * from './model';
export * from './functions';
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { inAuthContext } from '@dereekb/firebase-server';
import { OnCallDevelopmentParams, SCHEDULED_FUNCTION_DEV_FUNCTION_SPECIFIER } from '@dereekb/firebase';
import { CallableHttpFunction, RunnableHttpFunction } from '../../function/type';
import { NestAppPromiseGetter } from '../app';
import { NestApplicationScheduleConfiguredFunctionMap } from '../function/schedule.util';
import { OnCallWithNestContextFactory } from '../function/v1/call';
import { OnCallHandlerWithNestContextFactory } from '../function/v2/call';
import { AbstractFirebaseNestContext } from '../nest.provider';
import { onCallDevelopmentFunction, OnCallDevelopmentFunctionMap } from './development.function';
import { makeScheduledFunctionDevelopmentFunction } from './development.schedule.function';
import { unavailableError } from '../../function/error';

export interface FirebaseServerDevFunctionsConfig<N extends AbstractFirebaseNestContext<any, any>, S extends NestApplicationScheduleConfiguredFunctionMap> {
readonly enabled: boolean;
/**
* Whether or not to require an auth context when calling dev functions. True by default.
*/
readonly secure?: boolean;
readonly nest: NestAppPromiseGetter;
readonly developerFunctionsMap: OnCallDevelopmentFunctionMap<N>;
readonly onCallFactory: OnCallWithNestContextFactory<N> | OnCallHandlerWithNestContextFactory<N>;
readonly disableDevelopmentScheduleFunction?: boolean;
readonly allScheduledFunctions?: S;
}

export interface FirebaseServerDevFunctions {
readonly dev: RunnableHttpFunction<OnCallDevelopmentParams> | CallableHttpFunction<OnCallDevelopmentParams>;
}

export function firebaseServerDevFunctions<N extends AbstractFirebaseNestContext<any, any>, S extends NestApplicationScheduleConfiguredFunctionMap>(config: FirebaseServerDevFunctionsConfig<N, S>): FirebaseServerDevFunctions {
const { enabled, secure, nest, developerFunctionsMap, onCallFactory, allScheduledFunctions, disableDevelopmentScheduleFunction } = config;

let dev: RunnableHttpFunction<OnCallDevelopmentParams> | CallableHttpFunction<OnCallDevelopmentParams>;

if (enabled) {
const fullFunctionsMap: OnCallDevelopmentFunctionMap<N> = {
...developerFunctionsMap
};

if (allScheduledFunctions && disableDevelopmentScheduleFunction != null) {
fullFunctionsMap[SCHEDULED_FUNCTION_DEV_FUNCTION_SPECIFIER] = makeScheduledFunctionDevelopmentFunction({
allScheduledFunctions
});
}

let onCallFunction = onCallDevelopmentFunction(fullFunctionsMap);

if (secure != false) {
onCallFunction = inAuthContext(onCallFunction);
}

dev = onCallFactory(onCallFunction)(nest);
} else {
dev = onCallFactory(async (x) => {
throw unavailableError({
message: 'developer tools service is not enabled.'
});
})(nest);
}

return {
dev
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NestContextCallableRequest, NestContextCallableRequestWithAuth } from '../function/nest';

export interface AssertDevelopmentRequestFunctionContext<N, I = unknown> {
request: NestContextCallableRequest<N, I>;
specifier: string;
}

/**
* Function that asserts something given the input request.
*/
export type AssertDevelopmentRequestFunction<N, I = unknown> = (context: AssertDevelopmentRequestFunctionContext<N, I>) => void;
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { PromiseOrValue, serverError } from '@dereekb/util';
import { FirestoreModelIdentity, FirestoreModelTypes, OnCallDevelopmentParams, DevelopmentFirebaseFunctionSpecifierRef, DevelopmentFirebaseFunctionSpecifier } from '@dereekb/firebase';
import { badRequestError } from '../../function';
import { OnCallWithAuthorizedNestContext, OnCallWithNestContext } from '../function/call';
import { NestContextCallableRequest } from '../function/nest';
import { AssertDevelopmentRequestFunction } from './development.assert.function';

// MARK: Function
export type OnCallDevelopmentFunction<N, I = unknown, O = unknown> = (request: NestContextCallableRequest<N, I> & DevelopmentFirebaseFunctionSpecifierRef) => PromiseOrValue<O>;

export type OnCallDevelopmentFunctionMap<N, T extends FirestoreModelIdentity = FirestoreModelIdentity> = {
[K in FirestoreModelTypes<T>]?: OnCallDevelopmentFunction<N, any, any>;
};

export interface OnCallDevelopmentConfig<N> {
preAssert?: AssertDevelopmentRequestFunction<N, OnCallDevelopmentParams>;
}

/**
* Creates a OnCallWithAuthorizedNestContext function for creating a model.
*
* @param map
* @returns
*/
export function onCallDevelopmentFunction<N>(map: OnCallDevelopmentFunctionMap<N>, config: OnCallDevelopmentConfig<N> = {}): OnCallWithNestContext<N, OnCallDevelopmentParams, unknown> {
const { preAssert = () => undefined } = config;

return (request) => {
const specifier = request.data.specifier;
const devFn = map[specifier];

if (devFn) {
preAssert({ request, specifier });
return devFn({
...request,
specifier,
data: request.data.data
});
} else {
throw developmentUnknownSpecifierError(specifier);
}
};
}

export function developmentUnknownSpecifierError(specifier: DevelopmentFirebaseFunctionSpecifier) {
return badRequestError(
serverError({
status: 400,
code: 'UNKNOWN_SPECIFIER_ERROR',
message: `Invalid specifier "${specifier}" to run.`,
data: {
specifier
}
})
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { badRequestError } from '../../function/error';

export const NO_RUN_NAME_SPECIFIED_FOR_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_CODE = 'NO_RUN_NAME_SPECIFIED_FOR_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION';

export function noRunNameSpecifiedForScheduledFunctionDevelopmentFunction(type: unknown) {
return badRequestError({
code: NO_RUN_NAME_SPECIFIED_FOR_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_CODE,
message: `Must specify run parameter.`
});
}

export const UNKNOWN_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_NAME_CODE = 'UNKNOWN_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_NAME';

export function unknownScheduledFunctionDevelopmentFunctionName(name: unknown) {
return badRequestError({
code: UNKNOWN_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_NAME_CODE,
message: `Unknown function with name "${name}"`,
data: {
name
}
});
}

export const UNKNOWN_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_TYPE_CODE = 'UNKNOWN_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_TYPE';

export function unknownScheduledFunctionDevelopmentFunctionType(type: unknown) {
return badRequestError({
code: UNKNOWN_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_TYPE_CODE,
message: `Unknown type "${type}"`,
data: {
type
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ScheduledFunctionDevelopmentFirebaseFunctionListEntry, ScheduledFunctionDevelopmentFirebaseFunctionParams, ScheduledFunctionDevelopmentFirebaseFunctionResult } from '@dereekb/firebase';
import { forEachKeyValue, cachedGetter } from '@dereekb/util';
import { NestApplicationScheduleConfiguredFunctionMap } from '../function/schedule.util';
import { OnCallDevelopmentFunction } from './development.function';
import { noRunNameSpecifiedForScheduledFunctionDevelopmentFunction, unknownScheduledFunctionDevelopmentFunctionName, unknownScheduledFunctionDevelopmentFunctionType } from './development.schedule.function.error';

export interface MakeScheduledFunctionDevelopmentFunctionConfig {
readonly allScheduledFunctions: NestApplicationScheduleConfiguredFunctionMap;
}

export function makeScheduledFunctionDevelopmentFunction(config: MakeScheduledFunctionDevelopmentFunctionConfig): OnCallDevelopmentFunction<unknown, ScheduledFunctionDevelopmentFirebaseFunctionParams, ScheduledFunctionDevelopmentFirebaseFunctionResult> {
const { allScheduledFunctions } = config;
const getListValues = cachedGetter(() => {
const result: ScheduledFunctionDevelopmentFirebaseFunctionListEntry[] = [];

forEachKeyValue(allScheduledFunctions, {
forEach: (x) => {
const [functionName, config] = x;

result.push({
name: functionName.toString()
});
}
});

return result;
});

return async (request) => {
const { data } = request;
const { type } = data;

switch (type) {
case 'run':
const targetRunName = data.run;

if (!targetRunName) {
throw noRunNameSpecifiedForScheduledFunctionDevelopmentFunction(type);
}

const targetFunction = allScheduledFunctions[targetRunName];

if (!targetFunction) {
throw unknownScheduledFunctionDevelopmentFunctionName(targetRunName);
}

try {
await targetFunction._runNow();
} catch (e) {
console.error(`Failed manually running task "${targetRunName}".`, e);
throw e;
}

return {
type: 'run',
success: true
};
case 'list':
return {
type: 'list',
list: getListValues()
};
default:
throw unknownScheduledFunctionDevelopmentFunctionType(type);
}
};
}
5 changes: 5 additions & 0 deletions packages/firebase-server/src/lib/nest/development/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './development.app.function';
export * from './development.assert.function';
export * from './development.function';
export * from './development.schedule.function';
export * from './development.schedule.function.error';
Loading

0 comments on commit 375e3ac

Please sign in to comment.