Skip to content

Commit

Permalink
feat: added dbxFirebaseDocumentWithParentStore
Browse files Browse the repository at this point in the history
- added LockSet tests
- added other tests
  • Loading branch information
dereekb committed May 5, 2022
1 parent abac6aa commit f055d81
Show file tree
Hide file tree
Showing 20 changed files with 570 additions and 124 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
steps:
- checkout
# look for existing cache and restore if found
# NOTE: With use of npm ci, the cache is ignored.
# - restore_cache:
# keys:
# - node-cache-a{{ checksum "package-lock.json" }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ <h2>{{ name$ | async }}</h2>
<div class="guestbook-view-content">
<mat-divider></mat-divider>
<div>
<!-- <demo-guestbook-entry-list dbxFirebaseCollectionList demoGuestbookEntryCollection></demo-guestbook-entry-list> -->

</div>
<div>
<demo-guestbook-entry-list dbxFirebaseCollectionList demoGuestbookEntryCollection>
<dbx-list-empty-content empty>
<p>There are no entries yet in the guest book.</p>
</dbx-list-empty-content>
</demo-guestbook-entry-list>
</div>
</div>
</dbx-two-block>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DemoGuestbookListComponent, DemoGuestbookListViewComponent, DemoGuestbo
import { DemoGuestbookEntryListComponent, DemoGuestbookEntryListViewComponent, DemoGuestbookEntryListViewItemComponent } from './component/guestbook.entry.list.component';
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';

