Skip to content

Commit

Permalink
feat: added FirebaseServerStorageService
Browse files Browse the repository at this point in the history
- added firebase-server nestjs service/module related to the storage services
- updated demo and template to provide/export a storage moduleÏ
  • Loading branch information
dereekb committed Jul 4, 2022
1 parent 655088b commit 38bf98a
Show file tree
Hide file tree
Showing 21 changed files with 247 additions and 34 deletions.
3 changes: 2 additions & 1 deletion apps/demo-api/src/app/common/firebase/firebase.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Module } from '@nestjs/common';
import { DemoApiFirestoreModule } from './firestore.module';
import { DemoApiActionModule } from './action.module';
import { DemoApiAuthModule } from './auth.module';
import { DemoApiStorageModule } from './storage.module';

@Module({
imports: [DemoApiFirestoreModule, DemoApiActionModule, DemoApiAuthModule]
imports: [DemoApiFirestoreModule, DemoApiActionModule, DemoApiAuthModule, DemoApiStorageModule]
})
export class DemoApiFirebaseModule {}
1 change: 1 addition & 0 deletions apps/demo-api/src/app/common/firebase/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './auth.module';
export * from './auth.service';
export * from './firebase.module';
export * from './firestore.module';
export * from './storage.module';
5 changes: 5 additions & 0 deletions apps/demo-api/src/app/common/firebase/storage.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { firebaseServerStorageModuleMetadata } from '@dereekb/firebase-server';
import { Module } from '@nestjs/common';

