diff --git a/apps/demo-api/src/app/function/auth/auth.function.ts b/apps/demo-api/src/app/function/auth/auth.function.ts index 872036a25..c3242ed25 100644 --- a/apps/demo-api/src/app/function/auth/auth.function.ts +++ b/apps/demo-api/src/app/function/auth/auth.function.ts @@ -9,8 +9,6 @@ export const initUserOnCreate = onEventWithDemoNestContext((withNest functions.auth.user().onCreate(withNest(async (nest, data: UserRecord, context) => { const uid = data.uid; - console.log('Init user: ', uid, context); - if (uid) { await nest.profileActions.initProfileForUid(uid); } diff --git a/apps/demo-firebase/src/lib/functions.ts b/apps/demo-firebase/src/lib/functions.ts new file mode 100644 index 000000000..cec5a7f06 --- /dev/null +++ b/apps/demo-firebase/src/lib/functions.ts @@ -0,0 +1,38 @@ +import { ProfileFunctionTypeMap } from './profile/profile.api'; +import { FirebaseFunctionGetter, FirebaseFunctionsConfigMap, lazyFirebaseFunctionsFactory } from '@dereekb/firebase'; +import { Functions } from 'firebase/functions'; +import { guestbookFunctionMap, GuestbookFunctions, GuestbookFunctionTypeMap } from './guestbook'; +import { profileFunctionMap, ProfileFunctions } from './profile'; + +/** + * FirebaseFunctionsMap type for Demo + */ +export type DemoFirebaseFunctionsMap = { + guestbookFunctions: GuestbookFunctionTypeMap; + profileFunctions: ProfileFunctionTypeMap; +} + +/** + * LazyFirebaseFunctionsConfig for the DemoFirebaseFunctionsMap. + * + * The typings are enforced by the functions map. + */ +export const DEMO_FIREBASE_FUNCTIONS_CONFIG: FirebaseFunctionsConfigMap = { + guestbookFunctions: [GuestbookFunctions, guestbookFunctionMap], + profileFunctions: [ProfileFunctions, profileFunctionMap] +}; + +/** + * The LazyFirebaseFunctions result type. It is an abstract class to allow for dependency injection. + * + * The typings are enforced by the functions map. + */ +export abstract class DemoFirebaseFunctionsGetter { + abstract readonly guestbookFunctions: FirebaseFunctionGetter; + abstract readonly profileFunctions: FirebaseFunctionGetter; +} + +export function makeDemoFirebaseFunctions(functions: Functions): DemoFirebaseFunctionsGetter { + const factory = lazyFirebaseFunctionsFactory(DEMO_FIREBASE_FUNCTIONS_CONFIG); + return factory(functions); +} diff --git a/apps/demo-firebase/src/lib/guestbook/guestbook.api.ts b/apps/demo-firebase/src/lib/guestbook/guestbook.api.ts new file mode 100644 index 000000000..204d186ab --- /dev/null +++ b/apps/demo-firebase/src/lib/guestbook/guestbook.api.ts @@ -0,0 +1,53 @@ +import { GuestbookEntry } from './guestbook'; +import { Expose } from "class-transformer"; +import { FirebaseFunctionMap, firebaseFunctionMapFactory, FirebaseFunctionMapFunction, FirebaseFunctionTypeConfigMap } from "@dereekb/firebase"; +import { IsNotEmpty, IsString, MaxLength } from "class-validator"; +import { ModelKey } from '@dereekb/util'; + +export const GUESTBOOK_ENTRY_MESSAGE_MAX_LENGTH = 200; +export const GUESTBOOK_ENTRY_SIGNED_MAX_LENGTH = 40; + +export abstract class GuestbookEntryParams { + + @Expose() + @IsNotEmpty() + @IsString() + guestbook!: ModelKey; + +} + +export class UpdateGuestbookEntryParams extends GuestbookEntryParams { + + @Expose() + @IsNotEmpty() + @IsString() + @MaxLength(GUESTBOOK_ENTRY_MESSAGE_MAX_LENGTH) + message!: string; + + @Expose() + @IsNotEmpty() + @IsString() + @MaxLength(GUESTBOOK_ENTRY_SIGNED_MAX_LENGTH) + signed!: string; + +} + +export const guestbookEntryUpdateKey = 'guestbookEntryUpdateEntry'; +export const guestbookEntryDeleteKey = 'guestbookEntryDeleteEntry'; + +export type GuestbookFunctionTypeMap = { + [guestbookEntryUpdateKey]: [UpdateGuestbookEntryParams, GuestbookEntry] + [guestbookEntryDeleteKey]: [GuestbookEntryParams, GuestbookEntry] +} + +export const guestbookFunctionTypeConfigMap: FirebaseFunctionTypeConfigMap = { + [guestbookEntryUpdateKey]: null, + [guestbookEntryDeleteKey]: null +} + +export abstract class GuestbookFunctions implements FirebaseFunctionMap { + [guestbookEntryUpdateKey]: FirebaseFunctionMapFunction; + [guestbookEntryDeleteKey]: FirebaseFunctionMapFunction; +} + +export const guestbookFunctionMap = firebaseFunctionMapFactory(guestbookFunctionTypeConfigMap); diff --git a/apps/demo-firebase/src/lib/guestbook/guestbook.ts b/apps/demo-firebase/src/lib/guestbook/guestbook.ts index 10308fa5b..3c93cbc0e 100644 --- a/apps/demo-firebase/src/lib/guestbook/guestbook.ts +++ b/apps/demo-firebase/src/lib/guestbook/guestbook.ts @@ -61,10 +61,6 @@ export function guestbookFirestoreCollection(firestoreContext: FirestoreContext) // MARK: Guestbook Entry export interface GuestbookEntry extends UserRelatedById { - /** - * Arbitrary word without spaces - */ - word: string; /** * Guestbook message. */ @@ -81,6 +77,10 @@ export interface GuestbookEntry extends UserRelatedById { * Date the entry was originally created at. */ createdAt: Date; + /** + * Whether or not the entry has been published. This cannot be changed one published. + */ + published: boolean; } export interface GuestbookEntryRef extends DocumentReferenceRef { } @@ -91,11 +91,11 @@ export const guestbookEntryCollectionPath = 'guestbookEntry'; export const guestbookEntryConverter = makeSnapshotConverterFunctions({ fields: { - word: firestoreString(), message: firestoreString(), signed: firestoreString(), updatedAt: firestoreDate(), - createdAt: firestoreDate({ saveDefaultAsNow: true }) + createdAt: firestoreDate({ saveDefaultAsNow: true }), + published: firestoreBoolean({ defaultBeforeSave: false }) } }); diff --git a/apps/demo-firebase/src/lib/guestbook/index.ts b/apps/demo-firebase/src/lib/guestbook/index.ts index 571b3b8d1..650159f28 100644 --- a/apps/demo-firebase/src/lib/guestbook/index.ts +++ b/apps/demo-firebase/src/lib/guestbook/index.ts @@ -1,2 +1,3 @@ export * from './guestbook'; export * from './guestbook.query'; +export * from './guestbook.api'; diff --git a/apps/demo-firebase/src/lib/index.ts b/apps/demo-firebase/src/lib/index.ts index 8a1db0c12..c5cd93604 100644 --- a/apps/demo-firebase/src/lib/index.ts +++ b/apps/demo-firebase/src/lib/index.ts @@ -1,3 +1,4 @@ export * from './guestbook'; export * from './profile'; export * from './collection'; +export * from './functions'; diff --git a/apps/demo-firebase/src/lib/profile/profile.api.ts b/apps/demo-firebase/src/lib/profile/profile.api.ts index 13e89a8d0..71a72ec4c 100644 --- a/apps/demo-firebase/src/lib/profile/profile.api.ts +++ b/apps/demo-firebase/src/lib/profile/profile.api.ts @@ -1,6 +1,6 @@ import { Profile } from './profile'; import { Expose } from "class-transformer"; -import { FirebaseFunctionMap, firebaseFunctionMapFactory, FirebaseFunctionTypeConfigMap } from "@dereekb/firebase"; +import { FirebaseFunctionMap, firebaseFunctionMapFactory, FirebaseFunctionMapFunction, FirebaseFunctionTypeConfigMap } from "@dereekb/firebase"; import { IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator"; export class SetProfileUsernameParams { @@ -44,9 +44,9 @@ export const profileFunctionTypeConfigMap: FirebaseFunctionTypeConfigMap { } +export abstract class ProfileFunctions implements FirebaseFunctionMap { + [profileSetUsernameKey]: FirebaseFunctionMapFunction; +} /** * Used to generate our ProfileFunctionMap for a Functions instance. diff --git a/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.entry.popup.component.html b/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.entry.popup.component.html new file mode 100644 index 000000000..50c798d67 --- /dev/null +++ b/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.entry.popup.component.html @@ -0,0 +1,4 @@ + +

This is a dialog.

+ +
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 new file mode 100644 index 000000000..89a24a853 --- /dev/null +++ b/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.entry.popup.component.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; +import { AbstractDialogDirective } from '@dereekb/dbx-web'; +import { MatDialog } from '@angular/material/dialog'; +import { HandleActionFunction } from '@dereekb/dbx-core'; +import { DemoGuestbookEntryFormValue } from '../../../../shared/modules/guestbook/component/guestbook.entry.form.component'; +import { GuestbookEntryDocumentStore } from './../../../../shared/modules/guestbook/store/guestbook.entry.document.store'; +import { of } from 'rxjs'; + +export interface DemoGuestbookEntryPopupComponentConfig { + guestbookEntryDocumentStore: GuestbookEntryDocumentStore; +} + +@Component({ + template: ` + +

Enter your message for the guest book.

+
+ +

+ +
+
+ ` +}) +export class DemoGuestbookEntryPopupComponent extends AbstractDialogDirective { + + get guestbookEntryDocumentStore(): GuestbookEntryDocumentStore { + return this.data.guestbookEntryDocumentStore; + } + + get exists$() { + return this.guestbookEntryDocumentStore.exists$; + } + + static openPopup(matDialog: MatDialog, config: DemoGuestbookEntryPopupComponentConfig) { + return matDialog.open(DemoGuestbookEntryPopupComponent, { + data: config + }); + } + + readonly handleFormAction: HandleActionFunction = (value: DemoGuestbookEntryFormValue) => { + console.log('save.'); + return of(false); + } + +} diff --git a/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.view.component.html b/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.view.component.html index 0f6c9d4f4..e3a95872a 100644 --- a/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.view.component.html +++ b/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.view.component.html @@ -1,13 +1,24 @@ - +

{{ name$ | async }}

+
+
+ +
+
+

You have not created an entry in this guest book.

+ +
+
+

- +
diff --git a/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.view.component.ts b/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.view.component.ts index 2ecdf0492..8e0529a42 100644 --- a/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.view.component.ts +++ b/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/container/guestbook.view.component.ts @@ -1,7 +1,10 @@ -import { Component, OnDestroy } from '@angular/core'; +import { Component, OnDestroy, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; import { loadingStateContext } from '@dereekb/rxjs'; import { map } from 'rxjs'; import { GuestbookDocumentStore } from '../../../../shared/modules/guestbook/store/guestbook.document.store'; +import { GuestbookEntryDocumentStore } from '../../../../shared/modules/guestbook/store/guestbook.entry.document.store'; +import { DemoGuestbookEntryPopupComponent } from './guestbook.entry.popup.component'; @Component({ selector: 'demo-guestbook-view', @@ -9,15 +12,24 @@ import { GuestbookDocumentStore } from '../../../../shared/modules/guestbook/sto }) export class DemoGuestbookViewComponent implements OnDestroy { + @ViewChild(GuestbookEntryDocumentStore) + readonly documentStore!: GuestbookEntryDocumentStore; + readonly context = loadingStateContext({ obs: this.guestbookStore.dataLoadingState$ }); readonly data$ = this.guestbookStore.data$; readonly name$ = this.data$.pipe(map(x => x?.name)); - constructor(readonly guestbookStore: GuestbookDocumentStore) { } + constructor(readonly guestbookStore: GuestbookDocumentStore, readonly matDialog: MatDialog) { } ngOnDestroy(): void { this.context.destroy(); } + openEntry() { + DemoGuestbookEntryPopupComponent.openPopup(this.matDialog, { + guestbookEntryDocumentStore: this.documentStore + }); + } + } diff --git a/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/guestbook.module.ts b/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/guestbook.module.ts index ae24c90b8..42b72d44a 100644 --- a/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/guestbook.module.ts +++ b/apps/demo/src/app/modules/demo/modules/app/modules/guestbook/guestbook.module.ts @@ -6,6 +6,7 @@ import { DemoGuestbookListPageRightComponent } from './container/list.right.comp import { DemoGuestbookListPageComponent } from './container/list.component'; import { DemoGuestbookLayoutComponent } from './container/layout.component'; import { DemoGuestbookViewComponent } from './container/guestbook.view.component'; +import { DemoGuestbookEntryPopupComponent } from './container/guestbook.entry.popup.component'; @NgModule({ imports: [ @@ -15,6 +16,7 @@ import { DemoGuestbookViewComponent } from './container/guestbook.view.component }) ], declarations: [ + DemoGuestbookEntryPopupComponent, DemoGuestbookViewComponent, DemoGuestbookLayoutComponent, DemoGuestbookListPageComponent, diff --git a/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/component/guestbook.entry.form.component.ts b/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/component/guestbook.entry.form.component.ts index e69de29bb..2d7306948 100644 --- a/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/component/guestbook.entry.form.component.ts +++ b/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/component/guestbook.entry.form.component.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; +import { ProvideFormlyContext, AbstractSyncFormlyFormDirective } from "@dereekb/dbx-form"; +import { GuestbookEntry } from "@dereekb/demo-firebase"; +import { FormlyFieldConfig } from "@ngx-formly/core"; +import { guestbookEntryFields } from "./guestbook.entry.form"; + +export interface DemoGuestbookEntryFormValue extends Pick { } + +@Component({ + template: ``, + selector: 'demo-guestbook-entry-form', + providers: [ProvideFormlyContext()] +}) +export class DemoGuestbookEntryFormComponent extends AbstractSyncFormlyFormDirective { + + readonly fields: FormlyFieldConfig[] = guestbookEntryFields(); + +} diff --git a/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/component/guestbook.entry.form.ts b/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/component/guestbook.entry.form.ts new file mode 100644 index 000000000..63efc7417 --- /dev/null +++ b/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/component/guestbook.entry.form.ts @@ -0,0 +1,17 @@ +import { textAreaField, textField } from "@dereekb/dbx-form"; +import { GUESTBOOK_ENTRY_MESSAGE_MAX_LENGTH, GUESTBOOK_ENTRY_SIGNED_MAX_LENGTH } from "@dereekb/demo-firebase"; + +export function guestbookEntryFields() { + return [ + guestbookEntryMessageField(), + guestbookEntrySignedField() + ]; +} + +export function guestbookEntryMessageField() { + return textAreaField({ key: 'message', label: 'Message', maxLength: GUESTBOOK_ENTRY_MESSAGE_MAX_LENGTH, required: true }); +} + +export function guestbookEntrySignedField() { + return textField({ key: 'signed', label: 'Signed', maxLength: GUESTBOOK_ENTRY_SIGNED_MAX_LENGTH, required: true }); +} diff --git a/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/guestbook.module.ts b/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/guestbook.module.ts index 225ad20f3..902872843 100644 --- a/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/guestbook.module.ts +++ b/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/guestbook.module.ts @@ -5,28 +5,36 @@ import { DemoGuestbookEntryListComponent, DemoGuestbookEntryListViewComponent, D import { DemoGuestbookCollectionStoreDirective } from './store/guestbook.collection.store.directive'; import { DemoGuestbookDocumentStoreDirective } from './store/guestbook.document.store.directive'; import { DemoGuestbookEntryCollectionStoreDirective } from './store/guestbook.entry.collection.store.directive'; +import { DemoGuestbookEntryDocumentStoreDirective } from './store/guestbook.entry.document.store.directive'; +import { DemoGuestbookEntryFormComponent } from './component/guestbook.entry.form.component'; @NgModule({ imports: [ AppSharedModule ], declarations: [ - DemoGuestbookCollectionStoreDirective, - DemoGuestbookDocumentStoreDirective, - DemoGuestbookEntryCollectionStoreDirective, + // component + DemoGuestbookEntryFormComponent, DemoGuestbookListComponent, DemoGuestbookListViewComponent, DemoGuestbookListViewItemComponent, DemoGuestbookEntryListComponent, DemoGuestbookEntryListViewComponent, - DemoGuestbookEntryListViewItemComponent + DemoGuestbookEntryListViewItemComponent, + // store + DemoGuestbookCollectionStoreDirective, + DemoGuestbookDocumentStoreDirective, + DemoGuestbookEntryCollectionStoreDirective, + DemoGuestbookEntryDocumentStoreDirective, ], exports: [ + DemoGuestbookEntryFormComponent, + DemoGuestbookListComponent, + DemoGuestbookEntryListComponent, DemoGuestbookCollectionStoreDirective, DemoGuestbookDocumentStoreDirective, DemoGuestbookEntryCollectionStoreDirective, - DemoGuestbookListComponent, - DemoGuestbookEntryListComponent + DemoGuestbookEntryDocumentStoreDirective ] }) export class DemoSharedGuestbookModule { } diff --git a/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/store/guestbook.entry.document.store.directive.ts b/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/store/guestbook.entry.document.store.directive.ts new file mode 100644 index 000000000..9e5e329c5 --- /dev/null +++ b/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/store/guestbook.entry.document.store.directive.ts @@ -0,0 +1,17 @@ +import { Directive } from "@angular/core"; +import { DbxFirebaseDocumentStoreDirective, provideDbxFirebaseDocumentStoreDirective } from "@dereekb/dbx-firebase"; +import { GuestbookEntry, GuestbookEntryDocument } from "@dereekb/demo-firebase"; +import { GuestbookEntryDocumentStore } from "./guestbook.entry.document.store"; + +@Directive({ + exportAs: 'guestbookEntry', + selector: '[demoGuestbookEntryDocument]', + providers: provideDbxFirebaseDocumentStoreDirective(DemoGuestbookEntryDocumentStoreDirective, GuestbookEntryDocumentStore) +}) +export class DemoGuestbookEntryDocumentStoreDirective extends DbxFirebaseDocumentStoreDirective { + + constructor(store: GuestbookEntryDocumentStore) { + super(store); + } + +} 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 new file mode 100644 index 000000000..7dc277fbf --- /dev/null +++ b/apps/demo/src/app/modules/demo/modules/shared/modules/guestbook/store/guestbook.entry.document.store.ts @@ -0,0 +1,32 @@ +import { first, Observable, switchMap, shareReplay, from } from 'rxjs'; +import { Optional, Injectable } from "@angular/core"; +import { AbstractDbxFirebaseDocumentWithParentStore } from "@dereekb/dbx-firebase"; +import { DemoFirestoreCollections, Guestbook, GuestbookDocument, GuestbookEntry, guestbookEntryDeleteKey, GuestbookEntryDocument, guestbookEntryUpdateKey, GuestbookFunctions, GuestbookFunctionTypeMap, UpdateGuestbookEntryParams } from "@dereekb/demo-firebase"; +import { GuestbookDocumentStore } from "./guestbook.document.store"; +import { LoadingState, loadingStateFromObs } from '@dereekb/rxjs'; + +@Injectable() +export class GuestbookEntryDocumentStore extends AbstractDbxFirebaseDocumentWithParentStore { + + constructor(readonly guestbookFunctions: GuestbookFunctions, collections: DemoFirestoreCollections, @Optional() parent: GuestbookDocumentStore) { + super({ collectionFactory: collections.guestbookEntryCollectionFactory }); + + if (parent) { + this.setParentStore(parent); + } + } + + updateEntry(params: Omit): Observable> { + return this.parent$.pipe( + first(), + switchMap((parent) => + loadingStateFromObs(from(this.guestbookFunctions[guestbookEntryUpdateKey]({ + ...params, + guestbook: parent.id + }))) + ), + shareReplay(1) + ); + } + +} diff --git a/apps/demo/src/app/modules/demo/style/_demo.scss b/apps/demo/src/app/modules/demo/style/_demo.scss index a0870af80..01a4fa488 100644 --- a/apps/demo/src/app/modules/demo/style/_demo.scss +++ b/apps/demo/src/app/modules/demo/style/_demo.scss @@ -5,7 +5,7 @@ @use '../../../../style/variables'; // Define an alternate dark theme. -$demo-primary: mat.define-palette(mat.$amber-palette); +$demo-primary: mat.define-palette(mat.$deep-purple-palette); $demo-accent: mat.define-palette(mat.$blue-palette); $demo-warn: mat.define-palette(mat.$red-palette); $demo-mat-theme: mat.define-light-theme((color: (primary: $demo-primary, accent: $demo-accent, warn: $demo-warn), density: null, typography: null)); diff --git a/apps/demo/src/firebase/root.firebase.module.ts b/apps/demo/src/firebase/root.firebase.module.ts index a5bf00c13..f587e7782 100644 --- a/apps/demo/src/firebase/root.firebase.module.ts +++ b/apps/demo/src/firebase/root.firebase.module.ts @@ -1,8 +1,8 @@ import { FirestoreContext } from '@dereekb/firebase'; -import { DbxFirebaseFirestoreCollectionModule, DbxFirebaseEmulatorModule, DbxFirebaseDefaultProvidersModule, DbxFirebaseAuthModule } from '@dereekb/dbx-firebase'; +import { DbxFirebaseFirestoreCollectionModule, DbxFirebaseEmulatorModule, DbxFirebaseDefaultProvidersModule, DbxFirebaseAuthModule, DbxFirebaseFunctionsModule } from '@dereekb/dbx-firebase'; import { NgModule } from '@angular/core'; import { environment } from '../environments/environment'; -import { DemoFirestoreCollections, makeDemoFirestoreCollections } from '@dereekb/demo-firebase'; +import { DemoFirebaseFunctionsGetter, DemoFirestoreCollections, DEMO_FIREBASE_FUNCTIONS_CONFIG, makeDemoFirebaseFunctions, makeDemoFirestoreCollections } from '@dereekb/demo-firebase'; @NgModule({ imports: [ @@ -13,6 +13,11 @@ import { DemoFirestoreCollections, makeDemoFirestoreCollections } from '@dereekb appCollectionClass: DemoFirestoreCollections, collectionFactory: (firestoreContext: FirestoreContext) => makeDemoFirestoreCollections(firestoreContext) }), + DbxFirebaseFunctionsModule.forRoot({ + functionsGetterToken: DemoFirebaseFunctionsGetter, + functionsGetterFactory: makeDemoFirebaseFunctions, + functionsConfigMap: DEMO_FIREBASE_FUNCTIONS_CONFIG + }), DbxFirebaseAuthModule.forRoot({ delegateFactory: undefined // todo }) diff --git a/apps/demo/src/root.module.ts b/apps/demo/src/root.module.ts index 08abfa500..e7aa7ee5a 100644 --- a/apps/demo/src/root.module.ts +++ b/apps/demo/src/root.module.ts @@ -141,8 +141,7 @@ export function makeSegmentConfig(): DbxAnalyticsSegmentApiServiceConfig { }, { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { - floatLabel: 'always', - + floatLabel: 'always' } }], bootstrap: [UIView] diff --git a/packages/dbx-firebase/src/lib/firestore/firestore.context.service.ts b/packages/dbx-firebase/src/lib/firestore/firebase.firestore.context.service.ts similarity index 100% rename from packages/dbx-firebase/src/lib/firestore/firestore.context.service.ts rename to packages/dbx-firebase/src/lib/firestore/firebase.firestore.context.service.ts diff --git a/packages/dbx-firebase/src/lib/firestore/index.ts b/packages/dbx-firebase/src/lib/firestore/index.ts index 38a7cf95c..bcc6091dc 100644 --- a/packages/dbx-firebase/src/lib/firestore/index.ts +++ b/packages/dbx-firebase/src/lib/firestore/index.ts @@ -1,3 +1,3 @@ export * from './firebase.firestore'; export * from './firebase.firestore.module'; -export * from './firestore.context.service'; +export * from './firebase.firestore.context.service'; diff --git a/packages/dbx-firebase/src/lib/function/firebase.function.module.ts b/packages/dbx-firebase/src/lib/function/firebase.function.module.ts new file mode 100644 index 000000000..e0406de78 --- /dev/null +++ b/packages/dbx-firebase/src/lib/function/firebase.function.module.ts @@ -0,0 +1,58 @@ +import { ModuleWithProviders, NgModule, Provider } from "@angular/core"; +import { Functions } from "@angular/fire/functions"; +import { FirebaseFunctionsConfigMap, LazyFirebaseFunctions } from "@dereekb/firebase"; +import { ClassLikeType, forEachKeyValue } from "@dereekb/util"; + +export interface DbxFirebaseFunctionsModuleConfig { + functionsGetterToken: ClassLikeType; + functionsGetterFactory: (functions: Functions) => T; + /** + * Optional functions config map to provide. + * + * If provided, will inject all the types with factory functions so they can be injected into the app. + */ + functionsConfigMap?: FirebaseFunctionsConfigMap; +} + +/** + * Used to initialize the LazyFirebaseFunctions type for a DbxFirebase app. + */ +@NgModule() +export class DbxFirebaseFunctionsModule { + + static forRoot(config: DbxFirebaseFunctionsModuleConfig): ModuleWithProviders { + const providers: Provider[] = [{ + provide: config.functionsGetterToken, + useFactory: config.functionsGetterFactory, + deps: [Functions] + }]; + + if (config.functionsConfigMap) { + forEachKeyValue(config.functionsConfigMap, { + forEach: ([key, entry]) => { + const provide = entry[0]; + + providers.push({ + provide, + useFactory: (lazyFunctions: LazyFirebaseFunctions) => { + const getter = lazyFunctions[key as string]; + + if (!getter) { + throw new Error(`Could not create provider for firebase function getter "${provide}" as the getter was unavailable.`); + } else { + return getter(); + } + }, + deps: [config.functionsGetterToken] + }) + } + }); + } + + return { + ngModule: DbxFirebaseFunctionsModule, + providers + }; + } + +} diff --git a/packages/dbx-firebase/src/lib/function/index.ts b/packages/dbx-firebase/src/lib/function/index.ts new file mode 100644 index 000000000..85dc3bc81 --- /dev/null +++ b/packages/dbx-firebase/src/lib/function/index.ts @@ -0,0 +1 @@ +export * from './firebase.function.module'; diff --git a/packages/dbx-firebase/src/lib/index.ts b/packages/dbx-firebase/src/lib/index.ts index a22c335d5..37d30e859 100644 --- a/packages/dbx-firebase/src/lib/index.ts +++ b/packages/dbx-firebase/src/lib/index.ts @@ -1,5 +1,6 @@ export * from './auth'; export * from './firebase'; export * from './firestore'; +export * from './function'; export * from './model'; export * from './module'; diff --git a/packages/dbx-firebase/src/lib/model/store/index.ts b/packages/dbx-firebase/src/lib/model/store/index.ts index 804e07595..810d5d15b 100644 --- a/packages/dbx-firebase/src/lib/model/store/index.ts +++ b/packages/dbx-firebase/src/lib/model/store/index.ts @@ -3,6 +3,7 @@ export * from './store.collection'; export * from './store.collection.directive'; export * from './store.collection.list.directive'; export * from './store.document'; +export * from './store.document.auth.directive'; export * from './store.document.directive'; export * from './store.document.router.directive'; export * from './store.subcollection'; diff --git a/packages/dbx-firebase/src/lib/model/store/model.store.module.ts b/packages/dbx-firebase/src/lib/model/store/model.store.module.ts index 282b95aa6..72faaaeeb 100644 --- a/packages/dbx-firebase/src/lib/model/store/model.store.module.ts +++ b/packages/dbx-firebase/src/lib/model/store/model.store.module.ts @@ -1,16 +1,19 @@ import { NgModule } from "@angular/core"; import { DbxFirebaseCollectionListDirective } from "./store.collection.list.directive"; +import { DbxFirebaseDocumentAuthIdDirective } from "./store.document.auth.directive"; import { DbxFirebaseDocumentStoreRouteIdDirective } from "./store.document.router.directive"; @NgModule({ imports: [], declarations: [ DbxFirebaseCollectionListDirective, - DbxFirebaseDocumentStoreRouteIdDirective + DbxFirebaseDocumentStoreRouteIdDirective, + DbxFirebaseDocumentAuthIdDirective ], exports: [ DbxFirebaseCollectionListDirective, - DbxFirebaseDocumentStoreRouteIdDirective + DbxFirebaseDocumentStoreRouteIdDirective, + DbxFirebaseDocumentAuthIdDirective ] }) export class DbxFirebaseModelStoreModule { } diff --git a/packages/dbx-firebase/src/lib/model/store/store.document.auth.directive.ts b/packages/dbx-firebase/src/lib/model/store/store.document.auth.directive.ts new file mode 100644 index 000000000..ec6127aad --- /dev/null +++ b/packages/dbx-firebase/src/lib/model/store/store.document.auth.directive.ts @@ -0,0 +1,25 @@ +import { Directive, Host, OnInit } from "@angular/core"; +import { FirestoreDocument } from "@dereekb/firebase"; +import { AbstractSubscriptionDirective } from '@dereekb/dbx-core'; +import { DbxFirebaseAuthService } from "../../auth/service/firebase.auth.service"; +import { DbxFirebaseDocumentStoreDirective } from "./store.document.directive"; + +/** + * Utility directive for a host DbxFirebaseDocumentStoreDirective that sets the document's ID to match the ID of the current user. + * + * This is useful for cases where each document is keyed by the user (I.E. implements UserRelatedById). + */ +@Directive({ + selector: '[dbxFirebaseDocumentAuthId]' +}) +export class DbxFirebaseDocumentAuthIdDirective = FirestoreDocument> extends AbstractSubscriptionDirective implements OnInit { + + constructor(readonly dbxFirebaseAuthService: DbxFirebaseAuthService, @Host() readonly dbxFirebaseDocumentStoreDirective: DbxFirebaseDocumentStoreDirective) { + super(); + } + + ngOnInit(): void { + this.sub = this.dbxFirebaseDocumentStoreDirective.store.setId(this.dbxFirebaseAuthService.userIdentifier$); + } + +} diff --git a/packages/dbx-firebase/src/lib/model/store/store.document.directive.ts b/packages/dbx-firebase/src/lib/model/store/store.document.directive.ts index 9db2d44c6..814d21699 100644 --- a/packages/dbx-firebase/src/lib/model/store/store.document.directive.ts +++ b/packages/dbx-firebase/src/lib/model/store/store.document.directive.ts @@ -11,6 +11,8 @@ export abstract class DbxFirebaseDocumentStoreDirective; +// @ts-ignore +export abstract class FunctionFactoryTestModelFunctions implements FirebaseFunctionMap { } + +export const functionFactoryTestModelMap = firebaseFunctionMapFactory(testFunctionTypeConfigMap); + describe('firebaseFunctionMapFactory()', () => { const mockFunctions: Functions = {} as any; @@ -51,3 +56,49 @@ describe('firebaseFunctionMapFactory()', () => { }); }); + +// MARK: Lazy Factory +export type FunctionFactoryTestMapFunctionsMap = { + testFunctions: FunctionFactoryTestModelTypeMap; +} + +export const TEST_FUNCTION_FACTORY_FUNCTIONS_CONFIG: FirebaseFunctionsConfigMap = { + testFunctions: [FunctionFactoryTestModelFunctions, functionFactoryTestModelMap] +}; + +export abstract class FunctionFactoryTestFunctionsGetter { + abstract readonly testFunctions: FirebaseFunctionGetter; +} + +describe('lazyFirebaseFunctionsFactory()', () => { + + const mockFunctions: Functions = {} as any; + + it('should create a factory function', () => { + const factory = lazyFirebaseFunctionsFactory(TEST_FUNCTION_FACTORY_FUNCTIONS_CONFIG); + expect(factory).toBeDefined(); + expect(typeof factory).toBe('function'); + }); + + describe('function', () => { + + it('should return a FunctionFactoryTestFunctionsGetter.', () => { + const factory = lazyFirebaseFunctionsFactory(TEST_FUNCTION_FACTORY_FUNCTIONS_CONFIG); + const result = factory(mockFunctions); + + expect(result.testFunctions).toBeDefined(); + expect(result.testFunctions._type).toBeDefined(); + expect(result.testFunctions._type).toBe(FunctionFactoryTestModelFunctions); + expect(result.testFunctions._key).toBe('testFunctions'); + + const testFunctions = result.testFunctions(); + expect(testFunctions).toBeDefined(); + expect(testFunctions[functionFactoryTestModelFunctionA]).toBeDefined(); + expect(typeof testFunctions[functionFactoryTestModelFunctionA]).toBe('function'); + expect(testFunctions[functionFactoryTestModelFunctionB]).toBeDefined(); + expect(typeof testFunctions[functionFactoryTestModelFunctionB]).toBe('function'); + }); + + }); + +}); diff --git a/packages/firebase/src/lib/client/function/function.factory.ts b/packages/firebase/src/lib/client/function/function.factory.ts index 9e20f2ffb..fd8b68cf4 100644 --- a/packages/firebase/src/lib/client/function/function.factory.ts +++ b/packages/firebase/src/lib/client/function/function.factory.ts @@ -1,7 +1,9 @@ -import { mapObjectMap, Maybe } from '@dereekb/util'; +import { cachedGetter } from '@dereekb/util'; +import { ClassLikeType, ClassType, forEachKeyValue, Getter, mapObjectMap, Maybe } from '@dereekb/util'; import { Functions, httpsCallable, HttpsCallableOptions } from "firebase/functions"; import { FirebaseFunctionMap, FirebaseFunctionMapFunction, FirebaseFunctionTypeMap } from './function'; +// MARK: Functions Factory export interface FirebaseFunctionTypeConfig { options?: HttpsCallableOptions; } @@ -32,3 +34,49 @@ export function firebaseFunctionMapFactory(co return result; }; } + +// MARK: Lazy Functions Accessor +export type FirebaseFunctionMapKey = string; +export type FirebaseFunctionGetter = Getter & { _type: ClassLikeType, _key: FirebaseFunctionMapKey }; + +/** + * Map of all firebase functions in the app. + */ +export type FirebaseFunctionsMap = { + [key: FirebaseFunctionMapKey]: FirebaseFunctionTypeMap; +} + +export type FirebaseFunctionsConfigMap = { + [K in keyof M]: FirebaseFunctionsConfigMapEntry; +} + +export type FirebaseFunctionsConfigMapEntry = [ClassLikeType, FirebaseFunctionMapFactory]; + +/** + * Factory function for creating a FirebaseFunctionsMap for a given Functions instance. + */ +export type LazyFirebaseFunctionsFactory = (functions: Functions) => LazyFirebaseFunctions; + +/** + * Map of FirebaseFunctionGetter values that are lazy-loaded via the getter. + */ +export type LazyFirebaseFunctions = { + [K in keyof M]: FirebaseFunctionGetter>; +} + +export function lazyFirebaseFunctionsFactory = FirebaseFunctionsConfigMap>(configMap: C): LazyFirebaseFunctionsFactory { + return (functions: Functions) => { + + const mapFn = (config: FirebaseFunctionsConfigMapEntry, key: K) => { + const type = config[0]; + const factory: FirebaseFunctionMapFactory = config[1]; + const getter = cachedGetter(() => factory(functions)) as unknown as FirebaseFunctionGetter; + getter._type = type; + getter._key = key as string; + return getter; + }; + + const result: LazyFirebaseFunctions = mapObjectMap(configMap, mapFn as any); + return result; + }; +}