@NgModule({
imports: [
Expand All @@ -12,6 +13,7 @@ import { DemoGuestbookDocumentStoreDirective } from './store/guestbook.document.
declarations: [
DemoGuestbookCollectionStoreDirective,
DemoGuestbookDocumentStoreDirective,
DemoGuestbookEntryCollectionStoreDirective,
DemoGuestbookListComponent,
DemoGuestbookListViewComponent,
DemoGuestbookListViewItemComponent,
Expand All @@ -22,6 +24,7 @@ import { DemoGuestbookDocumentStoreDirective } from './store/guestbook.document.
exports: [
DemoGuestbookCollectionStoreDirective,
DemoGuestbookDocumentStoreDirective,
DemoGuestbookEntryCollectionStoreDirective,
DemoGuestbookListComponent,
DemoGuestbookEntryListComponent
]
Expand Down
19 changes: 11 additions & 8 deletions packages/dbx-core/src/lib/ngrx/store.lockset.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Observable } from 'rxjs';
import { map, Observable } from 'rxjs';
import { ComponentStore } from '@ngrx/component-store';
import { Inject, Optional, Injectable, OnDestroy } from '@angular/core';
import { LockSet } from '@dereekb/rxjs';
import { LockSet, SubscriptionObject, ObservableOrValue, asObservable } from '@dereekb/rxjs';
import { Maybe } from '@dereekb/util';

export interface LockSetComponent {
Expand Down Expand Up @@ -36,7 +36,7 @@ export abstract class LockSetComponentStore<S extends object> extends ComponentS
// MARK: Locks
protected setupLockSet({ parent, locks }: LockSetComponentStoreConfig): void {
if (parent) {
this.addParentLockSet(parent);
this.setParentLockSet(parent);
}

if (locks) {
Expand All @@ -48,11 +48,11 @@ export abstract class LockSetComponentStore<S extends object> extends ComponentS
}
}

protected addParentLockSet(obs: Observable<LockSetComponent>): void {
obs.subscribe((x) => x.lockSet.addChildLockSet(this.lockSet));
setParentLockSet(obs: ObservableOrValue<Maybe<LockSetComponent>>): void {
this.lockSet.setParentLockSet(asObservable(obs).pipe(map(x => x?.lockSet)));
}

protected addLock(key: string, obs: Observable<boolean>): void {
addLock(key: string, obs: Observable<boolean>): void {
this.lockSet.addLock(key, obs);
}

Expand All @@ -62,13 +62,16 @@ export abstract class LockSetComponentStore<S extends object> extends ComponentS
// Wait for any actions to complete before destroying.
this.lockSet.destroyOnNextUnlock({
fn: () => {
this._ngFinishDestroy();
this._destroyNow();
},
timeout: this.lockSetDestroyTimeoutMs,
}, this.lockSetDestroyDelayMs);
}

protected _ngFinishDestroy() {
/**
* Completes the cleanup of the object.
*/
_destroyNow() {
this.lockSet.destroy();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
*/
// use the node environment, as the jsdom environment breaks for tests that use the firestore.

import { authorizedTestWithMockItemCollection, DocumentReference, MockItem, MockItemDocument, MockItemFirestoreCollection } from "@dereekb/firebase";
import { loadingStateIsLoading } from "@dereekb/rxjs";
import { filter, first, of, timeout } from "rxjs";
import { authorizedTestWithMockItemCollection, MockItem, MockItemDocument, MockItemFirestoreCollection } from "@dereekb/firebase";
import { first } from "rxjs";
import { AbstractDbxFirebaseCollectionStore } from './store.collection';

export class TestDbxFirebaseCollectionStore extends AbstractDbxFirebaseCollectionStore<MockItem, MockItemDocument> {
Expand All @@ -28,7 +27,7 @@ describe('AbstractDbxFirebaseCollectionStore', () => {
});

afterEach(() => {
store.ngOnDestroy();
store._destroyNow();
});

describe('loader$', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface DbxFirebaseCollectionStore<T, D extends FirestoreDocument<T> =
setConstraints(observableOrValue: ObservableOrValue<Maybe<ArrayOrValue<FirestoreQueryConstraint<T>>>>): Subscription;
next(observableOrValue: ObservableOrValue<void>): void;
restart(observableOrValue: ObservableOrValue<void>): void;

readonly setFirestoreCollection: (() => void) | ((observableOrValue: ObservableOrValue<Maybe<FirestoreCollection<T, D>>>) => Subscription);
}

export interface DbxFirebaseCollectionStoreContextState<T, D extends FirestoreDocument<T> = FirestoreDocument<T>> {
Expand Down Expand Up @@ -103,6 +105,6 @@ export class AbstractDbxFirebaseCollectionStore<T, D extends FirestoreDocument<T
readonly accumulator$: Observable<FirebaseQueryItemAccumulator<T>> = this.loader$.pipe(switchMap(x => x.accumulator$));
readonly pageLoadingState$: Observable<PageListLoadingState<T>> = this.loader$.pipe(switchMap(x => x.pageLoadingState$));

readonly setFirestoreCollection = this.updater((state, firestoreCollection: Maybe<FirestoreCollection<T, D>>) => ({ ...state, firestoreCollection }));
readonly setFirestoreCollection = this.updater((state, firestoreCollection: FirestoreCollection<T, D> | null | undefined) => ({ ...state, firestoreCollection }));

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('AbstractDbxFirebaseDocumentStore', () => {

afterEach(() => {
sub.destroy();
store.ngOnDestroy();
store._destroyNow();
});

describe('loading a document', () => {
Expand Down Expand Up @@ -73,9 +73,6 @@ describe('AbstractDbxFirebaseDocumentStore', () => {

});

/*
// NOTE: This test breaks in the CI environment for some reason.
it('should not load anything if neither id nor ref are set.', (done) => {

let sub: SubscriptionObject = new SubscriptionObject();
Expand All @@ -90,7 +87,6 @@ describe('AbstractDbxFirebaseDocumentStore', () => {
});

});
*/

});

Expand Down
39 changes: 23 additions & 16 deletions packages/dbx-firebase/src/lib/model/store/store.document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { Observable, shareReplay, distinctUntilChanged, map, switchMap, combineL
import { DocumentSnapshot, DocumentReference, FirestoreCollection, FirestoreDocument, documentDataWithId, DocumentDataWithId } from '@dereekb/firebase';
import { filterMaybe, LoadingState, beginLoading, successResult, loadingStateFromObs, errorResult, ObservableOrValue } from '@dereekb/rxjs';
import { Maybe, ModelKey, isMaybeSo } from '@dereekb/util';
import { LockSetComponentStore } from '@dereekb/dbx-core';
import { LockSetComponent, LockSetComponentStore } from '@dereekb/dbx-core';
import { modelDoesNotExistError } from '../error';

export interface DbxFirebaseDocumentStore<T, D extends FirestoreDocument<T> = FirestoreDocument<T>> {
export interface DbxFirebaseDocumentStore<T, D extends FirestoreDocument<T> = FirestoreDocument<T>> extends LockSetComponent {
readonly firestoreCollection$: Observable<FirestoreCollection<T, D>>;

readonly currentInputId$: Observable<Maybe<ModelKey>>;
Expand All @@ -28,11 +28,15 @@ export interface DbxFirebaseDocumentStore<T, D extends FirestoreDocument<T> = Fi

setId: (observableOrValue: ObservableOrValue<string>) => Subscription;
setRef: (observableOrValue: ObservableOrValue<DocumentReference<T>>) => Subscription;
setFirestoreCollection: (observableOrValue: ObservableOrValue<FirestoreCollection<T, D>>) => Subscription;

/**
* Sets the firestore collection to retrieve document from.
*/
readonly setFirestoreCollection: (() => void) | ((observableOrValue: ObservableOrValue<Maybe<FirestoreCollection<T, D>>>) => Subscription);
}

export interface DbxFirebaseDocumentStoreContextState<T, D extends FirestoreDocument<T> = FirestoreDocument<T>> {
readonly firestoreCollection: FirestoreCollection<T, D>;
readonly firestoreCollection?: Maybe<FirestoreCollection<T, D>>;
readonly id?: Maybe<ModelKey>;
readonly ref?: Maybe<DocumentReference<T>>;
}
Expand All @@ -47,12 +51,16 @@ export class AbstractDbxFirebaseDocumentStore<T, D extends FirestoreDocument<T>


// MARK: Accessors
readonly firestoreCollection$: Observable<FirestoreCollection<T, D>> = this.state$.pipe(
map(x => x.firestoreCollection),
readonly currentFirestoreCollection$: Observable<Maybe<FirestoreCollection<T, D>>> = this.state$.pipe(
map((x) => x.firestoreCollection),
distinctUntilChanged(),
shareReplay(1)
);

readonly firestoreCollection$: Observable<FirestoreCollection<T, D>> = this.currentFirestoreCollection$.pipe(
filterMaybe()
);

readonly currentInputId$: Observable<Maybe<ModelKey>> = this.state$.pipe(
map(x => x.id),
distinctUntilChanged(),
Expand All @@ -77,14 +85,16 @@ export class AbstractDbxFirebaseDocumentStore<T, D extends FirestoreDocument<T>
shareReplay(1)
);

readonly currentDocument$: Observable<Maybe<D>> = combineLatest([this.firestoreCollection$, this.currentInputId$, this.currentInputRef$]).pipe(
readonly currentDocument$: Observable<Maybe<D>> = combineLatest([this.currentFirestoreCollection$, this.currentInputId$, this.currentInputRef$]).pipe(
map(([collection, id, ref]) => {
let document: Maybe<D>;

if (ref) {
document = collection.documentAccessor().loadDocument(ref);
} else if (id) {
document = collection.documentAccessor().loadDocumentForPath(id);
if (collection) {
if (ref) {
document = collection.documentAccessor().loadDocument(ref);
} else if (id) {
document = collection.documentAccessor().loadDocumentForPath(id);
}
}

return document;
Expand Down Expand Up @@ -172,13 +182,10 @@ export class AbstractDbxFirebaseDocumentStore<T, D extends FirestoreDocument<T>
readonly setId = this.updater((state, id: ModelKey) => (id) ? ({ ...state, id, ref: undefined }) : ({ ...state, id }));

/**
* Sets the id of the document to load.
* Sets the ref of the document to load.
*/
readonly setRef = this.updater((state, ref: DocumentReference<T>) => (ref) ? ({ ...state, id: undefined, ref }) : ({ ...state, ref }));

/**
* Sets the id of the document to load.
*/
readonly setFirestoreCollection = this.updater((state, firestoreCollection: FirestoreCollection<T, D>) => ({ ...state, firestoreCollection }));
readonly setFirestoreCollection = this.updater((state, firestoreCollection: Maybe<FirestoreCollection<T, D>>) => ({ ...state, firestoreCollection }));

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @jest-environment node
*/
// use the node environment, as the jsdom environment breaks for tests that use the firestore.

import { MockItemSubItem, MockItemSubItemDocument, authorizedTestWithMockItemCollection, MockItem, MockItemDocument, MockItemFirestoreCollection, MockItemSubItemFirestoreCollectionFactory } from "@dereekb/firebase";
import { SubscriptionObject } from "@dereekb/rxjs";
import { filter, first, of, timeout } from "rxjs";
import { AbstractDbxFirebaseDocumentStore } from "./store.document";
import { AbstractDbxFirebaseDocumentWithParentStore } from './store.subcollection.document';

export class TestDbxFirebaseDocumentStore extends AbstractDbxFirebaseDocumentStore<MockItem, MockItemDocument> {

constructor(firestoreCollection: MockItemFirestoreCollection) {
super({ firestoreCollection })
}

}

export class TestDbxFirebaseDocumentWithParentStore extends AbstractDbxFirebaseDocumentWithParentStore<MockItemSubItem, MockItem, MockItemSubItemDocument, MockItemDocument> {

constructor(collectionFactory: MockItemSubItemFirestoreCollectionFactory) {
super({ collectionFactory })
}

}

describe('AbstractDbxFirebaseCollectionWithParentStore', () => {

authorizedTestWithMockItemCollection((f) => {

let sub: SubscriptionObject;
let parentStore: TestDbxFirebaseDocumentStore;
let store: TestDbxFirebaseDocumentWithParentStore;

beforeEach(() => {
const firestoreCollection = f.instance.firestoreCollection;
parentStore = new TestDbxFirebaseDocumentStore(firestoreCollection);
store = new TestDbxFirebaseDocumentWithParentStore(f.instance.mockItemSubItemCollection);
sub = new SubscriptionObject();
});

afterEach(() => {
parentStore.ngOnDestroy();
store.ngOnDestroy();
sub.destroy();
});

describe('with parent store', () => {

beforeEach(() => {
store.setParentStore(parentStore);
});

it('should not load while a parent is not set.', (done) => {
sub.subscription = store.document$.pipe(timeout({ first: 500, with: () => of(false) }), first()).subscribe((result) => {
expect(result).toBe(false);
done();
});
});

describe('with parent loaded', () => {

beforeEach(() => {
parentStore.setId('test');
});

it('should load the document.', (done) => {
sub.subscription = store.document$.pipe(first()).subscribe((iteration) => {
expect(iteration).toBeDefined();
done();
});
});

});

});

});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { filterMaybe } from '@dereekb/rxjs';
import { Injectable } from '@angular/core';
import { Observable, shareReplay, distinctUntilChanged, map } from 'rxjs';
import { FirestoreDocument, FirestoreCollectionWithParentFactory } from '@dereekb/firebase';
import { Maybe } from '@dereekb/util';
import { AbstractDbxFirebaseDocumentStore, DbxFirebaseDocumentStore, DbxFirebaseDocumentStoreContextState } from './store.document';
import { DbxFirebaseComponentStoreWithParentSetParentEffectFunction, setParentEffect, DbxFirebaseComponentStoreWithParentSetParentStoreEffectFunction, setParentStoreEffect, DbxFirebaseComponentStoreWithParent, DbxFirebaseComponentStoreWithParentContextState } from './store.subcollection.rxjs';

export interface DbxFirebaseDocumentWithParentStore<T, PT, D extends FirestoreDocument<T> = FirestoreDocument<T>, PD extends FirestoreDocument<PT> = FirestoreDocument<PT>>
extends DbxFirebaseDocumentStore<T, D>, DbxFirebaseComponentStoreWithParent<T, PT, D, PD> { }

export interface DbxFirebaseDocumentWithParentStoreContextState<T, PT, D extends FirestoreDocument<T> = FirestoreDocument<T>, PD extends FirestoreDocument<PT> = FirestoreDocument<PT>>
extends DbxFirebaseDocumentStoreContextState<T, D>, DbxFirebaseComponentStoreWithParentContextState<T, PT, D, PD> { }

/**
* Abstract DbxFirebaseDocumentStore that has a parent document from which is derives it's FiresbaseCollection from.
*/
@Injectable()
export class AbstractDbxFirebaseDocumentWithParentStore<T, PT, D extends FirestoreDocument<T> = FirestoreDocument<T>, PD extends FirestoreDocument<PT> = FirestoreDocument<PT>, C extends DbxFirebaseDocumentWithParentStoreContextState<T, PT, D, PD> = DbxFirebaseDocumentWithParentStoreContextState<T, PT, D, PD>>
extends AbstractDbxFirebaseDocumentStore<T, D, C> implements DbxFirebaseDocumentWithParentStore<T, PT, D, PD> {

// MARK: Effects
readonly setParent: DbxFirebaseComponentStoreWithParentSetParentEffectFunction<PD> = setParentEffect(this);
readonly setParentStore: DbxFirebaseComponentStoreWithParentSetParentStoreEffectFunction<PT, PD> = setParentStoreEffect(this);

// MARK: Accessors
readonly currentParent$: Observable<Maybe<PD>> = this.state$.pipe(
map(x => x.parent),
distinctUntilChanged(),
shareReplay(1)
);

readonly parent$: Observable<PD> = this.currentParent$.pipe(
filterMaybe()
);

readonly currentCollectionFactory$: Observable<Maybe<FirestoreCollectionWithParentFactory<T, PT, D, PD>>> = this.state$.pipe(
map(x => x.collectionFactory),
distinctUntilChanged(),
shareReplay(1)
);

readonly collectionFactory$: Observable<FirestoreCollectionWithParentFactory<T, PT, D, PD>> = this.currentCollectionFactory$.pipe(
filterMaybe()
);

// MARK: State Changes
/**
* Sets the collection factory function to use.
*/
readonly setCollectionFactory = this.updater((state, collectionFactory: FirestoreCollectionWithParentFactory<T, PT, D, PD>) => ({ ...state, collectionFactory }));

/**
* Sets the parent on the current state.
*/
readonly _setParentDocument = this.updater((state, parent: Maybe<PD>) => ({ ...state, parent }));

}
Loading

0 comments on commit f055d81

Please sign in to comment.