@Module(firebaseServerStorageModuleMetadata())
export class DemoApiStorageModule {}
6 changes: 3 additions & 3 deletions packages/firebase-server/src/lib/auth/auth.nest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class TestAuthAppModule {}
/**
* Test context factory that will automatically instantiate TestAppModule for each test, and make it available.
*/
const firebaseAdminFunctionNestContext = firebaseAdminFunctionNestContextFactory({
const firebaseAuthAdminFunctionNestContext = firebaseAdminFunctionNestContextFactory({
nestModules: TestAuthAppModule,
injectFirebaseServerAppTokenProvider: true
});
Expand All @@ -71,10 +71,10 @@ type LoadClaimsTest = {
second?: number;
};

describe('firebase server auth', () => {
describe('firebase server nest auth', () => {
initFirebaseServerAdminTestEnvironment();

firebaseAdminFunctionNestContext((f) => {
firebaseAuthAdminFunctionNestContext((f) => {
let authService: TestAuthService;

beforeEach(() => {
Expand Down
8 changes: 4 additions & 4 deletions packages/firebase-server/src/lib/auth/auth.nest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ export const FIREBASE_AUTH_TOKEN: InjectionToken = 'FIREBASE_AUTH_TOKEN';
})
export class FirebaseServerAuthModule {}

// MARK: AppAuthCollections
export type ProvideFirestoreServerAuthServiceSimple<T extends FirebaseServerAuthService> = Pick<FactoryProvider<T>, 'provide'> & {
// MARK: AppAuth
export type ProvideFirebaseServerAuthServiceSimple<T extends FirebaseServerAuthService> = Pick<FactoryProvider<T>, 'provide'> & {
useFactory: (auth: admin.auth.Auth) => T;
};

export type ProvideFirebaseServerAuthService<T extends FirebaseServerAuthService> = FactoryProvider<T> | ProvideFirestoreServerAuthServiceSimple<T>;
export type ProvideFirebaseServerAuthService<T extends FirebaseServerAuthService> = FactoryProvider<T> | ProvideFirebaseServerAuthServiceSimple<T>;

export function provideFirebaseServerAuthService<T extends FirebaseServerAuthService>(provider: ProvideFirebaseServerAuthService<T>): [ProvideFirebaseServerAuthService<T>, Provider<T>] {
return [
Expand All @@ -45,7 +45,7 @@ export function provideFirebaseServerAuthService<T extends FirebaseServerAuthSer
];
}

// MARK: app firestore module
// MARK: app firebase auth module
export interface FirebaseServerAuthModuleMetadataConfig<T extends FirebaseServerAuthService> extends AdditionalModuleMetadata {
readonly serviceProvider: ProvideFirebaseServerAuthService<T>;
}
Expand Down
29 changes: 25 additions & 4 deletions packages/firebase-server/src/lib/nest/app.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { ClassType, Getter, asGetter, makeGetter, mergeArrayOrValueIntoArray } from '@dereekb/util';
import { DynamicModule, INestApplication, INestApplicationContext, NestApplicationOptions, Provider, Type } from '@nestjs/common';
import { DynamicModule, FactoryProvider, INestApplication, INestApplicationContext, NestApplicationOptions, Provider, Type } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import * as express from 'express';
import { firebaseServerAppTokenProvider } from '../firebase/firebase.nest';
import * as admin from 'firebase-admin';
import { ConfigureFirebaseWebhookMiddlewareModule } from './middleware/webhook';
import { ConfigureFirebaseAppCheckMiddlewareModule } from './middleware/appcheck.module';
import { StorageBucketId } from '@dereekb/firebase';
import { firebaseServerStorageDefaultBucketIdTokenProvider } from '../storage/storage.nest';

export interface NestServer {
server: express.Express;
Expand Down Expand Up @@ -50,6 +52,14 @@ export interface NestServerInstanceConfig<T> {
* Whether or not to configure webhook usage. This will configure routes to use
*/
readonly configureWebhooks?: boolean;
/**
* Default storage bucket to use. If provided, overrides what the app uses in the default FirebaseServerStorageContextModule and default FirebaseStorageContext.
*/
readonly defaultStorageBucket?: StorageBucketId;
/**
* Whether or not to force using the default storage bucket.
*/
readonly forceStorageBucket?: boolean;
/**
* Whether or not to verify with app check. Is true by default.
*/
Expand All @@ -61,23 +71,25 @@ export interface NestServerInstanceConfig<T> {
}

export function nestServerInstance<T>(config: NestServerInstanceConfig<T>): NestServerInstance<T> {
const { moduleClass, providers: additionalProviders } = config;
const { moduleClass, providers: additionalProviders, defaultStorageBucket: inputDefaultStorageBucket, forceStorageBucket } = config;
const serversCache = new Map<string, NestServer>();

const initNestServer = (firebaseApp: admin.app.App): NestServer => {
const appName = firebaseApp.name;
const defaultStorageBucket = inputDefaultStorageBucket ?? firebaseApp.options.storageBucket;

let nestServer = serversCache.get(appName);

if (!nestServer) {
const server = express();
const createNestServer = async (expressInstance: express.Express) => {
const providers = [firebaseServerAppTokenProvider(asGetter(firebaseApp))];
const providers: (Provider | FactoryProvider)[] = [firebaseServerAppTokenProvider(asGetter(firebaseApp))];

if (additionalProviders) {
mergeArrayOrValueIntoArray(providers, additionalProviders);
}

const imports: Type<unknown>[] = [moduleClass];
const imports: (Type<unknown> | DynamicModule)[] = [moduleClass];

// NOTE: https://cloud.google.com/functions/docs/writing/http#parsing_http_requests
const options: NestApplicationOptions = { bodyParser: false }; // firebase already parses the requests
Expand All @@ -90,6 +102,15 @@ export function nestServerInstance<T>(config: NestServerInstanceConfig<T>): Nest
imports.push(ConfigureFirebaseAppCheckMiddlewareModule);
}

if (defaultStorageBucket) {
providers.push(
firebaseServerStorageDefaultBucketIdTokenProvider({
defaultBucketId: defaultStorageBucket,
forceBucket: forceStorageBucket
})
);
}

const providersModule: DynamicModule = {
module: FirebaseNestServerRootModule,
imports,
Expand Down
7 changes: 6 additions & 1 deletion packages/firebase-server/src/lib/nest/nest.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { build, BuildFunction, Getter } from '@dereekb/util';
import { INestApplicationContext } from '@nestjs/common';
import { AuthDataRef } from '../auth';
import { FirebaseServerAuthService, FirebaseServerAuthServiceRef } from '../auth/auth.service';
import { FirebaseServerStorageService, FirebaseServerStorageServiceRef } from '../storage';
import { FirebaseServerActionsContext } from './function/context';
import { nestFirebaseForbiddenPermissionError } from './model/permission.error';

Expand Down Expand Up @@ -30,12 +31,16 @@ export abstract class AbstractNestContext {
constructor(readonly nest: INestApplicationContext) {}
}

export abstract class AbstractFirebaseNestContext<A, Y extends FirebaseModelsService<any, FirebaseAppModelContext<A>>> extends AbstractNestContext implements FirebaseServerAuthServiceRef {
export abstract class AbstractFirebaseNestContext<A, Y extends FirebaseModelsService<any, FirebaseAppModelContext<A>>> extends AbstractNestContext implements FirebaseServerAuthServiceRef, FirebaseServerStorageServiceRef {
abstract get actionContext(): FirebaseServerActionsContext;
abstract get authService(): FirebaseServerAuthService;
abstract get firebaseModelsService(): Y;
abstract get app(): A;

get storageService(): FirebaseServerStorageService {
return this.nest.get(FirebaseServerStorageService);
}

/**
* Creates a FirebaseAppModelContext instance.
*
Expand Down
1 change: 1 addition & 0 deletions packages/firebase-server/src/lib/storage/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './driver';
export * from './driver.accessor';
export * from './storage.nest';
export * from './storage.service';
export * from './storage';
46 changes: 46 additions & 0 deletions packages/firebase-server/src/lib/storage/storage.nest.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Module } from '@nestjs/common';
import { firebaseAdminFunctionNestContextFactory, initFirebaseServerAdminTestEnvironment } from '@dereekb/firebase-server/test';
import { firebaseServerStorageModuleMetadata } from './storage.nest';
import { FirebaseServerStorageService } from './storage.service';
import { FirebaseStorageContext } from '@dereekb/firebase';

class TestFirebaseServerStorageService extends FirebaseServerStorageService {}

@Module(
firebaseServerStorageModuleMetadata({
serviceProvider: {
provide: TestFirebaseServerStorageService,
useFactory: (x: FirebaseStorageContext) => new TestFirebaseServerStorageService(x)
}
})
)
class TestStorageAppModule {}

/**
* Test context factory that will automatically instantiate TestAppModule for each test, and make it available.
*/
const firebaseStorageAdminFunctionNestContext = firebaseAdminFunctionNestContextFactory({
nestModules: TestStorageAppModule,
injectFirebaseServerAppTokenProvider: true
});

describe('firebase nest storage', () => {
initFirebaseServerAdminTestEnvironment();

firebaseStorageAdminFunctionNestContext((f) => {
let storageService: TestFirebaseServerStorageService;

beforeEach(() => {
storageService = f.get(TestFirebaseServerStorageService);
});

describe('FirebaseServerStorageService', () => {
describe('file', () => {
it('should create a file', () => {
const file = storageService.file('hello.txt');
expect(file).toBeDefined();
});
});
});
});
});
78 changes: 76 additions & 2 deletions packages/firebase-server/src/lib/storage/storage.nest.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as admin from 'firebase-admin';
import { InjectionToken, Module, ModuleMetadata, Provider } from '@nestjs/common';
import { FactoryProvider, InjectionToken, Module, ModuleMetadata, Provider } from '@nestjs/common';
import { ClassLikeType } from '@dereekb/util';
import { FIREBASE_APP_TOKEN } from '../firebase/firebase.nest';
import { googleCloudFirebaseStorageContextFactory } from './storage';
import { FirebaseServerStorageService } from './storage.service';
import { AdditionalModuleMetadata, mergeModuleMetadata } from '@dereekb/nestjs';
import { FirebaseStorageContext, FirebaseStorageContextFactoryConfig, StorageBucketId } from '@dereekb/firebase';

// MARK: Tokens
/**
Expand All @@ -15,6 +18,11 @@ export const FIREBASE_STORAGE_TOKEN: InjectionToken = 'FIREBASE_STORAGE_TOKEN';
*/
export const FIREBASE_STORAGE_CONTEXT_TOKEN: InjectionToken = 'FIREBASE_STORAGE_CONTEXT_TOKEN';

/**
* Token to the default bucket id string
*/
export const FIREBASE_STORAGE_CONTEXT_FACTORY_CONFIG_TOKEN: InjectionToken = 'FIREBASE_STORAGE_CONTEXT_FACTORY_CONFIG_TOKEN';

/**
* Nest provider module for Firebase that provides a firestore, etc. from the firestore token.
*/
Expand All @@ -39,9 +47,75 @@ export class FirebaseServerStorageModule {}
{
provide: FIREBASE_STORAGE_CONTEXT_TOKEN,
useFactory: googleCloudFirebaseStorageContextFactory,
inject: [FIREBASE_STORAGE_TOKEN]
inject: [FIREBASE_STORAGE_TOKEN, FIREBASE_STORAGE_CONTEXT_FACTORY_CONFIG_TOKEN]
}
],
exports: [FirebaseServerStorageModule, FIREBASE_STORAGE_CONTEXT_TOKEN]
})
export class FirebaseServerStorageContextModule {}

// MARK: Token Configuration
export function firebaseServerStorageDefaultBucketIdTokenProvider(input: StorageBucketId | FirebaseStorageContextFactoryConfig): Provider {
const config = typeof input === 'string' ? { defaultBucketId: input } : input;

if (!config.defaultBucketId) {
throw new Error('Non-empty defaultBucketId is required.');
}

return {
provide: FIREBASE_STORAGE_CONTEXT_FACTORY_CONFIG_TOKEN,
useValue: config
};
}

// MARK: AppAuth
export type ProvideFirebaseServerStorageServiceSimple<T extends FirebaseServerStorageService> = Pick<FactoryProvider<T>, 'provide'> & {
useFactory: (context: FirebaseStorageContext) => T;
};

export function defaultProvideFirebaseServerStorageServiceSimple<T extends FirebaseServerStorageService = FirebaseServerStorageService>(): ProvideFirebaseServerStorageServiceSimple<FirebaseServerStorageService> {
return {
provide: FirebaseServerStorageService,
useFactory: (context: FirebaseStorageContext) => new FirebaseServerStorageService(context)
} as ProvideFirebaseServerStorageServiceSimple<FirebaseServerStorageService> as ProvideFirebaseServerStorageService<T>;
}

export type ProvideFirebaseServerStorageService<T extends FirebaseServerStorageService> = FactoryProvider<T> | ProvideFirebaseServerStorageServiceSimple<T>;

export function provideFirebaseServerStorageService<T extends FirebaseServerStorageService = FirebaseServerStorageService>(provider: ProvideFirebaseServerStorageService<T>): [ProvideFirebaseServerStorageService<T>, Provider<T>] {
return [
{
...provider,
inject: (provider as FactoryProvider<T>).inject ?? [FIREBASE_STORAGE_CONTEXT_TOKEN]
},
{
provide: FirebaseServerStorageService,
useExisting: provider.provide
}
];
}

// MARK: app firebase auth module
export interface FirebaseServerStorageModuleMetadataConfig<T extends FirebaseServerStorageService = FirebaseServerStorageService> extends AdditionalModuleMetadata {
readonly serviceProvider?: ProvideFirebaseServerStorageService<T>;
}

/**
* Convenience function used to generate ModuleMetadata for an app's Auth related modules and FirebaseServerStorageService provider.
*
* @param provide
* @param useFactory
* @returns
*/
export function firebaseServerStorageModuleMetadata<T extends FirebaseServerStorageService = FirebaseServerStorageService>(config?: FirebaseServerStorageModuleMetadataConfig<T>): ModuleMetadata {
const serviceProvider = config && config.serviceProvider ? config.serviceProvider : defaultProvideFirebaseServerStorageServiceSimple<T>();

return mergeModuleMetadata(
{
imports: [FirebaseServerStorageContextModule],
exports: [FirebaseServerStorageContextModule, serviceProvider.provide],
providers: provideFirebaseServerStorageService(serviceProvider)
},
config
);
}
28 changes: 28 additions & 0 deletions packages/firebase-server/src/lib/storage/storage.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FirebaseStorageAccessor, FirebaseStorageAccessorFile, FirebaseStorageAccessorFolder, FirebaseStorageContext, StorageBucketId, StoragePathInput } from '@dereekb/firebase';

// MARK: Service
/**
* Reference to a FirebaseServerStorageService
*/
export interface FirebaseServerStorageServiceRef<S extends FirebaseServerStorageService = FirebaseServerStorageService> {
readonly storageService: S;
}

/**
* Basic service that implements FirebaseStorageAccessor and provides a FirebaseStorageContext.
*/
export class FirebaseServerStorageService implements FirebaseStorageAccessor {
constructor(readonly storageContext: FirebaseStorageContext) {}

defaultBucket() {
return this.storageContext.defaultBucket();
}

file(path: StoragePathInput): FirebaseStorageAccessorFile {
return this.storageContext.file(path);
}

folder(path: StoragePathInput): FirebaseStorageAccessorFolder {
return this.storageContext.folder(path);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ export const firebaseAdminFunctionTestBuilder = jestTestContextBuilder<FirebaseA
}

const projectId = getGCloudTestProjectId();
const app = admin.initializeApp({ projectId });
const storageBucket = 'b-' + projectId;
const app = admin.initializeApp({ projectId, storageBucket });

return new FirebaseAdminFunctionTestContextInstance(firebaseFunctionsTestInstance!, app);
},
Expand Down
Loading

0 comments on commit 38bf98a

Please sign in to comment.