diff --git a/sample/angular.json b/sample/angular.json index bd7dc84a9..7a978ca9d 100644 --- a/sample/angular.json +++ b/sample/angular.json @@ -129,7 +129,13 @@ "main": "server.ts", "tsConfig": "tsconfig.server.json", "bundleDependencies": true, - "externalDependencies": [ ] + "externalDependencies": [ ], + "fileReplacements": [ + { + "replace": "src/firestore.ts", + "with": "src/firestore.server.ts" + } + ] }, "configurations": { "production": { @@ -138,6 +144,10 @@ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" + }, + { + "replace": "src/firestore.ts", + "with": "src/firestore.server.ts" } ], "sourceMap": false, diff --git a/sample/package.json b/sample/package.json index bc0b2860f..34a48e813 100644 --- a/sample/package.json +++ b/sample/package.json @@ -32,6 +32,7 @@ "core-js": "^3.6.5", "firebase": "^8.0.0", "first-input-delay": "^0.1.3", + "preboot": "^7.0.0", "proxy-polyfill": "^0.3.2", "rxjs": "~6.6.3", "tslib": "^2.0.1", @@ -46,6 +47,8 @@ "@angular/language-service": "~11.0.0", "@firebase/app-types": "^0.6.1", "@nguniversal/builders": "^10.1.0", + "@types/express": "^4.17.9", + "@types/express-serve-static-core": "^4.17.14", "@types/jasmine": "~3.5.0", "@types/jasminewd2": "~2.0.3", "codelyzer": "^6.0.0", diff --git a/sample/server.ts b/sample/server.ts index a82723a8c..c91f2c79d 100644 --- a/sample/server.ts +++ b/sample/server.ts @@ -34,9 +34,10 @@ export function app() { // Example Express Rest API endpoints // app.get('/api/**', (req, res) => { }); // Serve static files from /browser + // TODO sort out why the types broke, express? server.get('*.*', express.static(distFolder, { maxAge: '1y' - })); + }) as any); // All regular routes use the Universal engine server.get('*', (req, res) => { diff --git a/sample/src/app/app-routing.module.ts b/sample/src/app/app-routing.module.ts index 750e62d3c..e444ac0fc 100644 --- a/sample/src/app/app-routing.module.ts +++ b/sample/src/app/app-routing.module.ts @@ -6,9 +6,10 @@ import { AngularFireAuthGuard, canActivate, isNotAnonymous } from '@angular/fire import { SecondaryComponent } from './secondary/secondary.component'; const routes: Routes = [ - { path: '', component: HomeComponent, outlet: 'primary', pathMatch: 'prefix' }, - { path: '', component: SecondaryComponent, outlet: 'secondary', pathMatch: 'prefix' }, - { path: '', component: SecondaryComponent, outlet: 'tertiary', pathMatch: 'prefix' }, + { path: '', component: HomeComponent, outlet: 'primary' }, + { path: '', component: SecondaryComponent, outlet: 'secondary' }, + { path: '', component: SecondaryComponent, outlet: 'tertiary' }, + { path: 'index.html', component: HomeComponent, outlet: 'primary', pathMatch: 'full' }, { path: 'protected', component: ProtectedComponent, canActivate: [AngularFireAuthGuard] }, { path: 'lazy', loadChildren: () => import('./protected-lazy/protected-lazy.module').then(m => m.ProtectedLazyModule) }, { path: 'protected-lazy', diff --git a/sample/src/app/app.browser.module.ts b/sample/src/app/app.browser.module.ts new file mode 100644 index 000000000..8acbc17e3 --- /dev/null +++ b/sample/src/app/app.browser.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { AppModule } from './app.module'; +import { AppComponent } from './app.component'; +import { AngularFirestoreModule } from '@angular/fire/firestore-lazy'; + +@NgModule({ + imports: [ + AppModule, + AngularFirestoreModule.enablePersistence({ + synchronizeTabs: true + }) + ], + bootstrap: [AppComponent], +}) +export class AppBrowserModule {} diff --git a/sample/src/app/app.component.ts b/sample/src/app/app.component.ts index 90409f892..06cee7563 100644 --- a/sample/src/app/app.component.ts +++ b/sample/src/app/app.component.ts @@ -1,5 +1,6 @@ -import { ApplicationRef, Component } from '@angular/core'; +import { ApplicationRef, Component, Inject, isDevMode, Optional } from '@angular/core'; import { FirebaseApp } from '@angular/fire'; +import { RESPONSE } from '@nguniversal/express-engine/tokens'; import { debounceTime } from 'rxjs/operators'; @Component({ @@ -25,7 +26,12 @@ import { debounceTime } from 'rxjs/operators'; styles: [``] }) export class AppComponent { - constructor(public readonly firebaseApp: FirebaseApp, appRef: ApplicationRef) { - appRef.isStable.pipe(debounceTime(200)).subscribe(it => console.log('isStable', it)); + constructor(public readonly firebaseApp: FirebaseApp, appRef: ApplicationRef, @Optional() @Inject(RESPONSE) response: any) { + if (isDevMode()) { + appRef.isStable.pipe(debounceTime(200)).subscribe(it => console.log('isStable', it)); + } + if (response) { + response.setHeader('Cache-Control', 'public, max-age=600'); + } } } diff --git a/sample/src/app/app.module.ts b/sample/src/app/app.module.ts index cd31d6864..0f6e34f93 100644 --- a/sample/src/app/app.module.ts +++ b/sample/src/app/app.module.ts @@ -19,9 +19,9 @@ import { } from '@angular/fire/analytics'; import { FirestoreComponent } from './firestore/firestore.component'; -import { AngularFireDatabaseModule, USE_EMULATOR as USE_DATABASE_EMULATOR } from '@angular/fire/database'; -import { AngularFirestoreModule, USE_EMULATOR as USE_FIRESTORE_EMULATOR, SETTINGS as FIRESTORE_SETTINGS } from '@angular/fire/firestore'; -import { AngularFireStorageModule } from '@angular/fire/storage'; +import { AngularFireDatabaseModule, USE_EMULATOR as USE_DATABASE_EMULATOR } from '@angular/fire/database-lazy'; +import { USE_EMULATOR as USE_FIRESTORE_EMULATOR, SETTINGS as FIRESTORE_SETTINGS } from '../firestore'; +import { AngularFireStorageModule } from '@angular/fire/storage-lazy'; import { AngularFireAuthModule, USE_DEVICE_LANGUAGE, USE_EMULATOR as USE_AUTH_EMULATOR } from '@angular/fire/auth'; import { AngularFireMessagingModule, SERVICE_WORKER, VAPID_KEY } from '@angular/fire/messaging'; import { AngularFireFunctionsModule, USE_EMULATOR as USE_FUNCTIONS_EMULATOR, ORIGIN as FUNCTIONS_ORIGIN, NEW_ORIGIN_BEHAVIOR } from '@angular/fire/functions'; @@ -35,8 +35,6 @@ import { HomeComponent } from './home/home.component'; import { AuthComponent } from './auth/auth.component'; import { MessagingComponent } from './messaging/messaging.component'; import { FunctionsComponent } from './functions/functions.component'; -import { FirestoreOfflineComponent } from './firestore-offline/firestore-offline.component'; -import { FirestoreOfflineModule } from './firestore-offline/firestore-offline.module'; import { UpboatsComponent } from './upboats/upboats.component'; @NgModule({ @@ -44,7 +42,6 @@ import { UpboatsComponent } from './upboats/upboats.component'; AppComponent, StorageComponent, FirestoreComponent, - FirestoreOfflineComponent, DatabaseComponent, RemoteConfigComponent, HomeComponent, @@ -61,7 +58,6 @@ import { UpboatsComponent } from './upboats/upboats.component'; AngularFireModule.initializeApp(environment.firebase), AngularFireStorageModule, AngularFireDatabaseModule, - AngularFirestoreModule, AngularFireAuthModule, AngularFireAuthGuardModule, AngularFireRemoteConfigModule, @@ -69,14 +65,13 @@ import { UpboatsComponent } from './upboats/upboats.component'; AngularFireAnalyticsModule, AngularFireFunctionsModule, AngularFirePerformanceModule, - FirestoreOfflineModule ], providers: [ UserTrackingService, ScreenTrackingService, PerformanceMonitoringService, { provide: FIRESTORE_SETTINGS, useValue: { ignoreUndefinedProperties: true } }, - { provide: ANALYTICS_DEBUG_MODE, useValue: true }, + { provide: ANALYTICS_DEBUG_MODE, useFactory: isDevMode }, { provide: COLLECTION_ENABLED, useValue: true }, { provide: USE_AUTH_EMULATOR, useValue: environment.useEmulators ? ['localhost', 9099] : undefined }, { provide: USE_DATABASE_EMULATOR, useValue: environment.useEmulators ? ['localhost', 9000] : undefined }, diff --git a/sample/src/app/app.server.module.ts b/sample/src/app/app.server.module.ts index dd55bd1b6..fcb9f15d5 100644 --- a/sample/src/app/app.server.module.ts +++ b/sample/src/app/app.server.module.ts @@ -1,18 +1,19 @@ -import { isDevMode, NgModule } from '@angular/core'; +import { NgModule } from '@angular/core'; import { ServerModule, ServerTransferStateModule } from '@angular/platform-server'; - import { AppModule } from './app.module'; import { AppComponent } from './app.component'; import { APP_BASE_HREF } from '@angular/common'; +import { AngularFirestoreModule } from '@angular/fire/firestore-lazy/memory'; @NgModule({ imports: [ AppModule, ServerModule, - ServerTransferStateModule + ServerTransferStateModule, + AngularFirestoreModule ], providers: [ - { provide: APP_BASE_HREF, useFactory: () => isDevMode() ? '/us-central1/ssr' : '/ssr' }, + { provide: APP_BASE_HREF, useFactory: () => process.env.FUNCTIONS_EMULATOR === 'true' ? '/aftest-94085/us-central1/ssr' : '/ssr' }, ], bootstrap: [AppComponent], }) diff --git a/sample/src/app/database/database.component.ts b/sample/src/app/database/database.component.ts index 9438d4f52..c91bbea8c 100644 --- a/sample/src/app/database/database.component.ts +++ b/sample/src/app/database/database.component.ts @@ -1,10 +1,9 @@ import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core'; -import { AngularFireDatabase } from '@angular/fire/database'; -import { EMPTY, Observable } from 'rxjs'; +import { AngularFireDatabase } from '@angular/fire/database-lazy'; +import { Observable } from 'rxjs'; import { makeStateKey, TransferState } from '@angular/platform-browser'; import { startWith, tap } from 'rxjs/operators'; import { trace } from '@angular/fire/performance'; -import { isPlatformServer } from '@angular/common'; @Component({ selector: 'app-database', @@ -21,17 +20,13 @@ export class DatabaseComponent implements OnInit { public readonly testObjectValue$: Observable; constructor(state: TransferState, database: AngularFireDatabase, @Inject(PLATFORM_ID) platformId: object) { - if (isPlatformServer(platformId)) { - this.testObjectValue$ = EMPTY; - } else { - const doc = database.object('test'); - const key = makeStateKey(doc.query.toString()); - const existing = state.get(key, undefined); - this.testObjectValue$ = doc.valueChanges().pipe( - trace('database'), - existing ? startWith(existing) : tap(it => state.set(key, it)) - ); - } + const doc = database.object('test'); + const key = makeStateKey(doc.query.toString()); + const existing = state.get(key, undefined); + this.testObjectValue$ = doc.valueChanges().pipe( + trace('database'), + existing ? startWith(existing) : tap(it => state.set(key, it)) + ); } ngOnInit(): void { diff --git a/sample/src/app/firestore-offline/firestore-offline.component.spec.ts b/sample/src/app/firestore-offline/firestore-offline.component.spec.ts deleted file mode 100644 index 7c95a29de..000000000 --- a/sample/src/app/firestore-offline/firestore-offline.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { FirestoreOfflineComponent } from './firestore-offline.component'; - -describe('FirestoreComponent', () => { - let component: FirestoreOfflineComponent; - let fixture: ComponentFixture; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [ FirestoreOfflineComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(FirestoreOfflineComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/sample/src/app/firestore-offline/firestore-offline.component.ts b/sample/src/app/firestore-offline/firestore-offline.component.ts deleted file mode 100644 index cb21df1fd..000000000 --- a/sample/src/app/firestore-offline/firestore-offline.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { AngularFirestore } from '@angular/fire/firestore'; -import { Observable } from 'rxjs'; -import { startWith, tap } from 'rxjs/operators'; -import { makeStateKey, TransferState } from '@angular/platform-browser'; -import { trace } from '@angular/fire/performance'; -import { AngularFirestoreOffline } from './firestore-offline.module'; - -@Component({ - selector: 'app-firestore-offline', - template: `

- Firestore Offline! - {{ testDocValue$ | async | json }} - {{ persistenceEnabled$ | async }} -

`, - styles: [``] -}) -export class FirestoreOfflineComponent implements OnInit { - - public readonly persistenceEnabled$: Observable; - public readonly testDocValue$: Observable; - - constructor(state: TransferState, firestore: AngularFirestoreOffline) { - const doc = firestore.doc('test/1'); - const key = makeStateKey(doc.ref.path); - const existing = state.get(key, undefined); - this.testDocValue$ = firestore.doc('test/1').valueChanges().pipe( - trace('firestore'), - existing ? startWith(existing) : tap(it => state.set(key, it)) - ); - this.persistenceEnabled$ = firestore.persistenceEnabled$; - } - - ngOnInit(): void { - } - -} diff --git a/sample/src/app/firestore-offline/firestore-offline.module.ts b/sample/src/app/firestore-offline/firestore-offline.module.ts deleted file mode 100644 index 7bd3bc678..000000000 --- a/sample/src/app/firestore-offline/firestore-offline.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Inject, Injectable, InjectionToken, NgModule, NgZone, Optional, PLATFORM_ID } from '@angular/core'; -import { FirebaseOptions, FIREBASE_OPTIONS } from '@angular/fire'; -import { USE_EMULATOR } from '@angular/fire/firestore'; -import { AngularFirestore, SETTINGS, Settings } from '@angular/fire/firestore'; -import { USE_EMULATOR as USE_AUTH_EMULATOR } from '@angular/fire/auth'; - -export const FIRESTORE_OFFLINE = new InjectionToken('my.firestore'); - -@Injectable() -export class AngularFirestoreOffline extends AngularFirestore { - constructor( - @Inject(FIREBASE_OPTIONS) options: FirebaseOptions, - @Optional() @Inject(SETTINGS) settings: Settings | null, - // tslint:disable-next-line:ban-types - @Inject(PLATFORM_ID) platformId: Object, - zone: NgZone, - @Optional() @Inject(USE_EMULATOR) useEmulator: any, - @Optional() @Inject(USE_AUTH_EMULATOR) useAuthEmulator: any, - ) { - super(options, 'offline', true, settings, platformId, zone, { synchronizeTabs: true }, useEmulator, useAuthEmulator); - } -} - -@NgModule({ - providers: [ AngularFirestoreOffline ] -}) export class FirestoreOfflineModule { - -} diff --git a/sample/src/app/firestore/firestore.component.ts b/sample/src/app/firestore/firestore.component.ts index d01e0cf1c..52206e802 100644 --- a/sample/src/app/firestore/firestore.component.ts +++ b/sample/src/app/firestore/firestore.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { AngularFirestore } from '@angular/fire/firestore'; +import { AngularFirestore } from '../../firestore'; import { Observable } from 'rxjs'; import { startWith, tap } from 'rxjs/operators'; import { makeStateKey, TransferState } from '@angular/platform-browser'; @@ -10,24 +10,21 @@ import { trace } from '@angular/fire/performance'; template: `

Firestore! {{ testDocValue$ | async | json }} - {{ persistenceEnabled$ | async }}

`, styles: [``] }) export class FirestoreComponent implements OnInit { - public readonly persistenceEnabled$: Observable; public readonly testDocValue$: Observable; constructor(state: TransferState, firestore: AngularFirestore) { const doc = firestore.doc('test/1'); - const key = makeStateKey(doc.ref.path); + const key = makeStateKey('test/1'); const existing = state.get(key, undefined); - this.testDocValue$ = firestore.doc('test/1').valueChanges().pipe( + this.testDocValue$ = doc.valueChanges().pipe( trace('firestore'), existing ? startWith(existing) : tap(it => state.set(key, it)) ); - this.persistenceEnabled$ = firestore.persistenceEnabled$; } ngOnInit(): void { diff --git a/sample/src/app/home/home.component.ts b/sample/src/app/home/home.component.ts index 3f580e958..79f5ab976 100644 --- a/sample/src/app/home/home.component.ts +++ b/sample/src/app/home/home.component.ts @@ -8,7 +8,6 @@ import { FirebaseApp } from '@angular/fire'; {{ firebaseApp.name }} - diff --git a/sample/src/app/protected-lazy/protected-lazy.component.ts b/sample/src/app/protected-lazy/protected-lazy.component.ts index f84ce2d59..9563566fb 100644 --- a/sample/src/app/protected-lazy/protected-lazy.component.ts +++ b/sample/src/app/protected-lazy/protected-lazy.component.ts @@ -1,7 +1,6 @@ import { Component, OnInit } from '@angular/core'; -import { DocumentChangeAction } from '@angular/fire/firestore'; +import { AngularFirestore, DocumentChangeAction } from '../../firestore'; import { Observable } from 'rxjs'; -import { AngularFirestoreOffline } from '../firestore-offline/firestore-offline.module'; @Component({ selector: 'app-protected-lazy', @@ -12,7 +11,7 @@ export class ProtectedLazyComponent implements OnInit { public snapshot: Observable[]>; - constructor(private afs: AngularFirestoreOffline) { + constructor(private afs: AngularFirestore) { this.snapshot = afs.collection('test').snapshotChanges(); } diff --git a/sample/src/app/storage/storage.component.ts b/sample/src/app/storage/storage.component.ts index 2dc184ef1..5f8706a14 100644 --- a/sample/src/app/storage/storage.component.ts +++ b/sample/src/app/storage/storage.component.ts @@ -1,37 +1,19 @@ import { Component, OnInit } from '@angular/core'; -import { AngularFireStorage } from '@angular/fire/storage'; -import { Observable, of } from 'rxjs'; -import { startWith, tap } from 'rxjs/operators'; -import { makeStateKey, TransferState } from '@angular/platform-browser'; -import { trace } from '@angular/fire/performance'; - -const TRANSPARENT_PNG - = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; @Component({ selector: 'app-storage', template: `

Storage! - -
{{ 'google-g.png' | getDownloadURL | json }} +

`, styles: [] }) export class StorageComponent implements OnInit { - public readonly downloadUrl$: Observable; - constructor(storage: AngularFireStorage, state: TransferState) { - const icon = storage.ref('google-g.png'); - const key = makeStateKey('google-icon-url'); - const existing = state.get(key, undefined); - this.downloadUrl$ = existing ? of(existing) : icon.getDownloadURL().pipe( - trace('storage'), - tap(it => state.set(key, it)), - startWith(TRANSPARENT_PNG) - ); + constructor() { } ngOnInit(): void { diff --git a/sample/src/app/upboats/upboats.component.ts b/sample/src/app/upboats/upboats.component.ts index 4fa023171..daec0227d 100644 --- a/sample/src/app/upboats/upboats.component.ts +++ b/sample/src/app/upboats/upboats.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { map, startWith, tap } from 'rxjs/operators'; -import { AngularFirestoreOffline } from '../firestore-offline/firestore-offline.module'; import firebase from 'firebase/app'; import { makeStateKey, TransferState } from '@angular/platform-browser'; import { trace } from '@angular/fire/performance'; +import { AngularFirestore } from '../../firestore'; type Animal = { name: string, upboats: number, id: string, hasPendingWrites: boolean }; @@ -17,11 +17,11 @@ export class UpboatsComponent implements OnInit { public animals: Observable; - constructor(private firestore: AngularFirestoreOffline, state: TransferState) { + constructor(private firestore: AngularFirestore, state: TransferState) { const collection = firestore.collection('animals', ref => ref.orderBy('upboats', 'desc').orderBy('updatedAt', 'desc') ); - const key = makeStateKey(collection.ref.path); + const key = makeStateKey('animals'); const existing = state.get(key, undefined); this.animals = collection.snapshotChanges().pipe( trace('animals'), diff --git a/sample/src/firestore.server.ts b/sample/src/firestore.server.ts new file mode 100644 index 000000000..d9d0cf949 --- /dev/null +++ b/sample/src/firestore.server.ts @@ -0,0 +1 @@ +export * from '@angular/fire/firestore-lazy/memory'; diff --git a/sample/src/firestore.ts b/sample/src/firestore.ts new file mode 100644 index 000000000..ddee491e4 --- /dev/null +++ b/sample/src/firestore.ts @@ -0,0 +1 @@ +export * from '@angular/fire/firestore-lazy'; diff --git a/sample/src/main.ts b/sample/src/main.ts index ebf5fc9a6..852cd0e2c 100644 --- a/sample/src/main.ts +++ b/sample/src/main.ts @@ -1,7 +1,7 @@ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppModule } from './app/app.module'; +import { AppBrowserModule } from './app/app.browser.module'; import { environment } from './environments/environment'; if (environment.production) { @@ -9,6 +9,6 @@ if (environment.production) { } document.addEventListener('DOMContentLoaded', () => { - platformBrowserDynamic().bootstrapModule(AppModule) + platformBrowserDynamic().bootstrapModule(AppBrowserModule) .catch(err => console.error(err)); }); diff --git a/sample/tsconfig.app.json b/sample/tsconfig.app.json index 23dfeb122..9ee7ccfe4 100644 --- a/sample/tsconfig.app.json +++ b/sample/tsconfig.app.json @@ -7,7 +7,8 @@ }, "files": [ "src/main.ts", - "src/polyfills.ts" + "src/polyfills.ts", + "src/firestore.ts" ], "include": [ "src/**/*.d.ts" diff --git a/sample/yarn.lock b/sample/yarn.lock index 628f4959b..9dda32cba 100644 --- a/sample/yarn.lock +++ b/sample/yarn.lock @@ -1771,7 +1771,7 @@ dependencies: "@types/node" "*" -"@types/express-serve-static-core@*": +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.14": version "4.17.14" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.14.tgz#cabf91debeeb3cb04b798e2cff908864e89b6106" integrity sha512-uFTLwu94TfUFMToXNgRZikwPuZdOtDgs3syBtAIr/OXorL1kJqUJT9qCLnRZ5KBOWfZQikQ2xKgR2tnDj1OgDA== @@ -1789,6 +1789,16 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" +"@types/express@^4.17.9": + version "4.17.9" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.9.tgz#f5f2df6add703ff28428add52bdec8a1091b0a78" + integrity sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/fs-extra@^8.0.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" @@ -9693,6 +9703,11 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.3 source-map "^0.6.1" supports-color "^6.1.0" +preboot@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/preboot/-/preboot-7.0.0.tgz#a06e9ec5d60b0f0a0143981e89983173510459b9" + integrity sha512-moGFdwpdY91Hr7L1OdwpMWPLwJmijUdynZkP7zJzpIgTqCShisVVU5AMVHLdetu1EvOpMo9Hy7WPMH1mDk19hQ== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" diff --git a/src/database-lazy/database.module.ts b/src/database-lazy/database.module.ts new file mode 100644 index 000000000..ea183f8ec --- /dev/null +++ b/src/database-lazy/database.module.ts @@ -0,0 +1,7 @@ +import { NgModule } from '@angular/core'; +import { AngularFireDatabase } from './database'; + +@NgModule({ + providers: [ AngularFireDatabase ] +}) +export class AngularFireDatabaseModule { } diff --git a/src/database-lazy/database.spec.ts b/src/database-lazy/database.spec.ts new file mode 100644 index 000000000..4399151e0 --- /dev/null +++ b/src/database-lazy/database.spec.ts @@ -0,0 +1,121 @@ +import { AngularFireModule, FIREBASE_APP_NAME, FIREBASE_OPTIONS, FirebaseApp } from '@angular/fire'; +import { AngularFireDatabase, AngularFireDatabaseModule, URL } from './public_api'; +import { TestBed } from '@angular/core/testing'; +import { COMMON_CONFIG } from '../test-config'; +import { NgZone } from '@angular/core'; +import 'firebase/database'; +import { rando } from '../firestore/utils.spec'; + +describe('AngularFireLazyDatabase', () => { + let app: FirebaseApp; + let db: AngularFireDatabase; + let zone: NgZone; + let firebaseAppName: string; + + beforeEach(() => { + firebaseAppName = rando(); + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, firebaseAppName), + AngularFireDatabaseModule + ], + providers: [ + { provide: URL, useValue: 'http://localhost:9000' } + ] + }); + + app = TestBed.inject(FirebaseApp); + db = TestBed.inject(AngularFireDatabase); + zone = TestBed.inject(NgZone); + }); + + afterEach(() => { + app.delete(); + }); + + describe('', () => { + + it('should be an AngularFireDatabase type', () => { + expect(db instanceof AngularFireDatabase).toEqual(true); + }); + + it('should have an initialized Firebase app', () => { + expect(db.database.app).toBeDefined(); + }); + + it('should accept a Firebase App in the constructor', (done) => { + const database = new AngularFireDatabase(app.options, rando(), undefined, {}, zone, undefined, undefined); + expect(database instanceof AngularFireDatabase).toEqual(true); + database.database.app.delete().then(done, done); + }); + + it('should have an initialized Firebase app instance member', () => { + expect(db.database.app.name).toEqual(firebaseAppName); + }); + + }); + +}); + +describe('AngularFireLazyDatabase w/options', () => { + let app: FirebaseApp; + let db: AngularFireDatabase; + let firebaseAppName: string; + let url: string; + let query: string; + + beforeEach(() => { + query = rando(); + firebaseAppName = rando(); + url = `http://localhost:${Math.floor(Math.random() * 9999)}`; + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFireDatabaseModule + ], + providers: [ + { provide: FIREBASE_APP_NAME, useValue: firebaseAppName }, + { provide: FIREBASE_OPTIONS, useValue: COMMON_CONFIG }, + { provide: URL, useValue: url } + ] + }); + + app = TestBed.inject(FirebaseApp); + db = TestBed.inject(AngularFireDatabase); + }); + + afterEach(() => { + app.delete(); + }); + + describe('', () => { + + it('should be an AngularFireDatabase type', () => { + expect(db instanceof AngularFireDatabase).toEqual(true); + }); + + it('should have an initialized Firebase app', () => { + expect(db.database.app).toBeDefined(); + }); + + it('should have an initialized Firebase app instance member', () => { + expect(db.database.app.name).toEqual(firebaseAppName); + }); + + /* INVESTIGATE database(url) does not seem to be working + + it('database be pointing to the provided DB instance', () => { + expect(db.database.ref().toString()).toEqual(url); + }); + + it('list should be using the provided DB instance', () => { + expect(db.list(query).query.toString()).toEqual(`${url}/${query}`); + }); + + it('object should be using the provided DB instance', () => { + expect(db.object(query).query.toString()).toEqual(`${url}/${query}`); + }); + */ + }); + +}); diff --git a/src/database-lazy/database.ts b/src/database-lazy/database.ts new file mode 100644 index 000000000..1567e4271 --- /dev/null +++ b/src/database-lazy/database.ts @@ -0,0 +1,118 @@ +import { Inject, Injectable, InjectionToken, NgZone, Optional, PLATFORM_ID } from '@angular/core'; +import { AngularFireList, AngularFireObject, PathReference, QueryFn } from './interfaces'; +import { getRef } from './utils'; +import { createListReference } from './list/create-reference'; +import { createObjectReference } from './object/create-reference'; +import { + FIREBASE_APP_NAME, + FIREBASE_OPTIONS, + FirebaseAppConfig, + FirebaseOptions, + ɵAngularFireSchedulers, + ɵfirebaseAppFactory, + ɵkeepUnstableUntilFirstFactory, + ɵlazySDKProxy, + ɵapplyMixins, + ɵPromiseProxy, +} from '@angular/fire'; +import { Observable, of } from 'rxjs'; +import { USE_EMULATOR as USE_AUTH_EMULATOR } from '@angular/fire/auth'; +import { ɵfetchInstance } from '@angular/fire'; +import { map, observeOn, switchMap } from 'rxjs/operators'; +import { proxyPolyfillCompat } from './base'; +import firebase from 'firebase/app'; + +export const URL = new InjectionToken('angularfire2.realtimeDatabaseURL'); + +// SEMVER(7): use Parameters to detirmine the useEmulator arguments +// TODO(jamesdaniels): don't hardcode, but having tyepscript issues with firebase.database.Database +// type UseEmulatorArguments = Parameters; +type UseEmulatorArguments = [string, number]; +export const USE_EMULATOR = new InjectionToken('angularfire2.database.use-emulator'); + + +export interface AngularFireDatabase extends ɵPromiseProxy {} + +@Injectable({ + providedIn: 'any' +}) +export class AngularFireDatabase { + + public readonly schedulers: ɵAngularFireSchedulers; + public readonly keepUnstableUntilFirst: (obs$: Observable) => Observable; + + public readonly list: (pathOrRef: PathReference, queryFn?: QueryFn) => AngularFireList; + public readonly object: (pathOrRef: PathReference) => AngularFireObject; + public readonly createPushId: () => Promise; + + constructor( + @Inject(FIREBASE_OPTIONS) options: FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig: string | FirebaseAppConfig | null | undefined, + @Optional() @Inject(URL) databaseURL: string | null, + // tslint:disable-next-line:ban-types + @Inject(PLATFORM_ID) platformId: Object, + zone: NgZone, + @Optional() @Inject(USE_EMULATOR) _useEmulator: any, // tuple isn't working here + @Optional() @Inject(USE_AUTH_EMULATOR) useAuthEmulator: any, + ) { + this.schedulers = new ɵAngularFireSchedulers(zone); + this.keepUnstableUntilFirst = ɵkeepUnstableUntilFirstFactory(this.schedulers); + + const useEmulator: UseEmulatorArguments | null = _useEmulator; + + const database$ = of(undefined).pipe( + observeOn(this.schedulers.outsideAngular), + switchMap(() => zone.runOutsideAngular(() => import('firebase/database'))), + map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)), + map(app => + ɵfetchInstance(`${app.name}.database.${databaseURL}`, 'AngularFireDatabase', app, () => { + const database = zone.runOutsideAngular(() => app.database(databaseURL || undefined)); + if (useEmulator) { + database.useEmulator(...useEmulator); + } + return database; + }, [useEmulator]) + ) + ); + + this.list = (pathOrRef: PathReference, queryFn: QueryFn = ((fn) => fn)) => { + const query$ = database$.pipe(map(database => { + const ref = this.schedulers.ngZone.runOutsideAngular(() => getRef(database, pathOrRef)); + return queryFn(ref); + })); + return createListReference(query$, this); + }; + + this.object = (pathOrRef: PathReference) => { + const ref$ = database$.pipe(map(database => + this.schedulers.ngZone.runOutsideAngular(() => getRef(database, pathOrRef)) + )); + return createObjectReference(ref$, this); + }; + + this.createPushId = () => database$.pipe( + map(database => this.schedulers.ngZone.runOutsideAngular(() => database.ref())), + map(ref => ref.push().key), + ).toPromise(); + + return ɵlazySDKProxy(this, database$, zone); + + } + +} + +ɵapplyMixins(AngularFireDatabase, [proxyPolyfillCompat]); + + +export { + PathReference, + DatabaseSnapshot, + ChildEvent, + ListenEvent, + QueryFn, + AngularFireList, + AngularFireObject, + AngularFireAction, + Action, + SnapshotAction +} from './interfaces'; diff --git a/src/database-lazy/interfaces.ts b/src/database-lazy/interfaces.ts new file mode 100644 index 000000000..d7a85b93b --- /dev/null +++ b/src/database-lazy/interfaces.ts @@ -0,0 +1,71 @@ +import { Observable } from 'rxjs'; +import firebase from 'firebase/app'; + +export type FirebaseOperation = string | firebase.database.Reference | firebase.database.DataSnapshot; + +export interface AngularFireList { + query: Promise; + valueChanges(events?: ChildEvent[], options?: {}): Observable; + valueChanges(events?: ChildEvent[], options?: {idField: K}): Observable<(T & {[T in K]?: string})[]>; + snapshotChanges(events?: ChildEvent[]): Observable[]>; + stateChanges(events?: ChildEvent[]): Observable>; + auditTrail(events?: ChildEvent[]): Observable[]>; + update(item: FirebaseOperation, data: Partial): Promise; + set(item: FirebaseOperation, data: T): Promise; + push(data: T): Observable; + remove(item?: FirebaseOperation): Promise; +} + +export interface AngularFireObject { + query: Promise; + valueChanges(): Observable; + snapshotChanges(): Observable>; + update(data: Partial): Promise; + set(data: T): Promise; + remove(): Promise; +} + +export interface FirebaseOperationCases { + stringCase: () => Promise; + firebaseCase?: () => Promise; + snapshotCase?: () => Promise; + unwrappedSnapshotCase?: () => Promise; +} + +export type QueryFn = (ref: DatabaseReference) => DatabaseQuery; +export type ChildEvent = 'child_added' | 'child_removed' | 'child_changed' | 'child_moved'; +export type ListenEvent = 'value' | ChildEvent; + +export interface Action { + type: ListenEvent; + payload: T; +} + +export interface AngularFireAction extends Action { + prevKey: string | null | undefined; + key: string | null; +} + +export type SnapshotAction = AngularFireAction>; + +export type Primitive = number | string | boolean; + +export interface DatabaseSnapshotExists extends firebase.database.DataSnapshot { + exists(): true; + val(): T; + forEach(action: (a: DatabaseSnapshot) => boolean): boolean; +} + +export interface DatabaseSnapshotDoesNotExist extends firebase.database.DataSnapshot { + exists(): false; + val(): null; + forEach(action: (a: DatabaseSnapshot) => boolean): boolean; +} + +export type DatabaseSnapshot = DatabaseSnapshotExists | DatabaseSnapshotDoesNotExist; + +export type DatabaseReference = firebase.database.Reference; +export type DatabaseQuery = firebase.database.Query; +export type DataSnapshot = firebase.database.DataSnapshot; +export type QueryReference = DatabaseReference | DatabaseQuery; +export type PathReference = QueryReference | string; diff --git a/src/database-lazy/list/audit-trail.spec.ts b/src/database-lazy/list/audit-trail.spec.ts new file mode 100644 index 000000000..aa9d86c2a --- /dev/null +++ b/src/database-lazy/list/audit-trail.spec.ts @@ -0,0 +1,64 @@ +import { DatabaseReference } from '../interfaces'; +import { AngularFireModule, FirebaseApp } from '@angular/fire'; +import { AngularFireDatabase, AngularFireDatabaseModule, auditTrail, ChildEvent, URL } from '../public_api'; +import { TestBed } from '@angular/core/testing'; +import { COMMON_CONFIG } from '../../test-config'; +import { skip } from 'rxjs/operators'; +import 'firebase/database'; +import { rando } from '../../firestore/utils.spec'; + +describe('lazy auditTrail', () => { + let app: FirebaseApp; + let db: AngularFireDatabase; + let createRef: (path: string) => DatabaseReference; + let batch = {}; + const items = [{ name: 'zero' }, { name: 'one' }, { name: 'two' }].map((item, i) => ({ key: i.toString(), ...item })); + Object.keys(items).forEach((key, i) => { + batch[i] = items[key]; + }); + // make batch immutable to preserve integrity + batch = Object.freeze(batch); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFireDatabaseModule + ], + providers: [ + { provide: URL, useValue: 'http://localhost:9000' } + ] + }); + + app = TestBed.inject(FirebaseApp); + db = TestBed.inject(AngularFireDatabase); + createRef = (path: string) => db.database.ref(path); + }); + + afterEach(() => { + app.delete(); + }); + + function prepareAuditTrail(opts: { events?: ChildEvent[], skipnumber: number } = { skipnumber: 0 }) { + const { events, skipnumber } = opts; + const aref = createRef(rando()); + aref.set(batch); + const changes = auditTrail(aref, events); + return { + changes: changes.pipe(skip(skipnumber)), + ref: aref + }; + } + + it('should listen to all events by default', (done) => { + + const { changes } = prepareAuditTrail(); + changes.subscribe(actions => { + const data = actions.map(a => a.payload.val()); + expect(data).toEqual(items); + done(); + }); + + }); + +}); diff --git a/src/database-lazy/list/audit-trail.ts b/src/database-lazy/list/audit-trail.ts new file mode 100644 index 000000000..1ead7342b --- /dev/null +++ b/src/database-lazy/list/audit-trail.ts @@ -0,0 +1,62 @@ +import { AngularFireAction, ChildEvent, DatabaseQuery, DataSnapshot, SnapshotAction } from '../interfaces'; +import { stateChanges } from './state-changes'; +import { Observable, SchedulerLike } from 'rxjs'; +import { fromRef } from '../observable/fromRef'; + +import { map, scan, skipWhile, withLatestFrom } from 'rxjs/operators'; + +export function auditTrail(query: DatabaseQuery, events?: ChildEvent[], scheduler?: SchedulerLike): Observable[]> { + const auditTrail$ = stateChanges(query, events) + .pipe( + scan((current, action) => [...current, action], []) + ); + return waitForLoaded(query, auditTrail$, scheduler); +} + +interface LoadedMetadata { + data: AngularFireAction; + lastKeyToLoad: any; +} + +function loadedData(query: DatabaseQuery, scheduler?: SchedulerLike): Observable { + // Create an observable of loaded values to retrieve the + // known dataset. This will allow us to know what key to + // emit the "whole" array at when listening for child events. + return fromRef(query, 'value', 'on', scheduler) + .pipe( + map(data => { + // Store the last key in the data set + let lastKeyToLoad; + // Loop through loaded dataset to find the last key + data.payload.forEach(child => { + lastKeyToLoad = child.key; return false; + }); + // return data set and the current last key loaded + return { data, lastKeyToLoad }; + }) + ); +} + +function waitForLoaded(query: DatabaseQuery, action$: Observable[]>, scheduler?: SchedulerLike) { + const loaded$ = loadedData(query, scheduler); + return loaded$ + .pipe( + withLatestFrom(action$), + // Get the latest values from the "loaded" and "child" datasets + // We can use both datasets to form an array of the latest values. + map(([loaded, actions]) => { + // Store the last key in the data set + const lastKeyToLoad = loaded.lastKeyToLoad; + // Store all child keys loaded at this point + const loadedKeys = actions.map(snap => snap.key); + return { actions, lastKeyToLoad, loadedKeys }; + }), + // This is the magical part, only emit when the last load key + // in the dataset has been loaded by a child event. At this point + // we can assume the dataset is "whole". + skipWhile(meta => meta.loadedKeys.indexOf(meta.lastKeyToLoad) === -1), + // Pluck off the meta data because the user only cares + // to iterate through the snapshots + map(meta => meta.actions) + ); +} diff --git a/src/database-lazy/list/changes.spec.ts b/src/database-lazy/list/changes.spec.ts new file mode 100644 index 000000000..e0df8f161 --- /dev/null +++ b/src/database-lazy/list/changes.spec.ts @@ -0,0 +1,146 @@ +import firebase from 'firebase/app'; +import { AngularFireModule, FirebaseApp } from '@angular/fire'; +import { AngularFireDatabase, AngularFireDatabaseModule, listChanges, URL } from '../public_api'; +import { TestBed } from '@angular/core/testing'; +import { COMMON_CONFIG } from '../../test-config'; +import { skip, take } from 'rxjs/operators'; +import 'firebase/database'; +import { rando } from '../../firestore/utils.spec'; + +describe('lazy listChanges', () => { + let app: FirebaseApp; + let db: AngularFireDatabase; + let ref: (path: string) => firebase.database.Reference; + let batch = {}; + const items = [{ name: 'zero' }, { name: 'one' }, { name: 'two' }].map((item, i) => ({ key: i.toString(), ...item })); + Object.keys(items).forEach((key, i) => { + batch[i] = items[key]; + }); + // make batch immutable to preserve integrity + batch = Object.freeze(batch); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFireDatabaseModule + ], + providers: [ + { provide: URL, useValue: 'http://localhost:9000' } + ] + }); + + app = TestBed.inject(FirebaseApp); + db = TestBed.inject(AngularFireDatabase); + ref = (path: string) => db.database.ref(path); + }); + + afterEach(() => { + app.delete(); + }); + + describe('events', () => { + + it('should stream value at first', (done) => { + const someRef = ref(rando()); + const obs = listChanges(someRef, ['child_added']); + obs.pipe(take(1)).subscribe(changes => { + const data = changes.map(change => change.payload.val()); + expect(data).toEqual(items); + }).add(done); + someRef.set(batch); + }); + + it('should process a new child_added event', done => { + const aref = ref(rando()); + const obs = listChanges(aref, ['child_added']); + obs.pipe(skip(1), take(1)).subscribe(changes => { + const data = changes.map(change => change.payload.val()); + expect(data[3]).toEqual({ name: 'anotha one' }); + }).add(done); + aref.set(batch); + aref.push({ name: 'anotha one' }); + }); + + it('should stream in order events', (done) => { + const aref = ref(rando()); + const obs = listChanges(aref.orderByChild('name'), ['child_added']); + obs.pipe(take(1)).subscribe(changes => { + const names = changes.map(change => change.payload.val().name); + expect(names[0]).toEqual('one'); + expect(names[1]).toEqual('two'); + expect(names[2]).toEqual('zero'); + }).add(done); + aref.set(batch); + }); + + it('should stream in order events w/child_added', (done) => { + const aref = ref(rando()); + const obs = listChanges(aref.orderByChild('name'), ['child_added']); + obs.pipe(skip(1), take(1)).subscribe(changes => { + const names = changes.map(change => change.payload.val().name); + expect(names[0]).toEqual('anotha one'); + expect(names[1]).toEqual('one'); + expect(names[2]).toEqual('two'); + expect(names[3]).toEqual('zero'); + }).add(done); + aref.set(batch); + aref.push({ name: 'anotha one' }); + }); + + it('should stream events filtering', (done) => { + const aref = ref(rando()); + const obs = listChanges(aref.orderByChild('name').equalTo('zero'), ['child_added']); + obs.pipe(skip(1), take(1)).subscribe(changes => { + const names = changes.map(change => change.payload.val().name); + expect(names[0]).toEqual('zero'); + expect(names[1]).toEqual('zero'); + }).add(done); + aref.set(batch); + aref.push({ name: 'zero' }); + }); + + + /* FLAKES? aref.set not fufilling + + it('should process a new child_removed event', done => { + const aref = ref(rando()); + const obs = listChanges(aref, ['child_added','child_removed']); + aref.set(batch).then(() => { + const sub = obs.pipe(skip(1),take(1)).subscribe(changes => { + const data = changes.map(change => change.payload.val()); + expect(data.length).toEqual(items.length - 1); + }).add(done); + aref.child(items[0].key).remove(); + }); + }); + + it('should process a new child_changed event', (done) => { + const aref = ref(rando()); + const obs = listChanges(aref, ['child_added','child_changed']) + aref.set(batch).then(() => { + const sub = obs.pipe(skip(1),take(1)).subscribe(changes => { + const data = changes.map(change => change.payload.val()); + expect(data[1].name).toEqual('lol'); + }).add(done); + aref.child(items[1].key).update({ name: 'lol'}); + }); + }); + + it('should process a new child_moved event', (done) => { + const aref = ref(rando()); + const obs = listChanges(aref, ['child_added','child_moved']) + aref.set(batch).then(() => { + const sub = obs.pipe(skip(1),take(1)).subscribe(changes => { + const data = changes.map(change => change.payload.val()); + // We moved the first item to the last item, so we check that + // the new result is now the last result + expect(data[data.length - 1]).toEqual(items[0]); + }).add(done); + aref.child(items[0].key).setPriority('a', () => {}); + }); + });*/ + + }); + +}); diff --git a/src/database-lazy/list/changes.ts b/src/database-lazy/list/changes.ts new file mode 100644 index 000000000..cb37d0694 --- /dev/null +++ b/src/database-lazy/list/changes.ts @@ -0,0 +1,90 @@ +import { fromRef } from '../observable/fromRef'; +import { merge, Observable, of, SchedulerLike } from 'rxjs'; + +import { ChildEvent, DatabaseQuery, SnapshotAction } from '../interfaces'; +import { isNil } from '../utils'; + +import { distinctUntilChanged, scan, switchMap } from 'rxjs/operators'; + +export function listChanges(ref: DatabaseQuery, events: ChildEvent[], scheduler?: SchedulerLike): Observable[]> { + return fromRef(ref, 'value', 'once', scheduler).pipe( + switchMap(snapshotAction => { + const childEvent$ = [of(snapshotAction)]; + events.forEach(event => childEvent$.push(fromRef(ref, event, 'on', scheduler))); + return merge(...childEvent$).pipe(scan(buildView, [])); + }), + distinctUntilChanged() + ); +} + +function positionFor(changes: SnapshotAction[], key) { + const len = changes.length; + for (let i = 0; i < len; i++) { + if (changes[i].payload.key === key) { + return i; + } + } + return -1; +} + +function positionAfter(changes: SnapshotAction[], prevKey?: string) { + if (isNil(prevKey)) { + return 0; + } else { + const i = positionFor(changes, prevKey); + if (i === -1) { + return changes.length; + } else { + return i + 1; + } + } +} + +function buildView(current, action) { + const { payload, prevKey, key } = action; + const currentKeyPosition = positionFor(current, key); + const afterPreviousKeyPosition = positionAfter(current, prevKey); + switch (action.type) { + case 'value': + if (action.payload && action.payload.exists()) { + let prevKey = null; + action.payload.forEach(payload => { + const action = { payload, type: 'value', prevKey, key: payload.key }; + prevKey = payload.key; + current = [...current, action]; + return false; + }); + } + return current; + case 'child_added': + if (currentKeyPosition > -1) { + // check that the previouskey is what we expect, else reorder + const previous = current[currentKeyPosition - 1]; + if ((previous && previous.key || null) !== prevKey) { + current = current.filter(x => x.payload.key !== payload.key); + current.splice(afterPreviousKeyPosition, 0, action); + } + } else if (prevKey == null) { + return [action, ...current]; + } else { + current = current.slice(); + current.splice(afterPreviousKeyPosition, 0, action); + } + return current; + case 'child_removed': + return current.filter(x => x.payload.key !== payload.key); + case 'child_changed': + return current.map(x => x.payload.key === key ? action : x); + case 'child_moved': + if (currentKeyPosition > -1) { + const data = current.splice(currentKeyPosition, 1)[0]; + current = current.slice(); + current.splice(afterPreviousKeyPosition, 0, data); + return current; + } + return current; + // default will also remove null results + default: + return current; + } +} diff --git a/src/database-lazy/list/create-reference.ts b/src/database-lazy/list/create-reference.ts new file mode 100644 index 000000000..2dcd1ee14 --- /dev/null +++ b/src/database-lazy/list/create-reference.ts @@ -0,0 +1,54 @@ +import { AngularFireList, ChildEvent, DatabaseQuery } from '../interfaces'; +import { snapshotChanges } from './snapshot-changes'; +import { stateChanges } from './state-changes'; +import { auditTrail } from './audit-trail'; +import { createDataOperationMethod } from './data-operation'; +import { createRemoveMethod } from './remove'; +import { AngularFireDatabase } from '../database'; +import { map, shareReplay, switchMap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +export function createListReference(query$: Observable, afDatabase: AngularFireDatabase): AngularFireList { + const outsideAngularScheduler = afDatabase.schedulers.outsideAngular; + const ref$ = query$.pipe(map(query => afDatabase.schedulers.ngZone.run(() => query.ref))); + return { + query: query$.toPromise(), + update: createDataOperationMethod>(ref$, 'update'), + set: createDataOperationMethod(ref$, 'set'), + push: (data: T) => { + // this should be a hot observable + const obs = ref$.pipe(map(ref => ref.push(data)), shareReplay({ bufferSize: 1, refCount: false })); + obs.subscribe(); + return obs; + }, + remove: createRemoveMethod(ref$), + snapshotChanges: (events?: ChildEvent[]) => query$.pipe( + switchMap(query => snapshotChanges(query, events, outsideAngularScheduler)), + afDatabase.keepUnstableUntilFirst, + ), + stateChanges: (events?: ChildEvent[]) => query$.pipe( + switchMap(query => stateChanges(query, events, outsideAngularScheduler)), + afDatabase.keepUnstableUntilFirst, + ), + auditTrail: (events?: ChildEvent[]) => query$.pipe( + switchMap(query => auditTrail(query, events, outsideAngularScheduler)), + afDatabase.keepUnstableUntilFirst + ), + valueChanges: (events?: ChildEvent[], options?: {idField?: K}) => query$.pipe( + switchMap(query => snapshotChanges(query, events, outsideAngularScheduler)), + map(actions => actions.map(a => { + if (options && options.idField) { + return { + ...a.payload.val() as T, + ...{ + [options.idField]: a.key + } + }; + } else { + return a.payload.val() as T; + } + })), + afDatabase.keepUnstableUntilFirst + ), + }; +} diff --git a/src/database-lazy/list/data-operation.ts b/src/database-lazy/list/data-operation.ts new file mode 100644 index 000000000..93ae54149 --- /dev/null +++ b/src/database-lazy/list/data-operation.ts @@ -0,0 +1,15 @@ +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { DatabaseReference, DatabaseSnapshot, FirebaseOperation } from '../interfaces'; +import { checkOperationCases } from '../utils'; + +export function createDataOperationMethod(ref$: Observable, operation: string) { + return function dataOperation(item: FirebaseOperation, value: T) { + return checkOperationCases(item, { + // TODO fix the typing here, rather than lean on any + stringCase: () => ref$.pipe(switchMap(ref => ref.child(item as string)[operation](value))).toPromise() as any, + firebaseCase: () => (item as DatabaseReference)[operation](value), + snapshotCase: () => (item as DatabaseSnapshot).ref[operation](value) + }); + }; +} diff --git a/src/database-lazy/list/remove.ts b/src/database-lazy/list/remove.ts new file mode 100644 index 000000000..1b61daf89 --- /dev/null +++ b/src/database-lazy/list/remove.ts @@ -0,0 +1,17 @@ +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { DatabaseReference, DatabaseSnapshot, FirebaseOperation } from '../interfaces'; +import { checkOperationCases } from '../utils'; + +// TODO(davideast): Find out why TS thinks this returns firebase.Primise +// instead of Promise. +export function createRemoveMethod(ref$: Observable) { + return function remove(item?: FirebaseOperation): any { + if (!item) { return ref$.pipe(switchMap(ref => ref.remove())).toPromise(); } + return checkOperationCases(item, { + stringCase: () => ref$.pipe(switchMap(ref => ref.child(item as string).remove())).toPromise(), + firebaseCase: () => (item as DatabaseReference).remove(), + snapshotCase: () => (item as DatabaseSnapshot).ref.remove() + }); + }; +} diff --git a/src/database-lazy/list/snapshot-changes.spec.ts b/src/database-lazy/list/snapshot-changes.spec.ts new file mode 100644 index 000000000..4a7aeb30b --- /dev/null +++ b/src/database-lazy/list/snapshot-changes.spec.ts @@ -0,0 +1,142 @@ +import firebase from 'firebase/app'; +import { AngularFireModule, FirebaseApp } from '@angular/fire'; +import { AngularFireDatabase, AngularFireDatabaseModule, ChildEvent, snapshotChanges, URL } from '../public_api'; +import { TestBed } from '@angular/core/testing'; +import { COMMON_CONFIG } from '../../test-config'; +import { BehaviorSubject } from 'rxjs'; +import { skip, switchMap, take } from 'rxjs/operators'; +import 'firebase/database'; +import { rando } from '../../firestore/utils.spec'; + +describe('lazy snapshotChanges', () => { + let app: FirebaseApp; + let db: AngularFireDatabase; + let createRef: (path: string) => firebase.database.Reference; + let batch = {}; + const items = [{ name: 'zero' }, { name: 'one' }, { name: 'two' }].map((item, i) => ({ key: i.toString(), ...item })); + Object.keys(items).forEach((key, i) => { + batch[i] = items[key]; + }); + // make batch immutable to preserve integrity + batch = Object.freeze(batch); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFireDatabaseModule + ], + providers: [ + { provide: URL, useValue: 'http://localhost:9000' } + ] + }); + + app = TestBed.inject(FirebaseApp); + db = TestBed.inject(AngularFireDatabase); + createRef = (path: string) => db.database.ref(path); + }); + + afterEach(() => { + app.delete(); + }); + + function prepareSnapshotChanges(opts: { events?: ChildEvent[], skipnumber: number } = { skipnumber: 0 }) { + const { events, skipnumber } = opts; + const aref = createRef(rando()); + const snapChanges = snapshotChanges(aref, events); + return { + snapChanges: snapChanges.pipe(skip(skipnumber)), + ref: aref + }; + } + + it('should listen to all events by default', (done) => { + const { snapChanges, ref } = prepareSnapshotChanges(); + snapChanges.pipe(take(1)).subscribe(actions => { + const data = actions.map(a => a.payload.val()); + expect(data).toEqual(items); + }).add(done); + ref.set(batch); + }); + + it('should handle multiple subscriptions (hot)', (done) => { + const { snapChanges, ref } = prepareSnapshotChanges(); + const sub = snapChanges.subscribe(() => { + }).add(done); + snapChanges.pipe(take(1)).subscribe(actions => { + const data = actions.map(a => a.payload.val()); + expect(data).toEqual(items); + }).add(sub); + ref.set(batch); + }); + + it('should handle multiple subscriptions (warm)', done => { + const { snapChanges, ref } = prepareSnapshotChanges(); + snapChanges.pipe(take(1)).subscribe(() => { + }).add(() => { + snapChanges.pipe(take(1)).subscribe(actions => { + const data = actions.map(a => a.payload.val()); + expect(data).toEqual(items); + }).add(done); + }); + ref.set(batch); + }); + + it('should listen to only child_added events', (done) => { + const { snapChanges, ref } = prepareSnapshotChanges({ events: ['child_added'], skipnumber: 0 }); + snapChanges.pipe(take(1)).subscribe(actions => { + const data = actions.map(a => a.payload.val()); + expect(data).toEqual(items); + }).add(done); + ref.set(batch); + }); + + /* FLAKE? set promise not fufilling + it('should listen to only child_added, child_changed events', (done) => { + const { snapChanges, ref } = prepareSnapshotChanges({ + events: ['child_added', 'child_changed'], + skipnumber: 1 + }); + const name = 'ligatures'; + snapChanges.pipe(take(1)).subscribe(actions => { + const data = actions.map(a => a.payload!.val());; + const copy = [...items]; + copy[0].name = name; + expect(data).toEqual(copy); + }).add(done); + ref.set(batch).then(() => { + ref.child(items[0].key).update({ name }) + }); + });*/ + + it('should handle empty sets', done => { + const aref = createRef(rando()); + aref.set({}); + snapshotChanges(aref).pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(0); + }).add(done); + }); + + it('should handle dynamic queries that return empty sets', done => { + let count = 0; + const namefilter$ = new BehaviorSubject(null); + const aref = createRef(rando()); + aref.set(batch); + namefilter$.pipe(switchMap(name => { + const filteredRef = name ? aref.child('name').equalTo(name) : aref; + return snapshotChanges(filteredRef); + }), take(2)).subscribe(data => { + count = count + 1; + // the first time should all be 'added' + if (count === 1) { + expect(Object.keys(data).length).toEqual(3); + namefilter$.next(-1); + } + // on the second round, we should have filtered out everything + if (count === 2) { + expect(Object.keys(data).length).toEqual(0); + } + }).add(done); + }); + +}); diff --git a/src/database-lazy/list/snapshot-changes.ts b/src/database-lazy/list/snapshot-changes.ts new file mode 100644 index 000000000..7422c65bd --- /dev/null +++ b/src/database-lazy/list/snapshot-changes.ts @@ -0,0 +1,13 @@ +import { Observable, SchedulerLike } from 'rxjs'; +import { listChanges } from './changes'; +import { ChildEvent, DatabaseQuery, SnapshotAction } from '../interfaces'; +import { validateEventsArray } from './utils'; + +export function snapshotChanges( + query: DatabaseQuery, + events?: ChildEvent[], + scheduler?: SchedulerLike +): Observable[]> { + events = validateEventsArray(events); + return listChanges(query, events, scheduler); +} diff --git a/src/database-lazy/list/state-changes.spec.ts b/src/database-lazy/list/state-changes.spec.ts new file mode 100644 index 000000000..e9d1c9104 --- /dev/null +++ b/src/database-lazy/list/state-changes.spec.ts @@ -0,0 +1,64 @@ +import firebase from 'firebase/app'; +import { AngularFireModule, FirebaseApp } from '@angular/fire'; +import { AngularFireDatabase, AngularFireDatabaseModule, ChildEvent, stateChanges, URL } from '../public_api'; +import { TestBed } from '@angular/core/testing'; +import { COMMON_CONFIG } from '../../test-config'; +import { skip } from 'rxjs/operators'; +import 'firebase/database'; +import { rando } from '../../firestore/utils.spec'; + +describe('lazy stateChanges', () => { + let app: FirebaseApp; + let db: AngularFireDatabase; + let createRef: (path: string) => firebase.database.Reference; + let batch = {}; + const items = [{ name: 'zero' }, { name: 'one' }, { name: 'two' }].map((item, i) => ({ key: i.toString(), ...item })); + Object.keys(items).forEach((key, i) => { + batch[i] = items[key]; + }); + // make batch immutable to preserve integrity + batch = Object.freeze(batch); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFireDatabaseModule + ], + providers: [ + { provide: URL, useValue: 'http://localhost:9000' } + ] + }); + + app = TestBed.inject(FirebaseApp); + db = TestBed.inject(AngularFireDatabase); + createRef = (path: string) => db.database.ref(path); + }); + + afterEach(() => { + app.delete(); + }); + + function prepareStateChanges(opts: { events?: ChildEvent[], skipnumber: number } = { skipnumber: 0 }) { + const { events, skipnumber } = opts; + const aref = createRef(rando()); + aref.set(batch); + const changes = stateChanges(aref, events); + return { + changes: changes.pipe(skip(skipnumber)), + ref: aref + }; + } + + it('should listen to all events by default', (done) => { + + const { changes } = prepareStateChanges({ skipnumber: 2 }); + changes.subscribe(action => { + expect(action.key).toEqual('2'); + expect(action.payload.val()).toEqual(items[items.length - 1]); + done(); + }); + + }); + +}); diff --git a/src/database-lazy/list/state-changes.ts b/src/database-lazy/list/state-changes.ts new file mode 100644 index 000000000..192043e3b --- /dev/null +++ b/src/database-lazy/list/state-changes.ts @@ -0,0 +1,10 @@ +import { ChildEvent, DatabaseQuery } from '../interfaces'; +import { fromRef } from '../observable/fromRef'; +import { validateEventsArray } from './utils'; +import { merge, SchedulerLike } from 'rxjs'; + +export function stateChanges(query: DatabaseQuery, events?: ChildEvent[], scheduler?: SchedulerLike) { + events = validateEventsArray(events); + const childEvent$ = events.map(event => fromRef(query, event, 'on', scheduler)); + return merge(...childEvent$); +} diff --git a/src/database-lazy/list/utils.ts b/src/database-lazy/list/utils.ts new file mode 100644 index 000000000..35404600c --- /dev/null +++ b/src/database-lazy/list/utils.ts @@ -0,0 +1,8 @@ +import { isNil } from '../utils'; + +export function validateEventsArray(events?: any[]) { + if (isNil(events) || events.length === 0) { + events = ['child_added', 'child_removed', 'child_changed', 'child_moved']; + } + return events; +} diff --git a/src/database-lazy/object/create-reference.ts b/src/database-lazy/object/create-reference.ts new file mode 100644 index 000000000..8848cb073 --- /dev/null +++ b/src/database-lazy/object/create-reference.ts @@ -0,0 +1,23 @@ +import { map, switchMap } from 'rxjs/operators'; +import { AngularFireObject, DatabaseQuery } from '../interfaces'; +import { createObjectSnapshotChanges } from './snapshot-changes'; +import { AngularFireDatabase } from '../database'; +import { Observable } from 'rxjs'; + +export function createObjectReference(query$: Observable, afDatabase: AngularFireDatabase): AngularFireObject { + return { + query: query$.toPromise(), + snapshotChanges: () => query$.pipe( + switchMap(query => createObjectSnapshotChanges(query, afDatabase.schedulers.outsideAngular)()), + afDatabase.keepUnstableUntilFirst + ), + update: (data: Partial) => query$.pipe(switchMap(query => query.ref.update(data as any))).toPromise(), + set: (data: T) => query$.pipe(switchMap(query => query.ref.set(data))).toPromise(), + remove: () => query$.pipe(switchMap(query => query.ref.remove())).toPromise(), + valueChanges: () => query$.pipe( + switchMap(query => createObjectSnapshotChanges(query, afDatabase.schedulers.outsideAngular)()), + afDatabase.keepUnstableUntilFirst, + map(action => action.payload.exists() ? action.payload.val() as T : null) + ), + }; +} diff --git a/src/database-lazy/object/snapshot-changes.ts b/src/database-lazy/object/snapshot-changes.ts new file mode 100644 index 000000000..66ae6ef3c --- /dev/null +++ b/src/database-lazy/object/snapshot-changes.ts @@ -0,0 +1,9 @@ +import { Observable, SchedulerLike } from 'rxjs'; +import { fromRef } from '../observable/fromRef'; +import { DatabaseQuery, SnapshotAction } from '../interfaces'; + +export function createObjectSnapshotChanges(query: DatabaseQuery, scheduler?: SchedulerLike) { + return function snapshotChanges(): Observable> { + return fromRef(query, 'value', 'on', scheduler); + }; +} diff --git a/src/database-lazy/observable/fromRef.spec.ts b/src/database-lazy/observable/fromRef.spec.ts new file mode 100644 index 000000000..c01ac49e1 --- /dev/null +++ b/src/database-lazy/observable/fromRef.spec.ts @@ -0,0 +1,271 @@ +import { DatabaseReference } from '../interfaces'; +import { AngularFireModule, FirebaseApp, ɵZoneScheduler } from '@angular/fire'; +import { AngularFireDatabase, AngularFireDatabaseModule, fromRef } from '../public_api'; +import { TestBed } from '@angular/core/testing'; +import { COMMON_CONFIG } from '../../test-config'; +import { take } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; +import { rando } from '../../firestore/utils.spec'; + +describe('lazy fromRef', () => { + let app: FirebaseApp; + let db: AngularFireDatabase; + let ref: (path: string) => DatabaseReference; + let batch = {}; + const items = [{ name: 'one' }, { name: 'two' }, { name: 'three' }].map(item => ({ key: rando(), ...item })); + Object.keys(items).forEach((key) => { + const itemValue = items[key]; + batch[itemValue.key] = itemValue; + }); + // make batch immutable to preserve integrity + batch = Object.freeze(batch); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFireDatabaseModule + ], + providers: [ + { provide: URL, useValue: 'http://localhost:9000' } + ] + }); + + app = TestBed.inject(FirebaseApp); + db = TestBed.inject(AngularFireDatabase); + ref = (path: string) => db.database.ref(path); + }); + + afterEach(() => { + app.delete(); + }); + + it('it should be async by default', (done) => { + const itemRef = ref(rando()); + itemRef.set(batch); + const obs = fromRef(itemRef, 'value'); + let count = 0; + expect(count).toEqual(0); + const sub = obs.subscribe(() => { + count = count + 1; + expect(count).toEqual(1); + sub.unsubscribe(); + done(); + }); + expect(count).toEqual(0); + }); + + it('should take a scheduler', done => { + const itemRef = ref(rando()); + itemRef.set(batch); + + const testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + spyOn(testScheduler, 'schedule').and.callThrough(); + + const obs = fromRef(itemRef, 'value', 'once', testScheduler); + expect(testScheduler.schedule).not.toHaveBeenCalled(); + + obs.subscribe(() => { + expect(testScheduler.schedule).toHaveBeenCalled(); + done(); + }, err => { + console.error(err); + expect(false).toEqual(true, 'Shouldnt error'); + done(); + }, () => { + expect(testScheduler.schedule).toHaveBeenCalled(); + done(); + }); + testScheduler.flush(); + }); + + it('should schedule completed and error correctly', done => { + const testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + spyOn(testScheduler, 'schedule').and.callThrough(); + + // Error + const errorObservable = fromRef({ + once: (event, snap, err) => err() + } as any, + 'value', + 'once', + testScheduler + ); + errorObservable.subscribe(() => { + fail('Should not emit'); + }, () => { + expect(testScheduler.schedule).toHaveBeenCalled(); + }, () => { + fail('Should not complete'); + }); + + testScheduler.flush(); + + // Completed + const itemRef = ref(rando()); + itemRef.set(batch); + + const scheduler = new ɵZoneScheduler(Zone.current.fork({ + name: 'ExpectedZone' + })); + const completeObservable = fromRef( + itemRef, + 'value', + 'once', + scheduler + ); + completeObservable.subscribe( + () => { + }, + () => fail('Should not error'), + () => expect(Zone.current.name).toEqual('ExpectedZone') + ); + testScheduler.flush(); + done(); + }); + + + it('it should should handle non-existence', (done) => { + const itemRef = ref(rando()); + itemRef.set({}); + const obs = fromRef(itemRef, 'value'); + obs.pipe(take(1)).subscribe(change => { + expect(change.payload.exists()).toEqual(false); + expect(change.payload.val()).toEqual(null); + }).add(done); + }); + + it('once should complete', (done) => { + const itemRef = ref(rando()); + itemRef.set(batch); + const obs = fromRef(itemRef, 'value', 'once'); + obs.subscribe(() => { + }, () => { + }, done); + }); + + it('it should listen and then unsubscribe', (done) => { + const itemRef = ref(rando()); + itemRef.set(batch); + const obs = fromRef(itemRef, 'value'); + let count = 0; + const sub = obs.subscribe(() => { + count = count + 1; + // hard coding count to one will fail if the unsub + // doesn't actually unsub + expect(count).toEqual(1); + done(); + sub.unsubscribe(); + itemRef.push({ name: 'anotha one' }); + }); + }); + + describe('events', () => { + + it('should stream back a child_added event', async (done: any) => { + const itemRef = ref(rando()); + itemRef.set(batch); + const obs = fromRef(itemRef, 'child_added'); + let count = 0; + const sub = obs.subscribe(change => { + count = count + 1; + const { type, payload } = change; + expect(type).toEqual('child_added'); + expect(payload.val()).toEqual(batch[payload.key]); + if (count === items.length) { + done(); + sub.unsubscribe(); + expect(sub.closed).toEqual(true); + } + }); + }); + + it('should stream back a child_changed event', async (done: any) => { + const itemRef = ref(rando()); + itemRef.set(batch); + const obs = fromRef(itemRef, 'child_changed'); + const name = 'look at what you made me do'; + const key = items[0].key; + const sub = obs.subscribe(change => { + const { type, payload } = change; + expect(type).toEqual('child_changed'); + expect(payload.key).toEqual(key); + expect(payload.val()).toEqual({ key, name }); + sub.unsubscribe(); + done(); + }); + itemRef.child(key).update({ name }); + }); + + it('should stream back a child_removed event', async (done: any) => { + const itemRef = ref(rando()); + itemRef.set(batch); + const obs = fromRef(itemRef, 'child_removed'); + const key = items[0].key; + const name = items[0].name; + const sub = obs.subscribe(change => { + const { type, payload } = change; + expect(type).toEqual('child_removed'); + expect(payload.key).toEqual(key); + expect(payload.val()).toEqual({ key, name }); + sub.unsubscribe(); + done(); + }); + itemRef.child(key).remove(); + }); + + it('should stream back a child_moved event', async (done: any) => { + const itemRef = ref(rando()); + itemRef.set(batch); + const obs = fromRef(itemRef, 'child_moved'); + const key = items[2].key; + const name = items[2].name; + const sub = obs.subscribe(change => { + const { type, payload } = change; + expect(type).toEqual('child_moved'); + expect(payload.key).toEqual(key); + expect(payload.val()).toEqual({ key, name }); + sub.unsubscribe(); + done(); + }); + itemRef.child(key).setPriority(-100, () => { + }); + }); + + it('should stream back a value event', (done: any) => { + const itemRef = ref(rando()); + itemRef.set(batch); + const obs = fromRef(itemRef, 'value'); + const sub = obs.subscribe(change => { + const { type, payload } = change; + expect(type).toEqual('value'); + expect(payload.val()).toEqual(batch); + done(); + sub.unsubscribe(); + expect(sub.closed).toEqual(true); + }); + }); + + it('should stream back query results', (done: any) => { + const itemRef = ref(rando()); + itemRef.set(batch); + const query = itemRef.orderByChild('name').equalTo(items[0].name); + const obs = fromRef(query, 'value'); + obs.subscribe(change => { + let child = null; + change.payload.forEach(snap => { + child = snap.val(); + return true; + }); + expect(child).toEqual(items[0]); + done(); + }); + }); + + }); + +}); diff --git a/src/database-lazy/observable/fromRef.ts b/src/database-lazy/observable/fromRef.ts new file mode 100644 index 000000000..d08283235 --- /dev/null +++ b/src/database-lazy/observable/fromRef.ts @@ -0,0 +1,60 @@ +import { AngularFireAction, DatabaseQuery, DatabaseSnapshot, ListenEvent } from '../interfaces'; +import { asyncScheduler, Observable, SchedulerLike } from 'rxjs'; +import { map, share } from 'rxjs/operators'; + +interface SnapshotPrevKey { + snapshot: DatabaseSnapshot; + prevKey: string | null | undefined; +} + +/** + * Create an observable from a Database Reference or Database Query. + * @param ref Database Reference + * @param event Listen event type ('value', 'added', 'changed', 'removed', 'moved') + * @param listenType 'on' or 'once' + * @param scheduler - Rxjs scheduler + */ +export function fromRef(ref: DatabaseQuery, + event: ListenEvent, + listenType = 'on', + scheduler: SchedulerLike = asyncScheduler +): Observable>> { + return new Observable>(subscriber => { + let fn: any | null = null; + fn = ref[listenType](event, (snapshot, prevKey) => { + scheduler.schedule(() => { + subscriber.next({ snapshot, prevKey }); + }); + if (listenType === 'once') { + scheduler.schedule(() => subscriber.complete()); + } + }, err => { + scheduler.schedule(() => subscriber.error(err)); + }); + + if (listenType === 'on') { + return { + unsubscribe() { + if (fn != null) { + ref.off(event, fn); + } + } + }; + } else { + return { + unsubscribe() { + } + }; + } + }).pipe( + map(payload => { + const { snapshot, prevKey } = payload; + let key: string | null = null; + if (snapshot.exists()) { + key = snapshot.key; + } + return { type: event, payload: snapshot, prevKey, key }; + }), + share() + ); +} diff --git a/src/database-lazy/package.json b/src/database-lazy/package.json new file mode 100644 index 000000000..10e70bb74 --- /dev/null +++ b/src/database-lazy/package.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../node_modules/ng-packagr/package.schema.json", + "ngPackage": { + "lib": { + "umdModuleIds": { + "firebase/app": "firebase", + "@firebase/database": "firebase-database" + }, + "entryFile": "public_api.ts" + } + } +} diff --git a/src/database-lazy/public_api.ts b/src/database-lazy/public_api.ts new file mode 100644 index 000000000..f79e8fabf --- /dev/null +++ b/src/database-lazy/public_api.ts @@ -0,0 +1,11 @@ + +import 'firebase/database'; // strip out in the build process + +export * from './database'; +export * from './list/changes'; +export * from './list/create-reference'; +export * from './list/snapshot-changes'; +export * from './list/state-changes'; +export * from './list/audit-trail'; +export * from './observable/fromRef'; +export * from './database.module'; diff --git a/src/database-lazy/utils.spec.ts b/src/database-lazy/utils.spec.ts new file mode 100644 index 000000000..b437b0700 --- /dev/null +++ b/src/database-lazy/utils.spec.ts @@ -0,0 +1,26 @@ +import * as utils from './utils'; + +describe('utils', () => { + + describe('isString', () => { + + it('should be able to properly detect a string', () => { + const str = 'oh hai'; + const notStr = 101; + const bool = true; + const nul = null; + const obj = {}; + const fn = () => { }; + const undef = undefined; + expect(utils.isString(str)).toBe(true); + expect(utils.isString(notStr)).toBe(false); + expect(utils.isString(bool)).toBe(false); + expect(utils.isString(nul)).toBe(false); + expect(utils.isString(obj)).toBe(false); + expect(utils.isString(fn)).toBe(false); + expect(utils.isString(undef)).toBe(false); + }); + + }); + +}); diff --git a/src/database-lazy/utils.ts b/src/database-lazy/utils.ts new file mode 100644 index 000000000..56c7d9043 --- /dev/null +++ b/src/database-lazy/utils.ts @@ -0,0 +1,41 @@ +import { DatabaseReference, FirebaseOperation, FirebaseOperationCases, PathReference } from './interfaces'; +import firebase from 'firebase/app'; + +export function isString(value: any): boolean { + return typeof value === 'string'; +} + +export function isFirebaseDataSnapshot(value: any): boolean { + return typeof value.exportVal === 'function'; +} + +export function isNil(obj: any): boolean { + return obj === undefined || obj === null; +} + +export function isFirebaseRef(value: any): boolean { + return typeof value.set === 'function'; +} + +/** + * Returns a database reference given a Firebase App and an + * absolute or relative path. + * @param database - Firebase Database + * @param pathRef - Database path, relative or absolute + */ +export function getRef(database: firebase.database.Database, pathRef: PathReference): DatabaseReference { + // if a db ref was passed in, just return it + return isFirebaseRef(pathRef) ? pathRef as DatabaseReference + : database.ref(pathRef as string); +} + +export function checkOperationCases(item: FirebaseOperation, cases: FirebaseOperationCases): Promise { + if (isString(item)) { + return cases.stringCase(); + } else if (isFirebaseRef(item)) { + return cases.firebaseCase(); + } else if (isFirebaseDataSnapshot(item)) { + return cases.snapshotCase(); + } + throw new Error(`Expects a string, snapshot, or reference. Got: ${typeof item}`); +} diff --git a/src/firestore-lazy/collection-group/collection-group.spec.ts b/src/firestore-lazy/collection-group/collection-group.spec.ts new file mode 100644 index 000000000..1b704107b --- /dev/null +++ b/src/firestore-lazy/collection-group/collection-group.spec.ts @@ -0,0 +1,504 @@ +import { AngularFireModule, FirebaseApp } from '@angular/fire'; +import { AngularFirestore, AngularFirestoreCollectionGroup, AngularFirestoreModule, SETTINGS } from '../public_api'; +import { QueryGroupFn, Query } from '../interfaces'; +import { BehaviorSubject } from 'rxjs'; +import { skip, switchMap, take } from 'rxjs/operators'; +import { TestBed } from '@angular/core/testing'; +import { COMMON_CONFIG } from '../../test-config'; +import 'firebase/firestore'; + +import { + createRandomStocks, + delayAdd, + delayDelete, + delayUpdate, + deleteThemAll, + FAKE_STOCK_DATA, + rando, + randomName, + Stock +} from '../utils.spec'; + +async function collectionHarness(afs: AngularFirestore, items: number, queryGroupFn?: QueryGroupFn) { + const randomCollectionName = randomName(afs.firestore); + const ref = afs.firestore.collection(`${randomCollectionName}`); + const firestore = afs.firestore; + const collectionGroup = firestore.collectionGroup(randomCollectionName) as Query; + const queryFn = queryGroupFn || (ref => ref); + const stocks = new AngularFirestoreCollectionGroup(queryFn(collectionGroup), afs); + const names = await createRandomStocks(afs.firestore, ref, items); + return { randomCollectionName, ref, stocks, names }; +} + +describe('AngularFirestoreLazyCollectionGroup', () => { + let app: FirebaseApp; + let afs: AngularFirestore; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFirestoreModule + ], + providers: [ + { provide: SETTINGS, useValue: { host: 'localhost:8080', ssl: false } } + ] + }); + + app = TestBed.inject(FirebaseApp); + afs = TestBed.inject(AngularFirestore); + }); + + afterEach(() => { + app.delete(); + }); + + describe('valueChanges()', () => { + + it('should get unwrapped snapshot', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.valueChanges().subscribe(data => { + // unsub immediately as we will be deleting data at the bottom + // and that will trigger another subscribe callback and fail + // the test + sub.unsubscribe(); + // We added four things. This should be four. + // This could not be four if the batch failed or + // if the collection state is altered during a test run + expect(data.length).toEqual(ITEMS); + data.forEach(stock => { + // We used the same piece of data so they should all equal + expect(stock).toEqual(FAKE_STOCK_DATA); + }); + // Delete them all + const promises = names.map(name => ref.doc(name).delete()); + Promise.all(promises).then(done).catch(fail); + }); + + }); + + it('should handle multiple subscriptions (hot)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.valueChanges(); + const sub = changes.subscribe(() => { + }).add( + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + sub.unsubscribe(); + }) + ).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + + it('should handle multiple subscriptions (warm)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.valueChanges(); + changes.pipe(take(1)).subscribe(() => { + }).add(() => { + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + }).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + }); + + it('should handle dynamic queries that return empty sets', async (done) => { + const ITEMS = 10; + let count = 0; + + const pricefilter$ = new BehaviorSubject(null); + const randomCollectionName = randomName(afs.firestore); + const ref = afs.firestore.collection(`${randomCollectionName}`); + const names = await createRandomStocks(afs.firestore, ref, ITEMS); + const sub = pricefilter$.pipe(switchMap(price => { + return afs.collection(randomCollectionName, ref => price ? ref.where('price', '==', price) : ref).valueChanges(); + })).subscribe(data => { + count = count + 1; + // the first time should all be 'added' + if (count === 1) { + expect(data.length).toEqual(ITEMS); + pricefilter$.next(-1); + } + // on the second round, we should have filtered out everything + if (count === 2) { + expect(data.length).toEqual(0); + sub.unsubscribe(); + deleteThemAll(names, ref).then(done).catch(done.fail); + } + }); + }); + + it('should return the document\'s id along with the data if the idField option is provided.', async () => { + const ITEMS = 4; + const DOC_ID = 'docId'; + const { stocks } = await collectionHarness(afs, ITEMS); + + const sub = stocks.valueChanges({idField: DOC_ID}).subscribe(data => { + const allDocumentsHaveId = data.every(d => d.docId !== undefined); + + expect(allDocumentsHaveId).toBe(true); + sub.unsubscribe(); + }); + }); + + }); + + describe('snapshotChanges()', () => { + + it('should listen to all snapshotChanges() by default', async (done) => { + const ITEMS = 10; + let count = 0; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const sub = stocks.snapshotChanges().subscribe(data => { + count = count + 1; + // the first time should all be 'added' + if (count === 1) { + // make an update + ref.doc(names[0]).update({ price: 2 }); + } + // on the second round, make sure the array is still the same + // length but the updated item is now modified + if (count === 2) { + expect(data.length).toEqual(ITEMS); + const change = data.filter(x => x.payload.doc.id === names[0])[0]; + expect(change.type).toEqual('modified'); + sub.unsubscribe(); + deleteThemAll(names, ref).then(done).catch(done.fail); + } + }); + }); + + it('should handle multiple subscriptions (hot)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.snapshotChanges(); + const sub = changes.subscribe(() => { + }).add( + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + sub.unsubscribe(); + }) + ).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + + it('should handle multiple subscriptions (warm)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.snapshotChanges(); + changes.pipe(take(1)).subscribe(() => { + }).add(() => { + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + }).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + }); + + it('should update order on queries', async (done) => { + const ITEMS = 10; + let count = 0; + let firstIndex = 0; + const { ref, stocks, names } = + await collectionHarness(afs, ITEMS, ref => ref.orderBy('price', 'desc')); + const sub = stocks.snapshotChanges().subscribe(data => { + count = count + 1; + // the first time should all be 'added' + if (count === 1) { + // make an update + firstIndex = data.filter(d => d.payload.doc.id === names[0])[0].payload.newIndex; + ref.doc(names[0]).update({ price: 2 }); + } + // on the second round, make sure the array is still the same + // length but the updated item is now modified + if (count === 2) { + expect(data.length).toEqual(ITEMS); + const change = data.filter(x => x.payload.doc.id === names[0])[0]; + expect(change.type).toEqual('modified'); + expect(change.payload.oldIndex).toEqual(firstIndex); + sub.unsubscribe(); + deleteThemAll(names, ref).then(done).catch(done.fail); + } + }); + }); + + it('should be able to filter snapshotChanges() types - modified', async (done) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.snapshotChanges(['modified']).pipe(skip(1)).subscribe(data => { + sub.unsubscribe(); + const change = data.filter(x => x.payload.doc.id === names[0])[0]; + expect(data.length).toEqual(1); + expect(change.payload.doc.data().price).toEqual(2); + expect(change.type).toEqual('modified'); + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + + delayUpdate(ref, names[0], { price: 2 }); + }); + + it('should be able to filter snapshotChanges() types - added', async (done) => { + const ITEMS = 10; + const harness = await collectionHarness(afs, ITEMS); + const { randomCollectionName, ref, stocks } = harness; + let { names } = harness; + const nextId = ref.doc('a').id; + + const sub = stocks.snapshotChanges(['added']).pipe(skip(1)).subscribe(data => { + sub.unsubscribe(); + const change = data.filter(x => x.payload.doc.id === nextId)[0]; + expect(data.length).toEqual(ITEMS + 1); + expect(change.payload.doc.data().price).toEqual(2); + expect(change.type).toEqual('added'); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + + names = names.concat([nextId]); + // TODO these two add tests are the only one really testing collection-group queries + // should flex more, maybe split the stocks between more than one collection + delayAdd(ref.doc(names[0]).collection(randomCollectionName), nextId, { price: 2 }); + }); + + it('should be able to filter snapshotChanges() types - added w/same id', async (done) => { + const ITEMS = 10; + const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.snapshotChanges(['added']).pipe(skip(1)).subscribe(data => { + sub.unsubscribe(); + const change = data.filter(x => x.payload.doc.id === names[0])[1]; + expect(data.length).toEqual(ITEMS + 1); + expect(change.payload.doc.data().price).toEqual(3); + expect(change.type).toEqual('added'); + ref.doc(names[0]).collection(randomCollectionName).doc(names[0]).delete() + .then(() => deleteThemAll(names, ref)) + .then(done).catch(done.fail); + done(); + }); + + delayAdd(ref.doc(names[0]).collection(randomCollectionName), names[0], { price: 3 }); + }); + + /* TODO(jamesdaniels): revisit this test with metadata changes, need to do some additional skips + it('should be able to filter snapshotChanges() types - added/modified', async (done) => { + const ITEMS = 10; + + const harness = await collectionHarness(afs, ITEMS); + const { ref, stocks } = harness; + let { names } = harness; + + const nextId = ref.doc('a').id; + let count = 0; + + stocks.snapshotChanges(['added', 'modified']).pipe(skip(1), take(2)).subscribe(data => { + count += 1; + if (count === 1) { + const change = data.filter(x => x.payload.doc.id === nextId)[0]; + expect(data.length).toEqual(ITEMS + 1); + expect(change.payload.doc.data().price).toEqual(2); + expect(change.type).toEqual('added'); + delayUpdate(ref, names[0], { price: 2 }); + } + if (count === 2) { + const change = data.filter(x => x.payload.doc.id === names[0])[0]; + expect(data.length).toEqual(ITEMS + 1); + expect(change.payload.doc.data().price).toEqual(2); + expect(change.type).toEqual('modified'); + } + }).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + + names = names.concat([nextId]); + delayAdd(ref, nextId, { price: 2 }); + }); + */ + + it('should be able to filter snapshotChanges() types - removed', async (done) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.snapshotChanges(['added', 'removed']).pipe(skip(1)).subscribe(data => { + sub.unsubscribe(); + const change = data.filter(x => x.payload.doc.id === names[0]); + expect(data.length).toEqual(ITEMS - 1); + expect(change.length).toEqual(0); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + delayDelete(ref, names[0], 400); + }); + + }); + + describe('stateChanges()', () => { + + it('should get stateChanges() updates', async (done: any) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.stateChanges().subscribe(data => { + // unsub immediately as we will be deleting data at the bottom + // and that will trigger another subscribe callback and fail + // the test + sub.unsubscribe(); + // We added ten things. This should be ten. + // This could not be ten if the batch failed or + // if the collection state is altered during a test run + expect(data.length).toEqual(ITEMS); + data.forEach(action => { + // We used the same piece of data so they should all equal + expect(action.payload.doc.data()).toEqual(FAKE_STOCK_DATA); + }); + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + + }); + + it('should listen to all stateChanges() by default', async (done) => { + const ITEMS = 10; + let count = 0; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + stocks.stateChanges().subscribe(data => { + count = count + 1; + if (count === 1) { + ref.doc(names[0]).update({ price: 2 }); + } + if (count === 2) { + expect(data.length).toEqual(1); + expect(data[0].type).toEqual('modified'); + deleteThemAll(names, ref).then(done).catch(done.fail); + } + }); + }); + + it('should handle multiple subscriptions (hot)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.stateChanges(); + const sub = changes.subscribe(() => { + }).add( + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + sub.unsubscribe(); + }) + ).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + + it('should handle multiple subscriptions (warm)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.stateChanges(); + changes.pipe(take(1)).subscribe(() => { + }).add(() => { + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + }).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + }); + + it('should be able to filter stateChanges() types - modified', async (done) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.stateChanges(['modified']).subscribe(data => { + sub.unsubscribe(); + expect(data.length).toEqual(1); + expect(data[0].payload.doc.data().price).toEqual(2); + expect(data[0].type).toEqual('modified'); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + delayUpdate(ref, names[0], { price: 2 }); + }); + + it('should be able to filter stateChanges() types - added', async (done) => { + const ITEMS = 10; + + const harness = await collectionHarness(afs, ITEMS); + const { ref, stocks } = harness; + let { names } = harness; + + + const sub = stocks.stateChanges(['added']).pipe(skip(1)).subscribe(data => { + sub.unsubscribe(); + expect(data.length).toEqual(1); + expect(data[0].payload.doc.data().price).toEqual(2); + expect(data[0].type).toEqual('added'); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + const nextId = ref.doc('a').id; + names = names.concat([nextId]); + delayAdd(ref, nextId, { price: 2 }); + }); + + it('should be able to filter stateChanges() types - removed', async (done) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.stateChanges(['removed']).subscribe(data => { + sub.unsubscribe(); + expect(data.length).toEqual(1); + expect(data[0].type).toEqual('removed'); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + delayDelete(ref, names[0], 400); + }); + }); + + describe('auditTrail()', () => { + it('should listen to all events for auditTrail() by default', async (done) => { + const ITEMS = 10; + let count = 0; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const sub = stocks.auditTrail().subscribe(data => { + count = count + 1; + if (count === 1) { + ref.doc(names[0]).update({ price: 2 }); + } + if (count === 2) { + sub.unsubscribe(); + expect(data.length).toEqual(ITEMS + 1); + expect(data[data.length - 1].type).toEqual('modified'); + deleteThemAll(names, ref).then(done).catch(done.fail); + } + }); + }); + + it('should be able to filter auditTrail() types - removed', async (done) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.auditTrail(['removed']).subscribe(data => { + sub.unsubscribe(); + expect(data.length).toEqual(1); + expect(data[0].type).toEqual('removed'); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + delayDelete(ref, names[0], 400); + }); + }); + +}); diff --git a/src/firestore-lazy/collection-group/collection-group.ts b/src/firestore-lazy/collection-group/collection-group.ts new file mode 100644 index 000000000..4a448ec6f --- /dev/null +++ b/src/firestore-lazy/collection-group/collection-group.ts @@ -0,0 +1,114 @@ +import { Observable } from 'rxjs'; +import { fromCollectionRef } from '../observable/fromRef'; +import { filter, map, observeOn, scan, switchMap } from 'rxjs/operators'; +import firebase from 'firebase/app'; +import { DocumentChangeAction, DocumentChangeType, DocumentData, Query } from '../interfaces'; +import { validateEventsArray } from '../collection/collection'; +import { docChanges, sortedChanges } from '../collection/changes'; +import { AngularFirestore } from '../firestore'; + +/** + * AngularFirestoreCollectionGroup service + * + * This class holds a reference to a Firestore Collection Group Query. + * + * This class uses Symbol.observable to transform into Observable using Observable.from(). + * + * This class is rarely used directly and should be created from the AngularFirestore service. + * + * Example: + * + * const collectionGroup = firebase.firestore.collectionGroup('stocks'); + * const query = collectionRef.where('price', '>', '0.01'); + * const fakeStock = new AngularFirestoreCollectionGroup(query, afs); + * + * // Subscribe to changes as snapshots. This provides you data updates as well as delta updates. + * fakeStock.valueChanges().subscribe(value => console.log(value)); + */ +export class AngularFirestoreCollectionGroup { + /** + * The constructor takes in a CollectionGroupQuery to provide wrapper methods + * for data operations and data streaming. + */ + constructor( + private readonly query: Observable>, + private readonly afs: AngularFirestore) { } + + /** + * Listen to the latest change in the stream. This method returns changes + * as they occur and they are not sorted by query order. This allows you to construct + * your own data structure. + */ + stateChanges(events?: DocumentChangeType[]): Observable[]> { + if (!events || events.length === 0) { + return this.query.pipe( + switchMap(query => docChanges(query, this.afs.schedulers.outsideAngular)), + this.afs.keepUnstableUntilFirst + ); + } + return this.query.pipe( + switchMap(query => docChanges(query, this.afs.schedulers.outsideAngular)), + map(actions => actions.filter(change => events.indexOf(change.type) > -1)), + filter(changes => changes.length > 0), + this.afs.keepUnstableUntilFirst + ); + } + + /** + * Create a stream of changes as they occur it time. This method is similar to stateChanges() + * but it collects each event in an array over time. + */ + auditTrail(events?: DocumentChangeType[]): Observable[]> { + return this.stateChanges(events).pipe(scan((current, action) => [...current, ...action], [])); + } + + /** + * Create a stream of synchronized changes. This method keeps the local array in sorted + * query order. + */ + snapshotChanges(events?: DocumentChangeType[]): Observable[]> { + const validatedEvents = validateEventsArray(events); + return this.query.pipe( + switchMap(query => sortedChanges(query, validatedEvents, this.afs.schedulers.outsideAngular)), + this.afs.keepUnstableUntilFirst + ); + } + + /** + * Listen to all documents in the collection and its possible query as an Observable. + * + * If the `idField` option is provided, document IDs are included and mapped to the + * provided `idField` property name. + */ + valueChanges(): Observable; + // tslint:disable-next-line:unified-signatures + valueChanges({}): Observable; + valueChanges(options: {idField: K}): Observable<(T & { [T in K]: string })[]>; + valueChanges(options: {idField?: K} = {}): Observable { + return this.query.pipe( + switchMap(query => fromCollectionRef(query, this.afs.schedulers.outsideAngular)), + map(actions => actions.payload.docs.map(a => { + if (options.idField) { + return { + [options.idField]: a.id, + ...a.data() + } as T & { [T in K]: string }; + } else { + return a.data(); + } + })), + this.afs.keepUnstableUntilFirst + ); + } + + /** + * Retrieve the results of the query once. + */ + get(options?: firebase.firestore.GetOptions) { + return this.query.pipe( + switchMap(query => query.get(options)), + observeOn(this.afs.schedulers.insideAngular) + ); + } + +} diff --git a/src/firestore/collection/changes.ts b/src/firestore-lazy/collection/changes.ts similarity index 100% rename from src/firestore/collection/changes.ts rename to src/firestore-lazy/collection/changes.ts diff --git a/src/firestore-lazy/collection/collection.spec.ts b/src/firestore-lazy/collection/collection.spec.ts new file mode 100644 index 000000000..17b904235 --- /dev/null +++ b/src/firestore-lazy/collection/collection.spec.ts @@ -0,0 +1,486 @@ +import { AngularFireModule, FirebaseApp } from '@angular/fire'; +import { AngularFirestore, SETTINGS } from '../firestore'; +import { AngularFirestoreModule } from '../firestore.module'; +import { AngularFirestoreCollection } from './collection'; +import { QueryFn, CollectionReference } from '../interfaces'; +import { BehaviorSubject } from 'rxjs'; +import { skip, switchMap, take } from 'rxjs/operators'; +import 'firebase/firestore'; + +import { TestBed } from '@angular/core/testing'; +import { COMMON_CONFIG } from '../../test-config'; + +import { + createRandomStocks, + delayAdd, + delayDelete, + delayUpdate, + deleteThemAll, + FAKE_STOCK_DATA, + rando, + randomName, + Stock +} from '../utils.spec'; + +async function collectionHarness(afs: AngularFirestore, items: number, queryFn?: QueryFn) { + const randomCollectionName = randomName(afs.firestore); + const ref = afs.firestore.collection(`${randomCollectionName}`) as CollectionReference; + if (!queryFn) { + queryFn = (ref) => ref; + } + const stocks = new AngularFirestoreCollection(ref, queryFn(ref), afs); + const names = await createRandomStocks(afs.firestore, ref, items); + return { randomCollectionName, ref, stocks, names }; +} + +describe('AngularFirestoreLazyCollection', () => { + let app: FirebaseApp; + let afs: AngularFirestore; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFirestoreModule + ], + providers: [ + { provide: SETTINGS, useValue: { host: 'localhost:8080', ssl: false } } + ] + }); + + app = TestBed.inject(FirebaseApp); + afs = TestBed.inject(AngularFirestore); + }); + + afterEach(() => { + app.delete(); + }); + + describe('valueChanges()', () => { + + it('should get unwrapped snapshot', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.valueChanges().subscribe(data => { + // unsub immediately as we will be deleting data at the bottom + // and that will trigger another subscribe callback and fail + // the test + sub.unsubscribe(); + // We added four things. This should be four. + // This could not be four if the batch failed or + // if the collection state is altered during a test run + expect(data.length).toEqual(ITEMS); + data.forEach(stock => { + // We used the same piece of data so they should all equal + expect(stock).toEqual(FAKE_STOCK_DATA); + }); + // Delete them all + const promises = names.map(name => ref.doc(name).delete()); + Promise.all(promises).then(done).catch(fail); + }); + + }); + + /* FLAKE? timing out + it('should optionally map the doc ID to the emitted data object', async (done: any) => { + const ITEMS = 1; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const idField = 'myCustomID'; + const sub = stocks.valueChanges({idField}).subscribe(data => { + sub.unsubscribe(); + const stock = data[0]; + expect(stock[idField]).toBeDefined(); + expect(stock).toEqual(jasmine.objectContaining(FAKE_STOCK_DATA)); + deleteThemAll(names, ref).then(done).catch(fail); + }) + });*/ + + it('should handle multiple subscriptions (hot)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.valueChanges(); + const sub = changes.subscribe(() => { + }).add( + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + sub.unsubscribe(); + }) + ).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + + it('should handle multiple subscriptions (warm)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.valueChanges(); + changes.pipe(take(1)).subscribe(() => { + }).add(() => { + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + }).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + }); + + it('should handle dynamic queries that return empty sets', async (done) => { + const ITEMS = 10; + let count = 0; + + const pricefilter$ = new BehaviorSubject(null); + const randomCollectionName = randomName(afs.firestore); + const ref = afs.firestore.collection(`${randomCollectionName}`); + const names = await createRandomStocks(afs.firestore, ref, ITEMS); + const sub = pricefilter$.pipe(switchMap(price => { + return afs.collection(randomCollectionName, ref => price ? ref.where('price', '==', price) : ref).valueChanges(); + })).subscribe(data => { + count = count + 1; + // the first time should all be 'added' + if (count === 1) { + expect(data.length).toEqual(ITEMS); + pricefilter$.next(-1); + } + // on the second round, we should have filtered out everything + if (count === 2) { + expect(data.length).toEqual(0); + sub.unsubscribe(); + deleteThemAll(names, ref).then(done).catch(done.fail); + } + }); + }); + + }); + + describe('snapshotChanges()', () => { + + it('should listen to all snapshotChanges() by default', async (done) => { + const ITEMS = 10; + let count = 0; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const sub = stocks.snapshotChanges().subscribe(data => { + count = count + 1; + // the first time should all be 'added' + if (count === 1) { + // make an update + stocks.doc(names[0]).update({ price: 2 }); + } + // on the second round, make sure the array is still the same + // length but the updated item is now modified + if (count === 2) { + expect(data.length).toEqual(ITEMS); + const change = data.filter(x => x.payload.doc.id === names[0])[0]; + expect(change.type).toEqual('modified'); + sub.unsubscribe(); + deleteThemAll(names, ref).then(done).catch(done.fail); + } + }); + }); + + it('should handle multiple subscriptions (hot)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.snapshotChanges(); + const sub = changes.subscribe(() => { + }).add( + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + sub.unsubscribe(); + }) + ).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + + it('should handle multiple subscriptions (warm)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.snapshotChanges(); + changes.pipe(take(1)).subscribe(() => { + }).add(() => { + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + }).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + }); + + it('should update order on queries', async (done) => { + const ITEMS = 10; + let count = 0; + let firstIndex = 0; + const { ref, stocks, names } = + await collectionHarness(afs, ITEMS, ref => ref.orderBy('price', 'desc')); + const sub = stocks.snapshotChanges().subscribe(data => { + count = count + 1; + // the first time should all be 'added' + if (count === 1) { + // make an update + firstIndex = data.filter(d => d.payload.doc.id === names[0])[0].payload.newIndex; + stocks.doc(names[0]).update({ price: 2 }); + } + // on the second round, make sure the array is still the same + // length but the updated item is now modified + if (count === 2) { + expect(data.length).toEqual(ITEMS); + const change = data.filter(x => x.payload.doc.id === names[0])[0]; + expect(change.type).toEqual('modified'); + expect(change.payload.oldIndex).toEqual(firstIndex); + sub.unsubscribe(); + deleteThemAll(names, ref).then(done).catch(done.fail); + } + }); + }); + + it('should be able to filter snapshotChanges() types - modified', async (done) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.snapshotChanges(['modified']).pipe(skip(1)).subscribe(data => { + sub.unsubscribe(); + const change = data.filter(x => x.payload.doc.id === names[0])[0]; + expect(data.length).toEqual(1); + expect(change.payload.doc.data().price).toEqual(2); + expect(change.type).toEqual('modified'); + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + + delayUpdate(stocks, names[0], { price: 2 }); + }); + + it('should be able to filter snapshotChanges() types - added', async (done) => { + const ITEMS = 10; + const harness = await collectionHarness(afs, ITEMS); + const { ref, stocks } = harness; + let names = harness.names; + + const nextId = ref.doc('a').id; + + const sub = stocks.snapshotChanges(['added']).pipe(skip(1)).subscribe(data => { + sub.unsubscribe(); + const change = data.filter(x => x.payload.doc.id === nextId)[0]; + expect(data.length).toEqual(ITEMS + 1); + expect(change.payload.doc.data().price).toEqual(2); + expect(change.type).toEqual('added'); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + + names = names.concat([nextId]); + delayAdd(stocks, nextId, { price: 2 }); + }); + + /* TODO(jamesdaniels): revisit this now that we have metadata + it('should be able to filter snapshotChanges() types - added/modified', async (done) => { + const ITEMS = 10; + const harness = await collectionHarness(afs, ITEMS); + const { ref, stocks } = harness; + let names = harness.names; + + const nextId = ref.doc('a').id; + let count = 0; + + stocks.snapshotChanges(['added', 'modified']).pipe(skip(1), take(2)).subscribe(data => { + count += 1; + if (count === 1) { + const change = data.filter(x => x.payload.doc.id === nextId)[0]; + expect(data.length).toEqual(ITEMS + 1); + expect(change.payload.doc.data().price).toEqual(2); + expect(change.type).toEqual('added'); + delayUpdate(stocks, names[0], { price: 2 }); + } + if (count === 2) { + const change = data.filter(x => x.payload.doc.id === names[0])[0]; + expect(data.length).toEqual(ITEMS + 1); + expect(change.payload.doc.data().price).toEqual(2); + expect(change.type).toEqual('modified'); + } + }).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + + names = names.concat([nextId]); + delayAdd(stocks, nextId, { price: 2 }); + }); + */ + + it('should be able to filter snapshotChanges() types - removed', async (done) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.snapshotChanges(['added', 'removed']).pipe(skip(1)).subscribe(data => { + sub.unsubscribe(); + const change = data.filter(x => x.payload.doc.id === names[0]); + expect(data.length).toEqual(ITEMS - 1); + expect(change.length).toEqual(0); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + delayDelete(stocks, names[0], 400); + }); + + }); + + describe('stateChanges()', () => { + + it('should get stateChanges() updates', async (done: any) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.stateChanges().subscribe(data => { + // unsub immediately as we will be deleting data at the bottom + // and that will trigger another subscribe callback and fail + // the test + sub.unsubscribe(); + // We added ten things. This should be ten. + // This could not be ten if the batch failed or + // if the collection state is altered during a test run + expect(data.length).toEqual(ITEMS); + data.forEach(action => { + // We used the same piece of data so they should all equal + expect(action.payload.doc.data()).toEqual(FAKE_STOCK_DATA); + }); + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + + }); + + it('should listen to all stateChanges() by default', async (done) => { + const ITEMS = 10; + let count = 0; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + stocks.stateChanges().subscribe(data => { + count = count + 1; + if (count === 1) { + stocks.doc(names[0]).update({ price: 2 }); + } + if (count === 2) { + expect(data.length).toEqual(1); + expect(data[0].type).toEqual('modified'); + deleteThemAll(names, ref).then(done).catch(done.fail); + } + }); + }); + + it('should handle multiple subscriptions (hot)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.stateChanges(); + const sub = changes.subscribe(() => { + }).add( + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + sub.unsubscribe(); + }) + ).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + + it('should handle multiple subscriptions (warm)', async (done: any) => { + const ITEMS = 4; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.stateChanges(); + changes.pipe(take(1)).subscribe(() => { + }).add(() => { + changes.pipe(take(1)).subscribe(data => { + expect(data.length).toEqual(ITEMS); + }).add(() => { + deleteThemAll(names, ref).then(done).catch(done.fail); + }); + }); + }); + + it('should be able to filter stateChanges() types - modified', async (done) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.stateChanges(['modified']).subscribe(data => { + sub.unsubscribe(); + expect(data.length).toEqual(1); + expect(data[0].payload.doc.data().price).toEqual(2); + expect(data[0].type).toEqual('modified'); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + delayUpdate(stocks, names[0], { price: 2 }); + }); + + it('should be able to filter stateChanges() types - added', async (done) => { + const ITEMS = 10; + + const harness = await collectionHarness(afs, ITEMS); + const { ref, stocks } = harness; + let names = harness.names; + + const sub = stocks.stateChanges(['added']).pipe(skip(1)).subscribe(data => { + sub.unsubscribe(); + expect(data.length).toEqual(1); + expect(data[0].payload.doc.data().price).toEqual(2); + expect(data[0].type).toEqual('added'); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + const nextId = ref.doc('a').id; + names = names.concat([nextId]); + delayAdd(stocks, nextId, { price: 2 }); + }); + + it('should be able to filter stateChanges() types - removed', async (done) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.stateChanges(['removed']).subscribe(data => { + sub.unsubscribe(); + expect(data.length).toEqual(1); + expect(data[0].type).toEqual('removed'); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + delayDelete(stocks, names[0], 400); + }); + }); + + describe('auditTrail()', () => { + it('should listen to all events for auditTrail() by default', async (done) => { + const ITEMS = 10; + let count = 0; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + const sub = stocks.auditTrail().subscribe(data => { + count = count + 1; + if (count === 1) { + stocks.doc(names[0]).update({ price: 2 }); + } + if (count === 2) { + sub.unsubscribe(); + expect(data.length).toEqual(ITEMS + 1); + expect(data[data.length - 1].type).toEqual('modified'); + deleteThemAll(names, ref).then(done).catch(done.fail); + } + }); + }); + + it('should be able to filter auditTrail() types - removed', async (done) => { + const ITEMS = 10; + const { ref, stocks, names } = await collectionHarness(afs, ITEMS); + + const sub = stocks.auditTrail(['removed']).subscribe(data => { + sub.unsubscribe(); + expect(data.length).toEqual(1); + expect(data[0].type).toEqual('removed'); + deleteThemAll(names, ref).then(done).catch(done.fail); + done(); + }); + + delayDelete(stocks, names[0], 400); + }); + }); + +}); diff --git a/src/firestore-lazy/collection/collection.ts b/src/firestore-lazy/collection/collection.ts new file mode 100644 index 000000000..495249b7a --- /dev/null +++ b/src/firestore-lazy/collection/collection.ts @@ -0,0 +1,151 @@ +import { Observable } from 'rxjs'; +import { fromCollectionRef } from '../observable/fromRef'; +import { filter, map, observeOn, scan, switchMap } from 'rxjs/operators'; +import firebase from 'firebase/app'; +import { CollectionReference, DocumentChangeAction, DocumentChangeType, DocumentData, DocumentReference, Query } from '../interfaces'; +import { docChanges, sortedChanges } from './changes'; +import { AngularFirestoreDocument } from '../document/document'; +import { AngularFirestore } from '../firestore'; + +export function validateEventsArray(events?: DocumentChangeType[]) { + if (!events || events.length === 0) { + events = ['added', 'removed', 'modified']; + } + return events; +} + +/** + * AngularFirestoreCollection service + * + * This class creates a reference to a Firestore Collection. A reference and a query are provided in + * in the constructor. The query can be the unqueried reference if no query is desired.The class + * is generic which gives you type safety for data update methods and data streaming. + * + * This class uses Symbol.observable to transform into Observable using Observable.from(). + * + * This class is rarely used directly and should be created from the AngularFirestore service. + * + * Example: + * + * const collectionRef = firebase.firestore.collection('stocks'); + * const query = collectionRef.where('price', '>', '0.01'); + * const fakeStock = new AngularFirestoreCollection(collectionRef, query); + * + * // NOTE!: the updates are performed on the reference not the query + * await fakeStock.add({ name: 'FAKE', price: 0.01 }); + * + * // Subscribe to changes as snapshots. This provides you data updates as well as delta updates. + * fakeStock.valueChanges().subscribe(value => console.log(value)); + */ +export class AngularFirestoreCollection { + /** + * The constructor takes in a CollectionReference and Query to provide wrapper methods + * for data operations and data streaming. + * + * Note: Data operation methods are done on the reference not the query. This means + * when you update data it is not updating data to the window of your query unless + * the data fits the criteria of the query. See the AssociatedRefence type for details + * on this implication. + */ + constructor( + public readonly ref: Observable>, + private readonly query: Observable>, + private readonly afs: AngularFirestore) { } + + /** + * Listen to the latest change in the stream. This method returns changes + * as they occur and they are not sorted by query order. This allows you to construct + * your own data structure. + */ + stateChanges(events?: DocumentChangeType[]): Observable[]> { + if (!events || events.length === 0) { + this.query.pipe( + switchMap(query => docChanges(query, this.afs.schedulers.outsideAngular)), + filter(changes => changes.length > 0), + this.afs.keepUnstableUntilFirst + ); + } + return this.query.pipe( + switchMap(query => docChanges(query, this.afs.schedulers.outsideAngular)), + map(actions => actions.filter(change => events.indexOf(change.type) > -1)), + filter(changes => changes.length > 0), + this.afs.keepUnstableUntilFirst + ); + } + + /** + * Create a stream of changes as they occur it time. This method is similar to stateChanges() + * but it collects each event in an array over time. + */ + auditTrail(events?: DocumentChangeType[]): Observable[]> { + return this.stateChanges(events).pipe(scan((current, action) => [...current, ...action], [])); + } + + /** + * Create a stream of synchronized changes. This method keeps the local array in sorted + * query order. + */ + snapshotChanges(events?: DocumentChangeType[]): Observable[]> { + const validatedEvents = validateEventsArray(events); + return this.query.pipe( + switchMap(query => sortedChanges(query, validatedEvents, this.afs.schedulers.outsideAngular)), + this.afs.keepUnstableUntilFirst + ); + } + + /** + * Listen to all documents in the collection and its possible query as an Observable. + * + * If the `idField` option is provided, document IDs are included and mapped to the + * provided `idField` property name. + */ + valueChanges(): Observable; + // tslint:disable-next-line:unified-signatures + valueChanges({}): Observable; + valueChanges(options: {idField: K}): Observable<(T & { [T in K]: string })[]>; + valueChanges(options: {idField?: K} = {}): Observable { + return this.query.pipe( + switchMap(query => fromCollectionRef(query, this.afs.schedulers.outsideAngular)), + map(actions => actions.payload.docs.map(a => { + if (options.idField) { + return { + ...a.data() as {}, + ...{ [options.idField]: a.id } + } as T & { [T in K]: string }; + } else { + return a.data(); + } + })), + this.afs.keepUnstableUntilFirst + ); + } + + /** + * Retrieve the results of the query once. + */ + get(options?: firebase.firestore.GetOptions) { + return this.query.pipe( + switchMap(query => query.get(options)), + observeOn(this.afs.schedulers.insideAngular), + ); + } + + /** + * Add data to a collection reference. + * + * Note: Data operation methods are done on the reference not the query. This means + * when you update data it is not updating data to the window of your query unless + * the data fits the criteria of the query. + */ + add(data: T): Promise> { + return this.ref.toPromise().then(ref => ref.add(data)); + } + + /** + * Create a reference to a single document in a collection. + */ + doc(path?: string): AngularFirestoreDocument { + // TODO is there a better way to solve this type issue + return new AngularFirestoreDocument(this.ref.pipe(map(ref => ref.doc(path) as any)), this.afs); + } +} diff --git a/src/firestore-lazy/document/document.spec.ts b/src/firestore-lazy/document/document.spec.ts new file mode 100644 index 000000000..2a981b53d --- /dev/null +++ b/src/firestore-lazy/document/document.spec.ts @@ -0,0 +1,101 @@ +import { AngularFireModule, FirebaseApp } from '@angular/fire'; +import { AngularFirestore, SETTINGS } from '../firestore'; +import { AngularFirestoreModule } from '../firestore.module'; +import { AngularFirestoreDocument } from './document'; +import { DocumentReference } from '../interfaces'; +import { take } from 'rxjs/operators'; + +import { TestBed } from '@angular/core/testing'; +import { COMMON_CONFIG } from '../../test-config'; + +import { FAKE_STOCK_DATA, rando, randomName, Stock } from '../utils.spec'; +import firebase from 'firebase/app'; +import 'firebase/firestore'; + +describe('AngularFirestoreLazyDocument', () => { + let app: FirebaseApp; + let afs: AngularFirestore; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFirestoreModule + ], + providers: [ + { provide: SETTINGS, useValue: { host: 'localhost:8080', ssl: false } } + ] + }); + + app = TestBed.inject(FirebaseApp); + afs = TestBed.inject(AngularFirestore); + }); + + afterEach(() => { + app.delete(); + }); + + describe('valueChanges()', () => { + + it('should get unwrapped snapshot', async (done: any) => { + const randomCollectionName = afs.firestore.collection('a').doc().id; + const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`) as firebase.firestore.DocumentReference; + const stock = new AngularFirestoreDocument(ref, afs); + await stock.set(FAKE_STOCK_DATA); + const obs$ = stock.valueChanges(); + obs$.pipe(take(1)).subscribe(async data => { + expect(data).toEqual(FAKE_STOCK_DATA); + stock.delete().then(done).catch(done.fail); + }); + }); + + /* TODO(jamesdaniels): test is flaking, look into this + it('should optionally map the doc ID to the emitted data object', async (done: any) => { + const randomCollectionName = afs.firestore.collection('a').doc().id; + const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`); + const stock = new AngularFirestoreDocument(ref, afs); + await stock.set(FAKE_STOCK_DATA); + const idField = 'myCustomID'; + const obs$ = stock.valueChanges({ idField }); + obs$.pipe(take(1)).subscribe(async data => { + expect(data[idField]).toBeDefined(); + expect(data).toEqual(jasmine.objectContaining(FAKE_STOCK_DATA)); + stock.delete().then(done).catch(done.fail); + }); + });*/ + + }); + + describe('snapshotChanges()', () => { + + it('should get action updates', async (done: any) => { + const randomCollectionName = randomName(afs.firestore); + const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`) as DocumentReference; + const stock = new AngularFirestoreDocument(ref, afs); + await stock.set(FAKE_STOCK_DATA); + const sub = stock + .snapshotChanges() + .subscribe(async a => { + sub.unsubscribe(); + if (a.payload.exists) { + expect(a.payload.data()).toEqual(FAKE_STOCK_DATA); + stock.delete().then(done).catch(done.fail); + } + }); + }); + + it('should get unwrapped snapshot', async (done: any) => { + const randomCollectionName = afs.firestore.collection('a').doc().id; + const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`) as DocumentReference; + const stock = new AngularFirestoreDocument(ref, afs); + await stock.set(FAKE_STOCK_DATA); + const obs$ = stock.valueChanges(); + obs$.pipe(take(1)).subscribe(async data => { + expect(data).toEqual(FAKE_STOCK_DATA); + stock.delete().then(done).catch(done.fail); + }); + }); + + }); + +}); diff --git a/src/firestore-lazy/document/document.ts b/src/firestore-lazy/document/document.ts new file mode 100644 index 000000000..19fbec8e7 --- /dev/null +++ b/src/firestore-lazy/document/document.ts @@ -0,0 +1,114 @@ +import { from, Observable } from 'rxjs'; +import { Action, DocumentData, DocumentReference, DocumentSnapshot, QueryFn, SetOptions } from '../interfaces'; +import { fromDocRef } from '../observable/fromRef'; +import { map, observeOn, switchMap } from 'rxjs/operators'; +import { AngularFirestore, associateQuery } from '../firestore'; +import { AngularFirestoreCollection } from '../collection/collection'; +import firebase from 'firebase/app'; + +/** + * AngularFirestoreDocument service + * + * This class creates a reference to a Firestore Document. A reference is provided in + * in the constructor. The class is generic which gives you type safety for data update + * methods and data streaming. + * + * This class uses Symbol.observable to transform into Observable using Observable.from(). + * + * This class is rarely used directly and should be created from the AngularFirestore service. + * + * Example: + * + * const fakeStock = new AngularFirestoreDocument(doc('stocks/FAKE')); + * await fakeStock.set({ name: 'FAKE', price: 0.01 }); + * fakeStock.valueChanges().map(snap => { + * if(snap.exists) return snap.data(); + * return null; + * }).subscribe(value => console.log(value)); + * // OR! Transform using Observable.from() and the data is unwrapped for you + * Observable.from(fakeStock).subscribe(value => console.log(value)); + */ +export class AngularFirestoreDocument { + + /** + * The constructor takes in a DocumentReference to provide wrapper methods + * for data operations, data streaming, and Symbol.observable. + */ + constructor(public ref: Observable>, private afs: AngularFirestore) { } + + /** + * Create or overwrite a single document. + */ + set(data: T, options?: SetOptions): Promise { + return this.ref.toPromise().then(ref => ref.set(data, options)); + } + + /** + * Update some fields of a document without overwriting the entire document. + */ + update(data: Partial): Promise { + return this.ref.toPromise().then(ref => ref.update(data)); + } + + /** + * Delete a document. + */ + delete(): Promise { + return this.ref.toPromise().then(ref => ref.delete()); + } + + /** + * Create a reference to a sub-collection given a path and an optional query + * function. + */ + collection(path: string, queryFn?: QueryFn): AngularFirestoreCollection { + const promise = this.ref.pipe( + map(ref => { + const collectionRef = ref.collection(path) as firebase.firestore.CollectionReference; + return associateQuery(collectionRef, queryFn); + } + )); + const ref = promise.pipe(map(it => it.ref)); + const query = promise.pipe(map(it => it.query)); + return new AngularFirestoreCollection(ref, query, this.afs); + } + + /** + * Listen to snapshot updates from the document. + */ + snapshotChanges(): Observable>> { + return this.ref.pipe( + switchMap(ref => fromDocRef(ref, this.afs.schedulers.outsideAngular)), + this.afs.keepUnstableUntilFirst + ); + } + + /** + * Listen to unwrapped snapshot updates from the document. + * + * If the `idField` option is provided, document IDs are included and mapped to the + * provided `idField` property name. + */ + valueChanges(options?: { }): Observable; + valueChanges(options: { idField: K }): Observable<(T & { [T in K]: string }) | undefined>; + valueChanges(options: { idField?: K } = {}): Observable { + return this.snapshotChanges().pipe( + map(({ payload }) => + options.idField ? { + ...payload.data(), + ...{ [options.idField]: payload.id } + } as T & { [T in K]: string } : payload.data() + ) + ); + } + + /** + * Retrieve the document once. + */ + get(options?: firebase.firestore.GetOptions) { + return this.ref.pipe( + switchMap(ref => ref.get(options)), + observeOn(this.afs.schedulers.insideAngular), + ); + } +} diff --git a/src/firestore-lazy/firestore-memory.module.ts b/src/firestore-lazy/firestore-memory.module.ts new file mode 100644 index 000000000..e4fda5da5 --- /dev/null +++ b/src/firestore-lazy/firestore-memory.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; +import { AngularFirestore } from './firestore'; + +@NgModule({ + providers: [ AngularFirestore ] +}) +export class AngularFirestoreModule { + // firebase/firestore/memory does not have persistence capabilities +} diff --git a/src/firestore-lazy/firestore-memory.ts b/src/firestore-lazy/firestore-memory.ts new file mode 100644 index 000000000..db678e004 --- /dev/null +++ b/src/firestore-lazy/firestore-memory.ts @@ -0,0 +1,3 @@ +// See index.ts, this variant is built by ./memory/ng-package.json +export * from './public_api'; +export * from './firestore-memory.module'; diff --git a/src/firestore-lazy/firestore.module.ts b/src/firestore-lazy/firestore.module.ts new file mode 100644 index 000000000..37499f71f --- /dev/null +++ b/src/firestore-lazy/firestore.module.ts @@ -0,0 +1,23 @@ +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { PersistenceSettings } from './interfaces'; +import { AngularFirestore, ENABLE_PERSISTENCE, PERSISTENCE_SETTINGS } from './firestore'; + +import 'firebase/firestore'; // removed in build process when not UMD + +@NgModule({ + providers: [ AngularFirestore ] +}) +export class AngularFirestoreModule { + /** + * Attempt to enable persistent storage, if possible + */ + static enablePersistence(persistenceSettings?: PersistenceSettings): ModuleWithProviders { + return { + ngModule: AngularFirestoreModule, + providers: [ + { provide: ENABLE_PERSISTENCE, useValue: true }, + { provide: PERSISTENCE_SETTINGS, useValue: persistenceSettings }, + ] + }; + } +} diff --git a/src/firestore-lazy/firestore.spec.ts b/src/firestore-lazy/firestore.spec.ts new file mode 100644 index 000000000..3291bfc3c --- /dev/null +++ b/src/firestore-lazy/firestore.spec.ts @@ -0,0 +1,174 @@ +import { AngularFireModule, FIREBASE_APP_NAME, FIREBASE_OPTIONS, FirebaseApp } from '@angular/fire'; +import { AngularFirestore, SETTINGS } from './firestore'; +import { AngularFirestoreModule } from './firestore.module'; +import { AngularFirestoreDocument } from './document/document'; +import { AngularFirestoreCollection } from './collection/collection'; + +import { TestBed } from '@angular/core/testing'; +import { COMMON_CONFIG } from '../test-config'; +import 'firebase/firestore'; +import { rando } from './utils.spec'; + +describe('AngularFirestoreLazy', () => { + let app: FirebaseApp; + let afs: AngularFirestore; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFirestoreModule.enablePersistence() + ], + providers: [ + { provide: SETTINGS, useValue: { host: 'localhost:8080', ssl: false } } + ] + }); + + app = TestBed.inject(FirebaseApp); + afs = TestBed.inject(AngularFirestore); + }); + + afterEach(() => { + app.delete(); + }); + + it('should be the properly initialized type', () => { + expect(afs instanceof AngularFirestore).toBe(true); + }); + + it('should have an initialized Firebase app', () => { + expect(afs.firestore.app).toBeDefined(); + }); + + it('should create an AngularFirestoreDocument from a string path', () => { + const doc = afs.doc('a/doc'); + expect(doc instanceof AngularFirestoreDocument).toBe(true); + }); + + it('should create an AngularFirestoreDocument from a string path', () => { + const doc = afs.doc(afs.doc('a/doc').ref); + expect(doc instanceof AngularFirestoreDocument).toBe(true); + }); + + it('should create an AngularFirestoreCollection from a string path', () => { + const collection = afs.collection('stuffs'); + expect(collection instanceof AngularFirestoreCollection).toBe(true); + }); + + it('should create an AngularFirestoreCollection from a reference', () => { + const collection = afs.collection(afs.collection('stuffs').ref); + expect(collection instanceof AngularFirestoreCollection).toBe(true); + }); + + it('should throw on an invalid document path', () => { + const singleWrapper = () => afs.doc('collection'); + const tripleWrapper = () => afs.doc('collection/doc/subcollection'); + expect(singleWrapper).toThrowError(); + expect(tripleWrapper).toThrowError(); + }); + + it('should throw on an invalid collection path', () => { + const singleWrapper = () => afs.collection('collection/doc'); + const quadWrapper = () => afs.collection('collection/doc/subcollection/doc'); + expect(singleWrapper).toThrowError(); + expect(quadWrapper).toThrowError(); + }); + + if (typeof window === 'undefined') { + + it('should not enable persistence (Node.js)', (done) => { + afs.persistenceEnabled$.subscribe(isEnabled => { + expect(isEnabled).toBe(false); + done(); + }); + }); + + } else { + + it('should enable persistence', (done) => { + afs.persistenceEnabled$.subscribe(isEnabled => { + expect(isEnabled).toBe(true); + done(); + }); + }); + + } + +}); + +describe('AngularFirestoreLazy with different app', () => { + let app: FirebaseApp; + let afs: AngularFirestore; + let firebaseAppName: string; + + beforeEach(() => { + firebaseAppName = rando(); + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFirestoreModule + ], + providers: [ + { provide: FIREBASE_APP_NAME, useValue: firebaseAppName }, + { provide: FIREBASE_OPTIONS, useValue: COMMON_CONFIG }, + { provide: SETTINGS, useValue: { host: 'localhost:8080', ssl: false } } + ] + }); + + + app = TestBed.inject(FirebaseApp); + afs = TestBed.inject(AngularFirestore); + }); + + afterEach(() => { + app.delete(); + }); + + describe('', () => { + + it('should be an AngularFirestore type', () => { + expect(afs instanceof AngularFirestore).toEqual(true); + }); + + it('should have an initialized Firebase app', () => { + expect(afs.firestore.app).toBeDefined(); + }); + + it('should have an initialized Firebase app instance member', () => { + expect(afs.firestore.app.name).toEqual(firebaseAppName); + }); + }); + +}); + +describe('AngularFirestoreLazy without persistance', () => { + let app: FirebaseApp; + let afs: AngularFirestore; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFirestoreModule + ], + providers: [ + { provide: SETTINGS, useValue: { host: 'localhost:8080', ssl: false } } + ] + }); + + app = TestBed.inject(FirebaseApp); + afs = TestBed.inject(AngularFirestore); + }); + + afterEach(() => { + app.delete(); + }); + + it('should not enable persistence', (done) => { + afs.persistenceEnabled$.subscribe(isEnabled => { + expect(isEnabled).toBe(false); + done(); + }); + }); + +}); diff --git a/src/firestore-lazy/firestore.ts b/src/firestore-lazy/firestore.ts new file mode 100644 index 000000000..c34f54625 --- /dev/null +++ b/src/firestore-lazy/firestore.ts @@ -0,0 +1,256 @@ +import { Inject, Injectable, InjectionToken, NgZone, Optional, PLATFORM_ID } from '@angular/core'; +import { from, Observable, of } from 'rxjs'; +import { + AssociatedReference, + CollectionReference, + DocumentReference, + PersistenceSettings, + Query, + QueryFn, + QueryGroupFn, + Settings +} from './interfaces'; +import { AngularFirestoreDocument } from './document/document'; +import { AngularFirestoreCollection } from './collection/collection'; +import { AngularFirestoreCollectionGroup } from './collection-group/collection-group'; +import { + FIREBASE_APP_NAME, + FIREBASE_OPTIONS, + FirebaseAppConfig, + FirebaseOptions, + ɵAngularFireSchedulers, + ɵfirebaseAppFactory, + ɵkeepUnstableUntilFirstFactory, + ɵlazySDKProxy, + ɵPromiseProxy, + ɵapplyMixins, +} from '@angular/fire'; +import { isPlatformServer } from '@angular/common'; +import firebase from 'firebase/app'; +import { ɵfetchInstance } from '@angular/fire'; +import { map, observeOn, shareReplay, switchMap } from 'rxjs/operators'; +import { newId } from './util'; +import { proxyPolyfillCompat } from './base'; + +/** + * The value of this token determines whether or not the firestore will have persistance enabled + */ +export const ENABLE_PERSISTENCE = new InjectionToken('angularfire2.enableFirestorePersistence'); +export const PERSISTENCE_SETTINGS = new InjectionToken('angularfire2.firestore.persistenceSettings'); +export const SETTINGS = new InjectionToken('angularfire2.firestore.settings'); + +// SEMVER(7): use Parameters to detirmine the useEmulator arguments +// type UseEmulatorArguments = Parameters; +export type ɵUseEmulatorArguments = [string, number]; +export const USE_EMULATOR = new InjectionToken<ɵUseEmulatorArguments>('angularfire2.firestore.use-emulator'); + +/** + * A utility methods for associating a collection reference with + * a query. + * + * @param collectionRef - A collection reference to query + * @param queryFn - The callback to create a query + * + * Example: + * const { query, ref } = associateQuery(docRef.collection('items'), ref => { + * return ref.where('age', '<', 200); + * }); + */ +export function associateQuery(collectionRef: CollectionReference, queryFn = ref => ref): AssociatedReference { + const query = queryFn(collectionRef); + const ref = collectionRef; + return { query, ref }; +} + +export interface AngularFirestore extends Omit<ɵPromiseProxy, 'doc' | 'collection' | 'collectionGroup'> {} + +/** + * AngularFirestore Service + * + * This service is the main entry point for this feature module. It provides + * an API for creating Collection and Reference services. These services can + * then be used to do data updates and observable streams of the data. + * + * Example: + * + * import { Component } from '@angular/core'; + * import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from '@angular/fire/firestore'; + * import { Observable } from 'rxjs/Observable'; + * import { from } from 'rxjs/observable'; + * + * @Component({ + * selector: 'app-my-component', + * template: ` + *

Items for {{ (profile | async)?.name }} + *
    + *
  • {{ item.name }}
  • + *
+ *
+ * + * + *
+ * ` + * }) + * export class MyComponent implements OnInit { + * + * // services for data operations and data streaming + * private readonly itemsRef: AngularFirestoreCollection; + * private readonly profileRef: AngularFirestoreDocument; + * + * // observables for template + * items: Observable; + * profile: Observable; + * + * // inject main service + * constructor(private readonly afs: AngularFirestore) {} + * + * ngOnInit() { + * this.itemsRef = afs.collection('items', ref => ref.where('user', '==', 'davideast').limit(10)); + * this.items = this.itemsRef.valueChanges().map(snap => snap.docs.map(data => doc.data())); + * // this.items = from(this.itemsRef); // you can also do this with no mapping + * + * this.profileRef = afs.doc('users/davideast'); + * this.profile = this.profileRef.valueChanges(); + * } + * + * addItem(name: string) { + * const user = 'davideast'; + * this.itemsRef.add({ name, user }); + * } + * } + */ +@Injectable({ + providedIn: 'any' +}) +export class AngularFirestore { + + public readonly persistenceEnabled$: Observable; + public readonly schedulers: ɵAngularFireSchedulers; + public readonly keepUnstableUntilFirst: (obs: Observable) => Observable; + + /** + * Create a reference to a Firestore Collection based on a path or + * CollectionReference and an optional query function to narrow the result + * set. + */ + public readonly collection: (pathOrRef: string | CollectionReference, queryFn?: QueryFn) => AngularFirestoreCollection; + + /** + * Create a reference to a Firestore Collection Group based on a collectionId + * and an optional query function to narrow the result + * set. + */ + public readonly collectionGroup: (collectionId: string, queryGroupFn?: QueryGroupFn) => AngularFirestoreCollectionGroup; + + /** + * Create a reference to a Firestore Document based on a path or + * DocumentReference. Note that documents are not queryable because they are + * simply objects. However, documents have sub-collections that return a + * Collection reference and can be queried. + */ + public readonly doc: (pathOrRef: string | DocumentReference) => AngularFirestoreDocument; + + /** + * Returns a generated Firestore Document Id. + */ + public readonly createId = () => newId(); + + constructor( + @Inject(FIREBASE_OPTIONS) options: FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig: string | FirebaseAppConfig | null | undefined, + @Optional() @Inject(ENABLE_PERSISTENCE) shouldEnablePersistence: boolean | null, + @Optional() @Inject(SETTINGS) settings: Settings | null, + // tslint:disable-next-line:ban-types + @Inject(PLATFORM_ID) platformId: Object, + zone: NgZone, + @Optional() @Inject(PERSISTENCE_SETTINGS) persistenceSettings: PersistenceSettings | null, + @Optional() @Inject(USE_EMULATOR) _useEmulator: any, + ) { + this.schedulers = new ɵAngularFireSchedulers(zone); + this.keepUnstableUntilFirst = ɵkeepUnstableUntilFirstFactory(this.schedulers); + + const firestoreAndPersistenceEnabled = of(undefined).pipe( + observeOn(this.schedulers.outsideAngular), + // TODO wait for AngularFireAuth if it's available + switchMap(() => zone.runOutsideAngular(() => import('firebase/firestore'))), + map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)), + map(app => zone.runOutsideAngular(() => { + const useEmulator: ɵUseEmulatorArguments | null = _useEmulator; + return ɵfetchInstance(`${app.name}.firestore`, 'AngularFirestore', app, () => { + const firestore = zone.runOutsideAngular(() => app.firestore()); + if (settings) { + firestore.settings(settings); + } + if (useEmulator) { + firestore.useEmulator(...useEmulator); + } + + if (shouldEnablePersistence && !isPlatformServer(platformId)) { + // We need to try/catch here because not all enablePersistence() failures are caught + // https://github.com/firebase/firebase-js-sdk/issues/608 + const enablePersistence = () => { + try { + return from(firestore.enablePersistence(persistenceSettings || undefined).then(() => true, () => false)); + } catch (e) { + if (typeof console !== 'undefined') { console.warn(e); } + return of(false); + } + }; + return [firestore, zone.runOutsideAngular(enablePersistence)]; + } else { + return [firestore, of(false)]; + } + + }, [settings, useEmulator, shouldEnablePersistence]); + })), + shareReplay({ bufferSize: 1, refCount: false }), + ); + + const firestore = firestoreAndPersistenceEnabled.pipe(map(([firestore]) => firestore as firebase.firestore.Firestore)); + this.persistenceEnabled$ = firestoreAndPersistenceEnabled.pipe(switchMap(([_, it]) => it as Observable)); + + this.collection = (pathOrRef: string | CollectionReference, queryFn?: QueryFn) => { + const zoneAndQuery = firestore.pipe(map(firestoreInstance => { + let collectionRef: CollectionReference; + if (typeof pathOrRef === 'string') { + collectionRef = firestoreInstance.collection(pathOrRef) as firebase.firestore.CollectionReference; + } else { + collectionRef = pathOrRef; + } + return associateQuery(collectionRef, queryFn); + })); + const ref = zoneAndQuery.pipe(map(it => this.schedulers.ngZone.run(() => it.ref))); + const query = zoneAndQuery.pipe(map(it => it.query)); + return new AngularFirestoreCollection(ref, query, this); + }; + + this.doc = (pathOrRef: string | DocumentReference) => { + const ref = firestore.pipe( + map(firestoreInstance => { + if (typeof pathOrRef === 'string') { + return firestoreInstance.doc(pathOrRef) as DocumentReference; + } else { + return pathOrRef; + } + }), + map(ref => this.schedulers.ngZone.run(() => ref)) + ); + return new AngularFirestoreDocument(ref, this); + }; + + this.collectionGroup = (collectionId: string, queryGroupFn?: QueryGroupFn) => { + const queryFn = queryGroupFn || (ref => ref); + const query = firestore.pipe(map(firestoreInstance => { + const collectionGroup: Query = firestoreInstance.collectionGroup(collectionId) as firebase.firestore.Query; + return queryFn(collectionGroup); + })); + return new AngularFirestoreCollectionGroup(query, this); + }; + + return ɵlazySDKProxy(this, firestore, zone); + + } + +} + +ɵapplyMixins(AngularFirestore, [proxyPolyfillCompat]); diff --git a/src/firestore-lazy/index.ts b/src/firestore-lazy/index.ts new file mode 100644 index 000000000..62bd09bbe --- /dev/null +++ b/src/firestore-lazy/index.ts @@ -0,0 +1,6 @@ +// DO NOT MODIFY. This entry point is intended only for the side-effect import +// for firebase/firestore, it's here so Firestore variants can be used in other +// entry points. Ensure all APIs are exported on ./public_api as that's what +// the other entry points reexport. +export * from './public_api'; +export * from './firestore.module'; diff --git a/src/firestore-lazy/interfaces.ts b/src/firestore-lazy/interfaces.ts new file mode 100644 index 000000000..e512d7f1c --- /dev/null +++ b/src/firestore-lazy/interfaces.ts @@ -0,0 +1,86 @@ +import { Subscriber } from 'rxjs'; +import firebase from 'firebase/app'; + +export type Settings = firebase.firestore.Settings; +export type CollectionReference = firebase.firestore.CollectionReference; +export type DocumentReference = firebase.firestore.DocumentReference; +export type PersistenceSettings = firebase.firestore.PersistenceSettings; +export type DocumentChangeType = firebase.firestore.DocumentChangeType; +export type SnapshotOptions = firebase.firestore.SnapshotOptions; +export type FieldPath = firebase.firestore.FieldPath; +export type Query = firebase.firestore.Query; + +export type SetOptions = firebase.firestore.SetOptions; +export type DocumentData = firebase.firestore.DocumentData; + +export interface DocumentSnapshotExists extends firebase.firestore.DocumentSnapshot { + readonly exists: true; + data(options?: SnapshotOptions): T; +} + +export interface DocumentSnapshotDoesNotExist extends firebase.firestore.DocumentSnapshot { + readonly exists: false; + data(options?: SnapshotOptions): undefined; + get(fieldPath: string | FieldPath, options?: SnapshotOptions): undefined; +} + +export type DocumentSnapshot = DocumentSnapshotExists | DocumentSnapshotDoesNotExist; + +export interface QueryDocumentSnapshot extends firebase.firestore.QueryDocumentSnapshot { + data(options?: SnapshotOptions): T; +} + +export interface QuerySnapshot extends firebase.firestore.QuerySnapshot { + readonly docs: QueryDocumentSnapshot[]; +} + +export interface DocumentChange extends firebase.firestore.DocumentChange { + readonly doc: QueryDocumentSnapshot; +} + +export interface DocumentChangeAction { + type: DocumentChangeType; + payload: DocumentChange; +} + +export interface Action { + type: string; + payload: T; +} + +export interface Reference { + onSnapshot: (options: firebase.firestore.SnapshotListenOptions, sub: Subscriber) => any; +} + +// A convience type for making a query. +// Example: const query = (ref) => ref.where('name', == 'david'); +export type QueryFn = (ref: CollectionReference) => Query; + +export type QueryGroupFn = (query: Query) => Query; + +/** + * A structure that provides an association between a reference + * and a query on that reference. Note: Performing operations + * on the reference can lead to confusing results with complicated + * queries. + * + * Example: + * + * const query = ref.where('type', '==', 'Book'). + * .where('price', '>' 18.00) + * .where('price', '<' 100.00) + * .where('category', '==', 'Fiction') + * .where('publisher', '==', 'BigPublisher') + * + * // This addition would not be a result of the query above + * ref.add({ + * type: 'Magazine', + * price: 4.99, + * category: 'Sports', + * publisher: 'SportsPublisher' + * }); + */ +export interface AssociatedReference { + ref: CollectionReference; + query: Query; +} diff --git a/src/firestore-lazy/memory/ng-package.json b/src/firestore-lazy/memory/ng-package.json new file mode 100644 index 000000000..e122fa715 --- /dev/null +++ b/src/firestore-lazy/memory/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "../firestore-memory.ts" + } +} diff --git a/src/firestore/observable/fromRef.ts b/src/firestore-lazy/observable/fromRef.ts similarity index 100% rename from src/firestore/observable/fromRef.ts rename to src/firestore-lazy/observable/fromRef.ts diff --git a/src/firestore-lazy/package.json b/src/firestore-lazy/package.json new file mode 100644 index 000000000..277850f56 --- /dev/null +++ b/src/firestore-lazy/package.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../node_modules/ng-packagr/package.schema.json", + "ngPackage": { + "lib": { + "entryFile": "index.ts", + "umdModuleIds": { + "firebase/app": "firebase", + "@firebase/firestore": "firebase-firestore" + } + } + } +} diff --git a/src/firestore-lazy/public_api.ts b/src/firestore-lazy/public_api.ts new file mode 100644 index 000000000..6f2906f4a --- /dev/null +++ b/src/firestore-lazy/public_api.ts @@ -0,0 +1,10 @@ + +import 'firebase/firestore'; // removed in build process when not UMD + +export * from './firestore'; +export * from './collection/collection'; +export * from './collection-group/collection-group'; +export * from './document/document'; +export * from './collection/changes'; +export * from './observable/fromRef'; +export * from './interfaces'; diff --git a/src/firestore-lazy/util.ts b/src/firestore-lazy/util.ts new file mode 100644 index 000000000..5e6af715c --- /dev/null +++ b/src/firestore-lazy/util.ts @@ -0,0 +1,44 @@ + +function randomBytes(nBytes: number): Uint8Array { + // Polyfills for IE and WebWorker by using `self` and `msCrypto` when `crypto` is not available. + const crypto = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + typeof self !== 'undefined' && (self.crypto || (self as any).msCrypto); + if ((crypto as any).generateRandomBytes) { + return (crypto as any).generateRandomBytes(nBytes); + } + const bytes = new Uint8Array(nBytes); + if (crypto && typeof crypto.getRandomValues === 'function') { + crypto.getRandomValues(bytes); + } else { + // Falls back to Math.random + for (let i = 0; i < nBytes; i++) { + bytes[i] = Math.floor(Math.random() * 256); + } + } + return bytes; +} + +// just grabbed this from Firestore, so we don't need to await +export function newId() { + // Alphanumeric characters + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + // The largest byte value that is a multiple of `char.length`. + const maxMultiple = Math.floor(256 / chars.length) * chars.length; + + let autoId = ''; + const targetLength = 20; + while (autoId.length < targetLength) { + const bytes = randomBytes(40); + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < bytes.length; ++i) { + // Only accept values that are [0, maxMultiple), this ensures they can + // be evenly mapped to indices of `chars` via a modulo operation. + if (autoId.length < targetLength && bytes[i] < maxMultiple) { + autoId += chars.charAt(bytes[i] % chars.length); + } + } + } + return autoId; +} diff --git a/src/firestore-lazy/utils.spec.ts b/src/firestore-lazy/utils.spec.ts new file mode 100644 index 000000000..87bb97b00 --- /dev/null +++ b/src/firestore-lazy/utils.spec.ts @@ -0,0 +1,57 @@ +import firebase from 'firebase/app'; +import { AngularFirestoreCollection } from './collection/collection'; + +export interface Stock { + name: string; + price: number; +} + +export const FAKE_STOCK_DATA = { name: 'FAKE', price: 1 }; + +export const randomName = (firestore): string => firestore.collection('a').doc().id; + +export const createRandomStocks = async ( + firestore: firebase.firestore.Firestore, + collectionRef: firebase.firestore.CollectionReference, + numberOfItems +) => { + // Create a batch to update everything at once + const batch = firestore.batch(); + // Store the random names to delete them later + const count = 0; + let names: string[] = []; + Array.from(Array(numberOfItems)).forEach((a, i) => { + const name = randomName(firestore); + batch.set(collectionRef.doc(name), FAKE_STOCK_DATA); + names = [...names, name]; + }); + // Create the batch entries + // Commit! + await batch.commit(); + return names; +}; + +export function deleteThemAll(names, ref) { + const promises = names.map(name => ref.doc(name).delete()); + return Promise.all(promises); +} + +export function delayUpdate(collection: AngularFirestoreCollection|firebase.firestore.CollectionReference, path, data, delay = 250) { + setTimeout(() => { + collection.doc(path).update(data); + }, delay); +} + +export function delayAdd(collection: AngularFirestoreCollection|firebase.firestore.CollectionReference, path, data, delay = 250) { + setTimeout(() => { + collection.doc(path).set(data); + }, delay); +} + +export function delayDelete(collection: AngularFirestoreCollection|firebase.firestore.CollectionReference, path, delay = 250) { + setTimeout(() => { + collection.doc(path).delete(); + }, delay); +} + +export const rando = () => (Math.random() + 1).toString(36).substring(7); diff --git a/src/firestore/collection-group/collection-group.ts b/src/firestore/collection-group/collection-group.ts index 5e8ed5437..cc14031d8 100644 --- a/src/firestore/collection-group/collection-group.ts +++ b/src/firestore/collection-group/collection-group.ts @@ -1,11 +1,9 @@ import { from, Observable } from 'rxjs'; -import { fromCollectionRef } from '../observable/fromRef'; +import { fromCollectionRef, docChanges, sortedChanges } from '@angular/fire/firestore-lazy'; import { filter, map, observeOn, scan } from 'rxjs/operators'; import firebase from 'firebase/app'; - import { DocumentChangeAction, DocumentChangeType, DocumentData, Query } from '../interfaces'; import { validateEventsArray } from '../collection/collection'; -import { docChanges, sortedChanges } from '../collection/changes'; import { AngularFirestore } from '../firestore'; /** diff --git a/src/firestore/collection/collection.ts b/src/firestore/collection/collection.ts index 07c4e2131..077ea053e 100644 --- a/src/firestore/collection/collection.ts +++ b/src/firestore/collection/collection.ts @@ -1,10 +1,8 @@ import { from, Observable } from 'rxjs'; -import { fromCollectionRef } from '../observable/fromRef'; +import { fromCollectionRef, docChanges, sortedChanges } from '@angular/fire/firestore-lazy'; import { filter, map, observeOn, scan } from 'rxjs/operators'; import firebase from 'firebase/app'; - import { CollectionReference, DocumentChangeAction, DocumentChangeType, DocumentData, DocumentReference, Query } from '../interfaces'; -import { docChanges, sortedChanges } from './changes'; import { AngularFirestoreDocument } from '../document/document'; import { AngularFirestore } from '../firestore'; diff --git a/src/firestore/document/document.ts b/src/firestore/document/document.ts index f0a7ab992..c8dcf3d12 100644 --- a/src/firestore/document/document.ts +++ b/src/firestore/document/document.ts @@ -1,6 +1,6 @@ import { from, Observable } from 'rxjs'; import { Action, DocumentData, DocumentReference, DocumentSnapshot, QueryFn, SetOptions } from '../interfaces'; -import { fromDocRef } from '../observable/fromRef'; +import { fromDocRef } from '@angular/fire/firestore-lazy'; import { map, observeOn } from 'rxjs/operators'; import { AngularFirestore, associateQuery } from '../firestore'; import { AngularFirestoreCollection } from '../collection/collection'; diff --git a/src/firestore/firestore-memory.module.ts b/src/firestore/firestore-memory.module.ts new file mode 100644 index 000000000..a0c3c06e0 --- /dev/null +++ b/src/firestore/firestore-memory.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { AngularFirestore } from './firestore'; + +import 'firebase/firestore/memory'; + +@NgModule({ + providers: [ AngularFirestore ] +}) +export class AngularFirestoreModule { + // firebase/firestore/memory does not have persistence capabilities +} diff --git a/src/firestore/firestore-memory.ts b/src/firestore/firestore-memory.ts new file mode 100644 index 000000000..db678e004 --- /dev/null +++ b/src/firestore/firestore-memory.ts @@ -0,0 +1,3 @@ +// See index.ts, this variant is built by ./memory/ng-package.json +export * from './public_api'; +export * from './firestore-memory.module'; diff --git a/src/firestore/firestore.module.ts b/src/firestore/firestore.module.ts index f770fc38c..7323f05b1 100644 --- a/src/firestore/firestore.module.ts +++ b/src/firestore/firestore.module.ts @@ -1,7 +1,9 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; -import { PersistenceSettings } from './interfaces'; +import { PersistenceSettings } from '@angular/fire/firestore-lazy'; import { AngularFirestore, ENABLE_PERSISTENCE, PERSISTENCE_SETTINGS } from './firestore'; +import 'firebase/firestore'; + @NgModule({ providers: [ AngularFirestore ] }) diff --git a/src/firestore/firestore.ts b/src/firestore/firestore.ts index cbf68b8cb..de0b34307 100644 --- a/src/firestore/firestore.ts +++ b/src/firestore/firestore.ts @@ -1,7 +1,6 @@ -import { Inject, Injectable, InjectionToken, NgZone, Optional, PLATFORM_ID } from '@angular/core'; +import { Inject, Injectable, NgZone, Optional, PLATFORM_ID } from '@angular/core'; import { from, Observable, of } from 'rxjs'; import { - AssociatedReference, CollectionReference, DocumentReference, PersistenceSettings, @@ -21,50 +20,13 @@ import { ɵAngularFireSchedulers, ɵfirebaseAppFactory, ɵkeepUnstableUntilFirstFactory, - FirebaseApp } from '@angular/fire'; import { isPlatformServer } from '@angular/common'; import firebase from 'firebase/app'; -import 'firebase/firestore'; import { USE_EMULATOR as USE_AUTH_EMULATOR } from '@angular/fire/auth'; import { ɵfetchInstance, ɵlogAuthEmulatorError } from '@angular/fire'; - -/** - * The value of this token determines whether or not the firestore will have persistance enabled - */ -export const ENABLE_PERSISTENCE = new InjectionToken('angularfire2.enableFirestorePersistence'); -export const PERSISTENCE_SETTINGS = new InjectionToken('angularfire2.firestore.persistenceSettings'); -export const SETTINGS = new InjectionToken('angularfire2.firestore.settings'); - -// SEMVER(7): use Parameters to detirmine the useEmulator arguments -// type UseEmulatorArguments = Parameters; -type UseEmulatorArguments = [string, number]; -export const USE_EMULATOR = new InjectionToken('angularfire2.firestore.use-emulator'); - -/** - * A utility methods for associating a collection reference with - * a query. - * - * @param collectionRef - A collection reference to query - * @param queryFn - The callback to create a query - * - * Example: - * const { query, ref } = associateQuery(docRef.collection('items'), ref => { - * return ref.where('age', '<', 200); - * }); - */ -export function associateQuery(collectionRef: CollectionReference, queryFn = ref => ref): AssociatedReference { - const query = queryFn(collectionRef); - const ref = collectionRef; - return { query, ref }; -} - -type InstanceCache = Map; +import { ENABLE_PERSISTENCE, PERSISTENCE_SETTINGS, SETTINGS, USE_EMULATOR, associateQuery, ɵUseEmulatorArguments } from '@angular/fire/firestore-lazy'; +export { ENABLE_PERSISTENCE, PERSISTENCE_SETTINGS, SETTINGS, USE_EMULATOR, associateQuery }; /** * AngularFirestore Service @@ -154,7 +116,7 @@ export class AngularFirestore { if (!firebase.auth && useAuthEmulator) { ɵlogAuthEmulatorError(); } - const useEmulator: UseEmulatorArguments | null = _useEmulator; + const useEmulator: ɵUseEmulatorArguments | null = _useEmulator; [this.firestore, this.persistenceEnabled$] = ɵfetchInstance(`${app.name}.firestore`, 'AngularFirestore', app, () => { const firestore = zone.runOutsideAngular(() => app.firestore()); @@ -165,6 +127,9 @@ export class AngularFirestore { firestore.useEmulator(...useEmulator); } + // TODO can I tell if they are using the memory-only variant? would skip the warning if they + // try to enable persistence via DI. Also I could add a console.info suggesting memory-only + // if they aren't using it & not trying to enable persistence. if (shouldEnablePersistence && !isPlatformServer(platformId)) { // We need to try/catch here because not all enablePersistence() failures are caught // https://github.com/firebase/firebase-js-sdk/issues/608 diff --git a/src/firestore/index.ts b/src/firestore/index.ts new file mode 100644 index 000000000..62bd09bbe --- /dev/null +++ b/src/firestore/index.ts @@ -0,0 +1,6 @@ +// DO NOT MODIFY. This entry point is intended only for the side-effect import +// for firebase/firestore, it's here so Firestore variants can be used in other +// entry points. Ensure all APIs are exported on ./public_api as that's what +// the other entry points reexport. +export * from './public_api'; +export * from './firestore.module'; diff --git a/src/firestore/interfaces.ts b/src/firestore/interfaces.ts index e512d7f1c..92163b9d6 100644 --- a/src/firestore/interfaces.ts +++ b/src/firestore/interfaces.ts @@ -1,86 +1,24 @@ -import { Subscriber } from 'rxjs'; -import firebase from 'firebase/app'; - -export type Settings = firebase.firestore.Settings; -export type CollectionReference = firebase.firestore.CollectionReference; -export type DocumentReference = firebase.firestore.DocumentReference; -export type PersistenceSettings = firebase.firestore.PersistenceSettings; -export type DocumentChangeType = firebase.firestore.DocumentChangeType; -export type SnapshotOptions = firebase.firestore.SnapshotOptions; -export type FieldPath = firebase.firestore.FieldPath; -export type Query = firebase.firestore.Query; - -export type SetOptions = firebase.firestore.SetOptions; -export type DocumentData = firebase.firestore.DocumentData; - -export interface DocumentSnapshotExists extends firebase.firestore.DocumentSnapshot { - readonly exists: true; - data(options?: SnapshotOptions): T; -} - -export interface DocumentSnapshotDoesNotExist extends firebase.firestore.DocumentSnapshot { - readonly exists: false; - data(options?: SnapshotOptions): undefined; - get(fieldPath: string | FieldPath, options?: SnapshotOptions): undefined; -} - -export type DocumentSnapshot = DocumentSnapshotExists | DocumentSnapshotDoesNotExist; - -export interface QueryDocumentSnapshot extends firebase.firestore.QueryDocumentSnapshot { - data(options?: SnapshotOptions): T; -} - -export interface QuerySnapshot extends firebase.firestore.QuerySnapshot { - readonly docs: QueryDocumentSnapshot[]; -} - -export interface DocumentChange extends firebase.firestore.DocumentChange { - readonly doc: QueryDocumentSnapshot; -} - -export interface DocumentChangeAction { - type: DocumentChangeType; - payload: DocumentChange; -} - -export interface Action { - type: string; - payload: T; -} - -export interface Reference { - onSnapshot: (options: firebase.firestore.SnapshotListenOptions, sub: Subscriber) => any; -} - -// A convience type for making a query. -// Example: const query = (ref) => ref.where('name', == 'david'); -export type QueryFn = (ref: CollectionReference) => Query; - -export type QueryGroupFn = (query: Query) => Query; - -/** - * A structure that provides an association between a reference - * and a query on that reference. Note: Performing operations - * on the reference can lead to confusing results with complicated - * queries. - * - * Example: - * - * const query = ref.where('type', '==', 'Book'). - * .where('price', '>' 18.00) - * .where('price', '<' 100.00) - * .where('category', '==', 'Fiction') - * .where('publisher', '==', 'BigPublisher') - * - * // This addition would not be a result of the query above - * ref.add({ - * type: 'Magazine', - * price: 4.99, - * category: 'Sports', - * publisher: 'SportsPublisher' - * }); - */ -export interface AssociatedReference { - ref: CollectionReference; - query: Query; -} +export { + Settings, + CollectionReference, + DocumentReference, + PersistenceSettings, + DocumentChangeType, + SnapshotOptions, + FieldPath, + Query, + SetOptions, + DocumentData, + DocumentSnapshotExists, + DocumentSnapshotDoesNotExist, + DocumentSnapshot, + QueryDocumentSnapshot, + QuerySnapshot, + DocumentChange, + DocumentChangeAction, + Action, + Reference, + QueryFn, + QueryGroupFn, + AssociatedReference, +} from '@angular/fire/firestore-lazy'; diff --git a/src/firestore/memory/ng-package.json b/src/firestore/memory/ng-package.json new file mode 100644 index 000000000..e122fa715 --- /dev/null +++ b/src/firestore/memory/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "../firestore-memory.ts" + } +} diff --git a/src/firestore/package.json b/src/firestore/package.json index 75b34a51b..277850f56 100644 --- a/src/firestore/package.json +++ b/src/firestore/package.json @@ -2,7 +2,7 @@ "$schema": "../../node_modules/ng-packagr/package.schema.json", "ngPackage": { "lib": { - "entryFile": "public_api.ts", + "entryFile": "index.ts", "umdModuleIds": { "firebase/app": "firebase", "@firebase/firestore": "firebase-firestore" diff --git a/src/firestore/public_api.ts b/src/firestore/public_api.ts index 1670f1451..681f64953 100644 --- a/src/firestore/public_api.ts +++ b/src/firestore/public_api.ts @@ -1,8 +1,7 @@ export * from './firestore'; -export * from './firestore.module'; export * from './collection/collection'; export * from './collection-group/collection-group'; export * from './document/document'; -export * from './collection/changes'; -export * from './observable/fromRef'; +export { docChanges, sortedChanges, combineChanges, combineChange } from '@angular/fire/firestore-lazy'; +export { fromRef, fromDocRef, fromCollectionRef } from '@angular/fire/firestore-lazy'; export * from './interfaces'; diff --git a/src/performance/performance.service.ts b/src/performance/performance.service.ts index 94c532884..913204b0c 100644 --- a/src/performance/performance.service.ts +++ b/src/performance/performance.service.ts @@ -2,12 +2,12 @@ import { ApplicationRef, Injectable, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; import { first, tap } from 'rxjs/operators'; -const IS_STABLE_START_MARK = '_isStableStart'; +const IS_STABLE_START_MARK = 'Zone'; // use Zone.js's mark const IS_STABLE_END_MARK = '_isStableEnd'; +const PREBOOT_COMPLETE_END_MARK = '_prebootComplete'; function markStarts() { if (typeof(window) !== 'undefined' && window.performance) { - window.performance.mark(IS_STABLE_START_MARK); return true; } else { return false; @@ -23,6 +23,7 @@ export class PerformanceMonitoringService implements OnDestroy { constructor(appRef: ApplicationRef) { if (started) { + this.disposable = appRef.isStable.pipe( first(it => it), tap(() => { @@ -30,7 +31,13 @@ export class PerformanceMonitoringService implements OnDestroy { window.performance.measure('isStable', IS_STABLE_START_MARK, IS_STABLE_END_MARK); }) ).subscribe(); + + window.document.addEventListener('PrebootComplete', () => { + window.performance.mark(PREBOOT_COMPLETE_END_MARK); + window.performance.measure('prebootComplete', IS_STABLE_START_MARK, PREBOOT_COMPLETE_END_MARK); + }); } + } ngOnDestroy() { diff --git a/src/root.spec.ts b/src/root.spec.ts index 36a35b732..25af31f43 100644 --- a/src/root.spec.ts +++ b/src/root.spec.ts @@ -7,6 +7,10 @@ export * from './firestore/firestore.spec'; export * from './firestore/document/document.spec'; export * from './firestore/collection/collection.spec'; export * from './firestore/collection-group/collection-group.spec'; +export * from './firestore-lazy/firestore.spec'; +export * from './firestore-lazy/document/document.spec'; +export * from './firestore-lazy/collection/collection.spec'; +export * from './firestore-lazy/collection-group/collection-group.spec'; export * from './functions/functions.spec'; export * from './database/database.spec'; export * from './database/utils.spec'; @@ -15,7 +19,15 @@ export * from './database/list/changes.spec'; export * from './database/list/snapshot-changes.spec'; export * from './database/list/state-changes.spec'; export * from './database/list/audit-trail.spec'; +export * from './database-lazy/database.spec'; +export * from './database-lazy/utils.spec'; +export * from './database-lazy/observable/fromRef.spec'; +export * from './database-lazy/list/changes.spec'; +export * from './database-lazy/list/snapshot-changes.spec'; +export * from './database-lazy/list/state-changes.spec'; +export * from './database-lazy/list/audit-trail.spec'; export * from './messaging/messaging.spec'; export * from './remote-config/remote-config.spec'; export * from './storage/storage.spec'; +export * from './storage-lazy/storage.spec'; export * from './performance/performance.spec'; diff --git a/src/storage-lazy/interfaces.ts b/src/storage-lazy/interfaces.ts new file mode 100644 index 000000000..1f1265065 --- /dev/null +++ b/src/storage-lazy/interfaces.ts @@ -0,0 +1,10 @@ +import firebase from 'firebase/app'; + +export type UploadTask = firebase.storage.UploadTask; +export type UploadTaskSnapshot = firebase.storage.UploadTaskSnapshot; +export type UploadMetadata = firebase.storage.UploadMetadata; + +export type SettableMetadata = firebase.storage.SettableMetadata; +export type Reference = firebase.storage.Reference; +export type StringFormat = firebase.storage.StringFormat; +export type ListResult = firebase.storage.ListResult; diff --git a/src/storage-lazy/observable/fromTask.ts b/src/storage-lazy/observable/fromTask.ts new file mode 100644 index 000000000..a049bc1d3 --- /dev/null +++ b/src/storage-lazy/observable/fromTask.ts @@ -0,0 +1,33 @@ +import { Observable } from 'rxjs'; +import { UploadTask, UploadTaskSnapshot } from '../interfaces'; +import firebase from 'firebase/app'; + +export function fromTask(task: UploadTask) { + return new Observable(subscriber => { + const progress = (snap: UploadTaskSnapshot) => subscriber.next(snap); + const error = e => subscriber.error(e); + const complete = () => subscriber.complete(); + progress(task.snapshot); + switch (task.snapshot.state) { + case firebase.storage.TaskState.SUCCESS: + case firebase.storage.TaskState.CANCELED: + complete(); + break; + case firebase.storage.TaskState.ERROR: + error(new Error('task was already in error state')); + break; + default: + // on's type if Function, rather than () => void, need to wrap + const unsub = task.on('state_changed', progress, (e) => { + progress(task.snapshot); + error(e); + }, () => { + progress(task.snapshot); + complete(); + }); + return function unsubscribe() { + unsub(); + }; + } + }); +} diff --git a/src/storage-lazy/package.json b/src/storage-lazy/package.json new file mode 100644 index 000000000..252f86ad2 --- /dev/null +++ b/src/storage-lazy/package.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../node_modules/ng-packagr/package.schema.json", + "ngPackage": { + "lib": { + "entryFile": "public_api.ts", + "umdModuleIds": { + "firebase/app": "firebase", + "@firebase/storage": "firebase-storage" + } + } + } +} diff --git a/src/storage/pipes/storageUrl.pipe.ts b/src/storage-lazy/pipes/storageUrl.pipe.ts similarity index 66% rename from src/storage/pipes/storageUrl.pipe.ts rename to src/storage-lazy/pipes/storageUrl.pipe.ts index 1aaf2caeb..03731965f 100644 --- a/src/storage/pipes/storageUrl.pipe.ts +++ b/src/storage-lazy/pipes/storageUrl.pipe.ts @@ -1,6 +1,8 @@ import { AsyncPipe } from '@angular/common'; import { ChangeDetectorRef, NgModule, OnDestroy, Pipe, PipeTransform } from '@angular/core'; -import { Observable } from 'rxjs'; +import { makeStateKey, TransferState } from '@angular/platform-browser'; +import { Observable, of } from 'rxjs'; +import { tap } from 'rxjs/operators'; import { AngularFireStorage } from '../storage'; /** to be used with in combination with | async */ @@ -14,14 +16,18 @@ export class GetDownloadURLPipe implements PipeTransform, OnDestroy { private path: string; private downloadUrl$: Observable; - constructor(private storage: AngularFireStorage, cdr: ChangeDetectorRef) { + constructor(private storage: AngularFireStorage, cdr: ChangeDetectorRef, private state: TransferState) { this.asyncPipe = new AsyncPipe(cdr); } transform(path: string) { if (path !== this.path) { this.path = path; - this.downloadUrl$ = this.storage.ref(path).getDownloadURL(); + const key = makeStateKey(`|getDownloadURL|${path}`); + const existing = this.state.get(key, undefined); + this.downloadUrl$ = existing ? of(existing) : this.storage.ref(path).getDownloadURL().pipe( + tap(it => this.state.set(key, it)) + ); } return this.asyncPipe.transform(this.downloadUrl$); } diff --git a/src/storage-lazy/public_api.ts b/src/storage-lazy/public_api.ts new file mode 100644 index 000000000..33fc87ea1 --- /dev/null +++ b/src/storage-lazy/public_api.ts @@ -0,0 +1,9 @@ + +import 'firebase/storage'; // trimmed out by build script + +export * from './ref'; +export * from './storage'; +export * from './task'; +export * from './observable/fromTask'; +export * from './storage.module'; +export * from './pipes/storageUrl.pipe'; diff --git a/src/storage-lazy/ref.ts b/src/storage-lazy/ref.ts new file mode 100644 index 000000000..c0bc80a71 --- /dev/null +++ b/src/storage-lazy/ref.ts @@ -0,0 +1,51 @@ +import { ListResult, Reference, SettableMetadata, StringFormat, UploadMetadata } from './interfaces'; +import { AngularFireUploadTask, createUploadTask } from './task'; +import { from, Observable, of } from 'rxjs'; +import { ɵAngularFireSchedulers } from '@angular/fire'; +import { map, observeOn, switchMap } from 'rxjs/operators'; + +export interface AngularFireStorageReference { + getDownloadURL(): Observable; + getMetadata(): Observable; + delete(): Observable; + child(path: string): AngularFireStorageReference; + updateMetadata(meta: SettableMetadata): Observable; + put(data: any, metadata?: UploadMetadata | undefined): AngularFireUploadTask; + putString(data: string, format?: string | undefined, metadata?: UploadMetadata | undefined): AngularFireUploadTask; + listAll(): Observable; +} + +/** + * Create an AngularFire wrapped Storage Reference. This object + * creates observable methods from promise based methods. + */ +export function createStorageRef( + ref$: Observable, + schedulers: ɵAngularFireSchedulers, + keepUnstableUntilFirst: (obs$: Observable) => Observable +): AngularFireStorageReference { + return { + getDownloadURL: () => ref$.pipe( + observeOn(schedulers.outsideAngular), + switchMap(ref => ref.getDownloadURL()), + keepUnstableUntilFirst + ), + getMetadata: () => ref$.pipe( + observeOn(schedulers.outsideAngular), + switchMap(ref => ref.getMetadata()), + keepUnstableUntilFirst + ), + delete: () => ref$.pipe(switchMap(ref => ref.delete())), + child: (path: string) => createStorageRef(ref$.pipe(map(ref => ref.child(path))), schedulers, keepUnstableUntilFirst), + updateMetadata: (meta: SettableMetadata) => ref$.pipe(switchMap(ref => ref.updateMetadata(meta))), + put: (data: any, metadata?: UploadMetadata) => { + const task = ref$.pipe(map(ref => ref.put(data, metadata))); + return createUploadTask(task); + }, + putString: (data: string, format?: StringFormat, metadata?: UploadMetadata) => { + const task = ref$.pipe(map(ref => ref.putString(data, format, metadata))); + return createUploadTask(task); + }, + listAll: () => ref$.pipe(switchMap(ref => ref.listAll())) + }; +} diff --git a/src/storage-lazy/storage.module.ts b/src/storage-lazy/storage.module.ts new file mode 100644 index 000000000..c6181645f --- /dev/null +++ b/src/storage-lazy/storage.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; +import { GetDownloadURLPipeModule } from './pipes/storageUrl.pipe'; +import { AngularFireStorage } from './storage'; + +@NgModule({ + exports: [ GetDownloadURLPipeModule ], + providers: [ AngularFireStorage ] +}) +export class AngularFireStorageModule { } diff --git a/src/storage-lazy/storage.spec.ts b/src/storage-lazy/storage.spec.ts new file mode 100644 index 000000000..c708cfb0f --- /dev/null +++ b/src/storage-lazy/storage.spec.ts @@ -0,0 +1,231 @@ +import { forkJoin, from } from 'rxjs'; +import { mergeMap, tap } from 'rxjs/operators'; +import { TestBed } from '@angular/core/testing'; +import { AngularFireModule, FIREBASE_APP_NAME, FIREBASE_OPTIONS, FirebaseApp } from '@angular/fire'; +import { AngularFireStorage, AngularFireStorageModule, AngularFireUploadTask, BUCKET } from './public_api'; +import { COMMON_CONFIG } from '../test-config'; +import { rando } from '../firestore/utils.spec'; +import { GetDownloadURLPipe } from './pipes/storageUrl.pipe'; +import { ChangeDetectorRef } from '@angular/core'; +import 'firebase/storage'; + +if (typeof XMLHttpRequest === 'undefined') { + globalThis.XMLHttpRequest = require('xhr2'); +} + +const blobOrBuffer = (data: string, options: {}) => { + if (typeof Blob === 'undefined') { + return Buffer.from(data, 'utf8'); + } else { + return new Blob([JSON.stringify(data)], options); + } +}; + +describe('AngularFireLazyStorage', () => { + let app: FirebaseApp; + let afStorage: AngularFireStorage; + let cdr: ChangeDetectorRef; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFireStorageModule, + ], + providers: [ + ChangeDetectorRef + ] + }); + + app = TestBed.inject(FirebaseApp); + afStorage = TestBed.inject(AngularFireStorage); + cdr = TestBed.inject(ChangeDetectorRef); + }); + + afterEach(() => { + app.delete(); + }); + + it('should exist', () => { + expect(afStorage instanceof AngularFireStorage).toBe(true); + }); + + it('should have the Firebase storage instance', () => { + expect(afStorage.storage).toBeDefined(); + }); + + it('should have an initialized Firebase app', () => { + expect(afStorage.storage.app).toBeDefined(); + }); + + describe('upload task', () => { + + it('should upload and delete a file', (done) => { + const data = { angular: 'fire' }; + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); + const ref = afStorage.ref('af.json'); + const task = ref.put(blob); + task.snapshotChanges() + .subscribe( + snap => { + expect(snap).toBeDefined(); + }, + done.fail, + () => { + ref.delete().subscribe(done, done.fail); + }); + }); + + it('should upload a file and observe the download url', (done) => { + const data = { angular: 'fire' }; + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); + const ref = afStorage.ref('af.json'); + ref.put(blob).then(() => { + const url$ = ref.getDownloadURL(); + url$.subscribe( + url => { + expect(url).toBeDefined(); + }, + done.fail, + () => { + ref.delete().subscribe(done, done.fail); + } + ); + }); + }); + + it('should resolve the task as a promise', (done) => { + const data = { angular: 'promise' }; + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); + const ref = afStorage.ref('af.json'); + const task: AngularFireUploadTask = ref.put(blob); + task.then(snap => { + expect(snap).toBeDefined(); + done(); + }).catch(done.fail); + }); + + }); + + describe('reference', () => { + + it('it should upload, download, and delete', (done) => { + const data = { angular: 'fire' }; + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); + const ref = afStorage.ref('af.json'); + const task = ref.put(blob); + // Wait for the upload + forkJoin([task.snapshotChanges()]) + .pipe( + // get the url download + mergeMap(() => ref.getDownloadURL()), + // assert the URL + tap(url => expect(url).toBeDefined()), + // Delete the file + mergeMap(() => ref.delete()) + ) + // finish the test + .subscribe(done, done.fail); + }); + + it('should upload, get metadata, and delete', (done) => { + const data = { angular: 'fire' }; + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); + const ref = afStorage.ref('af.json'); + const task = ref.put(blob, { customMetadata: { blah: 'blah' } }); + // Wait for the upload + forkJoin([task.snapshotChanges()]) + .pipe( + // get the metadata download + mergeMap(() => ref.getMetadata()), + // assert the URL + tap(meta => expect(meta.customMetadata).toEqual({ blah: 'blah' })), + // Delete the file + mergeMap(() => ref.delete()) + ) + // finish the test + .subscribe(done, done.fail); + }); + + }); + +}); + +describe('AngularFireLazyStorage w/options', () => { + let app: FirebaseApp; + let afStorage: AngularFireStorage; + let firebaseAppName: string; + let storageBucket: string; + + beforeEach(() => { + firebaseAppName = rando(); + storageBucket = 'angularfire2-test2'; + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG, rando()), + AngularFireStorageModule + ], + providers: [ + { provide: FIREBASE_APP_NAME, useValue: firebaseAppName }, + { provide: FIREBASE_OPTIONS, useValue: COMMON_CONFIG }, + { provide: BUCKET, useValue: storageBucket } + ] + }); + + app = TestBed.inject(FirebaseApp); + afStorage = TestBed.inject(AngularFireStorage); + }); + + afterEach(() => { + app.delete(); + }); + + describe('', () => { + + it('should exist', () => { + expect(afStorage instanceof AngularFireStorage).toBe(true); + }); + + it('should have the Firebase storage instance', () => { + expect(afStorage.storage).toBeDefined(); + }); + + it('should have an initialized Firebase app', () => { + expect(afStorage.storage.app).toBeDefined(); + }); + + it('should be hooked up the right app', () => { + expect(afStorage.storage.app.name).toEqual(firebaseAppName); + }); + + it('storage be pointing towards a different bucket', () => { + expect(afStorage.storage.ref().toString()).toEqual(`gs://${storageBucket}/`); + }); + + // TODO tests for Node? + if (typeof Blob !== 'undefined') { + + it('it should upload, download, and delete', (done) => { + const data = { angular: 'fire' }; + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); + const ref = afStorage.ref('af.json'); + const task = ref.put(blob); + // Wait for the upload + forkJoin([task.snapshotChanges()]) + .pipe( + // get the url download + mergeMap(() => ref.getDownloadURL()), + // assert the URL + tap(url => expect(url).toMatch(new RegExp(`https:\\/\\/firebasestorage\\.googleapis\\.com\\/v0\\/b\\/${storageBucket}\\/o\\/af\\.json`))), + // Delete the file + mergeMap(() => ref.delete()) + ) + // finish the test + .subscribe(done, done.fail); + }); + + } + + }); + +}); diff --git a/src/storage-lazy/storage.ts b/src/storage-lazy/storage.ts new file mode 100644 index 000000000..47bc48211 --- /dev/null +++ b/src/storage-lazy/storage.ts @@ -0,0 +1,96 @@ +import { Inject, Injectable, InjectionToken, NgZone, Optional, PLATFORM_ID } from '@angular/core'; +import { AngularFireStorageReference, createStorageRef } from './ref'; +import { Observable, of } from 'rxjs'; +import { + FIREBASE_APP_NAME, + FIREBASE_OPTIONS, + FirebaseAppConfig, + FirebaseOptions, + ɵAngularFireSchedulers, + ɵfetchInstance, + ɵfirebaseAppFactory, + ɵkeepUnstableUntilFirstFactory, + ɵlazySDKProxy, + ɵapplyMixins, + ɵPromiseProxy +} from '@angular/fire'; +import { UploadMetadata } from './interfaces'; +import firebase from 'firebase/app'; +import { map, observeOn, switchMap } from 'rxjs/operators'; +import { AngularFireUploadTask } from './task'; +import { proxyPolyfillCompat } from './base'; + +export const BUCKET = new InjectionToken('angularfire2.storageBucket'); +export const MAX_UPLOAD_RETRY_TIME = new InjectionToken('angularfire2.storage.maxUploadRetryTime'); +export const MAX_OPERATION_RETRY_TIME = new InjectionToken('angularfire2.storage.maxOperationRetryTime'); + +export interface AngularFireStorage extends Omit<ɵPromiseProxy, 'ref' | 'refFromURL'> {} + +/** + * AngularFireStorage Service + * + * This service is the main entry point for this feature module. It provides + * an API for uploading and downloading binary files from Cloud Storage for + * Firebase. + */ +@Injectable({ + providedIn: 'any' +}) +export class AngularFireStorage { + + public readonly keepUnstableUntilFirst: (obs: Observable) => Observable; + public readonly schedulers: ɵAngularFireSchedulers; + + public readonly ref: (path: string) => AngularFireStorageReference; + public readonly refFromURL: (url: string) => AngularFireStorageReference; + public readonly upload: (path: string, data: any, metadata?: UploadMetadata) => AngularFireUploadTask; + + constructor( + @Inject(FIREBASE_OPTIONS) options: FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig: string | FirebaseAppConfig | null | undefined, + @Optional() @Inject(BUCKET) storageBucket: string | null, + // tslint:disable-next-line:ban-types + @Inject(PLATFORM_ID) platformId: Object, + zone: NgZone, + @Optional() @Inject(MAX_UPLOAD_RETRY_TIME) maxUploadRetryTime: number | any, + @Optional() @Inject(MAX_OPERATION_RETRY_TIME) maxOperationRetryTime: number | any, + ) { + this.schedulers = new ɵAngularFireSchedulers(zone); + this.keepUnstableUntilFirst = ɵkeepUnstableUntilFirstFactory(this.schedulers); + + const storage = of(undefined).pipe( + observeOn(this.schedulers.outsideAngular), + switchMap(() => zone.runOutsideAngular(() => import('firebase/storage'))), + map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)), + map(app => + ɵfetchInstance(`${app.name}.storage.${storageBucket}`, 'AngularFireStorage', app, () => { + const storage = zone.runOutsideAngular(() => app.storage(storageBucket || undefined)); + if (maxUploadRetryTime) { + storage.setMaxUploadRetryTime(maxUploadRetryTime); + } + if (maxOperationRetryTime) { + storage.setMaxOperationRetryTime(maxOperationRetryTime); + } + return storage; + }, [maxUploadRetryTime, maxOperationRetryTime]) + ) + ); + + this.ref = (path) => createStorageRef(storage.pipe(map(it => it.ref(path))), this.schedulers, this.keepUnstableUntilFirst); + + this.refFromURL = (path) => + createStorageRef(storage.pipe(map(it => it.refFromURL(path))), this.schedulers, this.keepUnstableUntilFirst); + + this.upload = (path, data, metadata?) => { + const storageRef = storage.pipe(map(it => it.ref(path))); + const ref = createStorageRef(storageRef, this.schedulers, this.keepUnstableUntilFirst); + return ref.put(data, metadata); + }; + + return ɵlazySDKProxy(this, storage, zone); + + } + +} + +ɵapplyMixins(AngularFireStorage, [proxyPolyfillCompat]); diff --git a/src/storage-lazy/task.ts b/src/storage-lazy/task.ts new file mode 100644 index 000000000..e66ef8534 --- /dev/null +++ b/src/storage-lazy/task.ts @@ -0,0 +1,31 @@ +import { UploadTask, UploadTaskSnapshot } from './interfaces'; +import { fromTask } from './observable/fromTask'; +import { Observable } from 'rxjs'; +import { map, shareReplay, switchMap } from 'rxjs/operators'; + +export interface AngularFireUploadTask { + snapshot: Observable; + progress: Observable; + pause(): Promise; + cancel(): Promise; + resume(): Promise; +} + +/** + * Create an AngularFireUploadTask from a regular UploadTask from the Storage SDK. + * This method creates an observable of the upload and returns on object that provides + * multiple methods for controlling and monitoring the file upload. + */ +export function createUploadTask(task: Observable): AngularFireUploadTask { + const task$ = task.pipe(shareReplay({ refCount: false, bufferSize: 1 })); + const snapshot = task.pipe(switchMap(fromTask)); + return { + pause: () => task$.toPromise().then(it => it.pause()), + cancel: () => task$.toPromise().then(it => it.cancel()), + resume: () => task$.toPromise().then(it => it.resume()), + snapshot, + progress: snapshot.pipe( + map(s => s.bytesTransferred / s.totalBytes * 100) + ) + }; +} diff --git a/src/storage/observable/fromTask.ts b/src/storage/observable/fromTask.ts deleted file mode 100644 index a4442dd1e..000000000 --- a/src/storage/observable/fromTask.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Observable } from 'rxjs'; -import { shareReplay } from 'rxjs/operators'; -import { UploadTask, UploadTaskSnapshot } from '../interfaces'; - -export function fromTask(task: UploadTask) { - return new Observable(subscriber => { - const progress = (snap: UploadTaskSnapshot) => subscriber.next(snap); - const error = e => subscriber.error(e); - const complete = () => subscriber.complete(); - task.on('state_changed', progress, (e) => { - progress(task.snapshot); - error(e); - }, () => { - progress(task.snapshot); - complete(); - }); - }).pipe( - shareReplay({ bufferSize: 1, refCount: false }) - ); -} diff --git a/src/storage/public_api.ts b/src/storage/public_api.ts index 460348fa1..cdbe823ef 100644 --- a/src/storage/public_api.ts +++ b/src/storage/public_api.ts @@ -1,6 +1,5 @@ export * from './ref'; export * from './storage'; export * from './task'; -export * from './observable/fromTask'; export * from './storage.module'; -export * from './pipes/storageUrl.pipe'; +export { GetDownloadURLPipe, fromTask } from '@angular/fire/storage-lazy'; diff --git a/src/storage/ref.ts b/src/storage/ref.ts index 06ef4862c..300c7666b 100644 --- a/src/storage/ref.ts +++ b/src/storage/ref.ts @@ -8,7 +8,7 @@ export interface AngularFireStorageReference { getDownloadURL(): Observable; getMetadata(): Observable; delete(): Observable; - child(path: string): any; + child(path: string): AngularFireStorageReference; updateMetadata(meta: SettableMetadata): Observable; put(data: any, metadata?: UploadMetadata | undefined): AngularFireUploadTask; putString(data: string, format?: string | undefined, metadata?: UploadMetadata | undefined): AngularFireUploadTask; diff --git a/src/storage/storage.module.ts b/src/storage/storage.module.ts index c6181645f..e9d75d7e9 100644 --- a/src/storage/storage.module.ts +++ b/src/storage/storage.module.ts @@ -1,5 +1,5 @@ import { NgModule } from '@angular/core'; -import { GetDownloadURLPipeModule } from './pipes/storageUrl.pipe'; +import { GetDownloadURLPipeModule } from '@angular/fire/storage-lazy'; import { AngularFireStorage } from './storage'; @NgModule({ diff --git a/src/storage/storage.ts b/src/storage/storage.ts index e5e906acf..85f96df32 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -14,10 +14,8 @@ import { import { UploadMetadata } from './interfaces'; import 'firebase/storage'; import firebase from 'firebase/app'; - -export const BUCKET = new InjectionToken('angularfire2.storageBucket'); -export const MAX_UPLOAD_RETRY_TIME = new InjectionToken('angularfire2.storage.maxUploadRetryTime'); -export const MAX_OPERATION_RETRY_TIME = new InjectionToken('angularfire2.storage.maxOperationRetryTime'); +import { BUCKET, MAX_UPLOAD_RETRY_TIME, MAX_OPERATION_RETRY_TIME } from '@angular/fire/storage-lazy'; +export { BUCKET, MAX_UPLOAD_RETRY_TIME, MAX_OPERATION_RETRY_TIME }; /** * AngularFireStorage Service diff --git a/src/storage/task.ts b/src/storage/task.ts index bccc7154c..3587dff1e 100644 --- a/src/storage/task.ts +++ b/src/storage/task.ts @@ -1,5 +1,5 @@ import { UploadTask, UploadTaskSnapshot } from './interfaces'; -import { fromTask } from './observable/fromTask'; +import { fromTask } from '@angular/fire/storage-lazy'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; diff --git a/tools/build.ts b/tools/build.ts index b7cf5e0d0..79d7c10c4 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -9,10 +9,11 @@ import firebase from 'firebase/app'; // TODO infer these from the package.json const MODULES = [ 'core', 'analytics', 'auth', 'auth-guard', 'database', - 'firestore', 'functions', 'remote-config', - 'storage', 'messaging', 'performance' + 'firestore', 'firestore-lazy', 'functions', 'remote-config', + 'storage', 'messaging', 'performance', 'storage-lazy', + 'database-lazy', // 'firestore/memory', 'firestore-lazy/memory', ]; -const LAZY_MODULES = ['analytics', 'auth', 'functions', 'messaging', 'remote-config']; +const LAZY_MODULES = [['analytics'], ['auth'], ['functions'], ['firestore-lazy', 'firestore'], ['messaging'], ['remote-config'], ['storage-lazy', 'storage'], ['database-lazy', 'database']]; const UMD_NAMES = MODULES.map(m => m === 'core' ? 'angular-fire' : `angular-fire-${m}`); const ENTRY_NAMES = MODULES.map(m => m === 'core' ? '@angular/fire' : `@angular/fire/${m}`); @@ -24,6 +25,9 @@ function proxyPolyfillCompat() { messaging: tsKeys(), performance: tsKeys(), 'remote-config': tsKeys(), + 'firestore-lazy': tsKeys(), + 'storage-lazy': tsKeys(), + 'database-lazy': tsKeys(), }; return Promise.all(Object.keys(defaultObject).map(module => @@ -95,7 +99,9 @@ async function measure(module: string) { } async function fixImportForLazyModules() { - await Promise.all(LAZY_MODULES.map(async module => { + // firebase-lazy/memory is special-case, make sure to also cover that as we make changes + await Promise.all(LAZY_MODULES.map(async ([module, _sdk]) => { + const sdk = _sdk || module; const packageJson = JSON.parse((await readFile(dest(module, 'package.json'))).toString()); const entries = Array.from(new Set(Object.values(packageJson).filter(v => typeof v === 'string' && v.endsWith('.js')))) as string[]; // TODO don't hardcode esm2015 here, perhaps we should scan all the entry directories @@ -106,16 +112,41 @@ async function fixImportForLazyModules() { let newSource: string; if (path.endsWith('.umd.js')) { // in the UMD for lazy modules replace the dyanamic import - newSource = source.replace(`import('firebase/${module}')`, 'rxjs.of(undefined)'); + newSource = source.replace(`import('firebase/${sdk}')`, 'rxjs.of(undefined)'); } else { // in everything else get rid of the global side-effect import - newSource = source.replace(new RegExp(`^import 'firebase/${module}'.+$`, 'gm'), ''); + newSource = source.replace(new RegExp(`^import 'firebase/${sdk}'.+$`, 'gm'), ''); } await writeFile(dest(module, path), newSource); })); })); } +async function fixFirestoreLazyMemoryModule() { + const module = 'firestore-lazy/memory'; + const packageJson = JSON.parse((await readFile(dest(module, 'package.json'))).toString()); + const entries = Array.from(new Set(Object.values(packageJson).filter(v => typeof v === 'string' && v.endsWith('.js')))) as string[]; + // TODO don't hardcode esm2015 here, perhaps we should scan all the entry directories + // e.g, if ng-packagr starts building other non-flattened entries we'll lose the dynamic import + entries.push(`../../esm2015/${module}/public_api.js`); // the import isn't pulled into the ESM public_api + entries.push(`../../esm2015/${module}/firestore.js`); + await Promise.all(entries.map(async path => { + const source = (await readFile(dest(module, path))).toString(); + let newSource: string; + if (path.endsWith('.umd.js')) { + // in the UMD for lazy modules replace the dyanamic import + newSource = source.replace('import(\'firebase/firestore\')', 'rxjs.of(undefined)') + .replace('require(\'firebase/firestore\')', 'require(\'firebase/firestore/memory\')') + .replace(', \'firebase/firestore\',', ', \'firebase/firestore/memory\','); + } else { + // in everything else get rid of the global side-effect import + newSource = source.replace(/^import 'firebase\/firestore'.+$/gm, '') + .replace('import(\'firebase/firestore\')', 'import(\'firebase/firestore/memory\')'); + } + await writeFile(dest(module, path), newSource); + })); +} + async function buildLibrary() { await proxyPolyfillCompat(); await spawnPromise('npx', ['ng', 'build']); @@ -127,6 +158,7 @@ async function buildLibrary() { replacePackageJsonVersions(), replacePackageCoreVersion(), fixImportForLazyModules(), + fixFirestoreLazyMemoryModule(), ]); } diff --git a/tsconfig.base.json b/tsconfig.base.json index f60094014..e1f64f306 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -26,14 +26,15 @@ "@angular/fire/auth": ["dist/packages-dist/auth"], "@angular/fire/auth-guard": ["dist/packages-dist/auth-guard"], "@angular/fire/database": ["dist/packages-dist/database"], - "@angular/fire/database-deprecated": ["dist/packages-dist/database-deprecated"], - "@angular/fire/firebase-node": ["dist/packages-dist/firebase-node"], + "@angular/fire/database-lazy": ["dist/packages-dist/database-lazy"], "@angular/fire/firestore": ["dist/packages-dist/firestore"], + "@angular/fire/firestore-lazy": ["dist/packages-dist/firestore-lazy"], "@angular/fire/functions": ["dist/packages-dist/functions"], "@angular/fire/messaging": ["dist/packages-dist/messaging"], "@angular/fire/performance": ["dist/packages-dist/performance"], "@angular/fire/remote-config": ["dist/packages-dist/remote-config"], - "@angular/fire/storage": ["dist/packages-dist/storage"] + "@angular/fire/storage": ["dist/packages-dist/storage"], + "@angular/fire/storage-lazy": ["dist/packages-dist/storage-lazy"] } } } diff --git a/tsconfig.json b/tsconfig.json index aa1210871..95263ebbf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "target": "es2015", "declaration": true, "inlineSources": true, + "stripInternal": true, "types": [ "node", "zone.js" diff --git a/yarn.lock b/yarn.lock index f8d2144d2..8d2b9c010 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,23 +10,23 @@ "@angular-devkit/core" "10.2.0" rxjs "6.6.2" -"@angular-devkit/architect@0.1100.0", "@angular-devkit/architect@>= 0.900 < 0.1200": - version "0.1100.0" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1100.0.tgz#0ef9cb3616e0368fa6898574cafeec7cd4357930" - integrity sha512-JFPEpEgxJGk5eaJsEilQNI5rOAKCawMdGFAq1uBlYeXSt3iMfFfn//ayvIsE7L2y5b4MC0rzafWSNyDSP3+WuA== +"@angular-devkit/architect@0.1100.2", "@angular-devkit/architect@>= 0.900 < 0.1200": + version "0.1100.2" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1100.2.tgz#7567af030afe7d6cdea1771bcd2a193a19a90dc9" + integrity sha512-wSMMM8eBPol48OtvIyrIq2H9rOIiJmrPEtPbH0BSuPX0B8BckVImeTPzloqxSrpul4tY7Iwx0zwISDEgb59Vbw== dependencies: - "@angular-devkit/core" "11.0.0" + "@angular-devkit/core" "11.0.2" rxjs "6.6.3" "@angular-devkit/build-angular@>= 0.900 < 0.1200": - version "0.1100.0" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1100.0.tgz#11c29c3f324150ad3ae867fb06a7dc9b2dcaa910" - integrity sha512-jCgtnqfBLO00LNImqtjeW07ijYXdpzhsOM4jzlhafh/NesjWJXgg1NI1K7QJvmVL79TeqbBsMj8IOLGTMUCDJw== - dependencies: - "@angular-devkit/architect" "0.1100.0" - "@angular-devkit/build-optimizer" "0.1100.0" - "@angular-devkit/build-webpack" "0.1100.0" - "@angular-devkit/core" "11.0.0" + version "0.1100.2" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1100.2.tgz#afbeef979df4dbafeed3ff3de438dc9f35e2d148" + integrity sha512-5Qo3DDKggzUJKibNgeyE5mIMFYP0tVebNvMatpbnYnR/U0fUuuQdvNC68s380M5KoOuubfeXr0Js0VFk0mkaow== + dependencies: + "@angular-devkit/architect" "0.1100.2" + "@angular-devkit/build-optimizer" "0.1100.2" + "@angular-devkit/build-webpack" "0.1100.2" + "@angular-devkit/core" "11.0.2" "@babel/core" "7.12.3" "@babel/generator" "7.12.1" "@babel/plugin-transform-runtime" "7.12.1" @@ -34,7 +34,7 @@ "@babel/runtime" "7.12.1" "@babel/template" "7.10.4" "@jsdevtools/coverage-istanbul-loader" "3.0.5" - "@ngtools/webpack" "11.0.0" + "@ngtools/webpack" "11.0.2" ansi-colors "4.1.1" autoprefixer "9.8.6" babel-loader "8.1.0" @@ -44,7 +44,7 @@ circular-dependency-plugin "5.2.0" copy-webpack-plugin "6.2.1" core-js "3.6.5" - css-loader "5.0.0" + css-loader "4.3.0" cssnano "4.1.10" file-loader "6.1.1" find-cache-dir "3.3.1" @@ -101,10 +101,10 @@ "@angular-devkit/architect" "0.1002.0" rxjs "6.6.2" -"@angular-devkit/build-optimizer@0.1100.0": - version "0.1100.0" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1100.0.tgz#fae70c407fa2ec26ef839f9f2706cb3be990121b" - integrity sha512-RitDB5JCNDUN2CoNqf/FwLCwdWruApjxb7nUVb9C/uQgGEnrBojyxS/Rv/jCioom86s0sfY9wo79jdxd6AercQ== +"@angular-devkit/build-optimizer@0.1100.2": + version "0.1100.2" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1100.2.tgz#93dea833aed64d265cfdfebb6580e10cf909630b" + integrity sha512-2ZdEeAs0a53g9LDkP5H2mCEPLyk7yd9P7eTepNYvIOz3xJ6W6dB2CqotPMfnHgd4o12cbzCOWrPBxbfo/VnMig== dependencies: loader-utils "2.0.0" source-map "0.7.3" @@ -112,13 +112,13 @@ typescript "4.0.5" webpack-sources "2.0.1" -"@angular-devkit/build-webpack@0.1100.0": - version "0.1100.0" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1100.0.tgz#66b78cc1f5d9d5f2f0b551d3f848bebef4a54ad7" - integrity sha512-9diP/A6NtQxSxjbBMj9h9MHrAj4VqCvuFraR928eFaxEoRKcIwSTHhOiolRm+GL5V0VB+O53FRYDk3gC7BGjmQ== +"@angular-devkit/build-webpack@0.1100.2": + version "0.1100.2" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1100.2.tgz#1613334c396931de295d47d8ec8ef980592cc00d" + integrity sha512-XVMtWoxNa3wJLRjJ846Y02PzupdbUizdAtggRu2731RLMvI1KawWlsTURi12MNUnoVQYm9eldiIA/Y1UqeE8mQ== dependencies: - "@angular-devkit/architect" "0.1100.0" - "@angular-devkit/core" "11.0.0" + "@angular-devkit/architect" "0.1100.2" + "@angular-devkit/core" "11.0.2" rxjs "6.6.3" "@angular-devkit/core@10.2.0": @@ -132,10 +132,10 @@ rxjs "6.6.2" source-map "0.7.3" -"@angular-devkit/core@11.0.0", "@angular-devkit/core@^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.0.0.tgz#bf69f1fb7a00d0496785f84122daae7dc27a4b14" - integrity sha512-fXZtSs3J4S12hboi3om1FA+QS0e8nuQMyzl2nkmtuhcELUFMmSrEl36dtCni5e7Svs46BUAZ5w8EazIkgGQDJg== +"@angular-devkit/core@11.0.2", "@angular-devkit/core@^9.0.0 || ^10.0.0 || ^11.0.0": + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.0.2.tgz#dd3475912e830740e71e14e3168d609e8ddef8c6" + integrity sha512-vUmmUNmNM9oRcDmt0PunU/ayglo0apq4pGL9Z5jj6alf2WwEiTcGHjyuZSDIO9MOLi41519jp3mDx79qXvvyww== dependencies: ajv "6.12.6" fast-json-stable-stringify "2.1.0" @@ -154,12 +154,12 @@ rxjs "6.4.0" source-map "0.7.3" -"@angular-devkit/schematics@11.0.0", "@angular-devkit/schematics@^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.0.0.tgz#ebdbd3b4cf9f740f793df3200cd28c00447abfc8" - integrity sha512-oCz9E0thA5WdGDuv6biu3X5kw5/vNE4ZZOKT2sHBQMpAuuDYrDpfTYQJjXQtjfXWvmlr8L8aqDD9N4HXsE4Esw== +"@angular-devkit/schematics@11.0.2", "@angular-devkit/schematics@^9.0.0 || ^10.0.0 || ^11.0.0": + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.0.2.tgz#b5aa914d7e91d92b4eeadb7aed3b5228497abbf3" + integrity sha512-unNewc+Y9ofrdKxXNoSHKUL6wvV8Vgh2nJMTLI1VAw8nfqgWphI+s5XwbVzog65nhZ10xJeaUm9u5R8pxLDpQg== dependencies: - "@angular-devkit/core" "11.0.0" + "@angular-devkit/core" "11.0.2" ora "5.1.0" rxjs "6.6.3" @@ -172,22 +172,22 @@ rxjs "6.4.0" "@angular/animations@ ^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-11.0.0.tgz#6f567930dca8eb8ab1320f1f48feb981493b86c6" - integrity sha512-RGaAnZOI73bPnNWrJq/p8sc+hpUBhScq139M6r4qQjQPsPahazL6v6hHAgRhZNemqw164d1oE4K/22O/i0E3Tw== + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-11.0.2.tgz#c095ab0aed4491732c81a894987bcab1a854ab15" + integrity sha512-uF/RlBY1rznbuw+1lm8Q2HKDrBOQQ2Bi2cUPuef+ALn+lxGl501eHlE+PTtBjDEzJcJPfd4pE3Ww3+3Il+D+Tw== dependencies: tslib "^2.0.0" "@angular/cli@^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-11.0.0.tgz#8dcd73bd528e76b21178c43becab10837cfe8039" - integrity sha512-U9sh9r1CSqS78QjuosM3JDXUUTf8eVP1+kSchWEsxjJ0kfdvj7PvtKD1kmRH7HA5lD2q7QfGEvfHpfxMVzKxRg== - dependencies: - "@angular-devkit/architect" "0.1100.0" - "@angular-devkit/core" "11.0.0" - "@angular-devkit/schematics" "11.0.0" - "@schematics/angular" "11.0.0" - "@schematics/update" "0.1100.0" + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-11.0.2.tgz#15ee1732258deec8ecb93f6ccac52d95230074d8" + integrity sha512-mebt4ikwXD3gsbHRxKCpn83yW3UVnhiVDEpSXljs1YxscZ1X1dXrxb2g6LdAJwVp9xY5ERqRQeZM7eChqLTrvg== + dependencies: + "@angular-devkit/architect" "0.1100.2" + "@angular-devkit/core" "11.0.2" + "@angular-devkit/schematics" "11.0.2" + "@schematics/angular" "11.0.2" + "@schematics/update" "0.1100.2" "@yarnpkg/lockfile" "1.1.0" ansi-colors "4.1.1" debug "4.2.0" @@ -205,16 +205,16 @@ uuid "8.3.1" "@angular/common@^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-11.0.0.tgz#cc2a14b36c56f6c4d93427c2f8c17f55e4b464c9" - integrity sha512-chlbtxR7jpPs3Rc1ymdp3UfUzqEr57OFIxVMG6hROODclPQQk/7oOHdQB4hpUObaF9y4ZTLeKHKWiR/twi21Pg== + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-11.0.2.tgz#7558b940a1110a9c6c65103b1ae8e18f2c8e939c" + integrity sha512-DGJuSBDt+bF77AzJNrLzeaFGSdwQ3OjgP9UUv1eKvaxp9D+lDam8suIJMuBwTsJII/yrDndY75ENPNTEqhmB2A== dependencies: tslib "^2.0.0" "@angular/compiler-cli@^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-11.0.0.tgz#ff4c2c16284a31a4f8ff1d224f593f64a1458234" - integrity sha512-zrd/cU9syZ8XuQ3ItfIGaKDn1ZBCWyiqdLVRH9VDmyNqQFiCc/VWQ9Th9z8qpLptgdpzE9+lKFgeZJTDtbcveQ== + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-11.0.2.tgz#961df7f08dc98a6ea202e6aa22dc81ff29c9719d" + integrity sha512-I39zNcf6q0NN4PKCbY6Lm4WP69ujLrAew56X5yvlECW9CJlidV0qi1S/DGgAWhXTDOt8XA/KP1hD1pgJtMHjJQ== dependencies: "@babel/core" "^7.8.6" "@babel/types" "^7.8.6" @@ -230,7 +230,7 @@ source-map "^0.6.1" sourcemap-codec "^1.4.8" tslib "^2.0.0" - yargs "15.3.0" + yargs "^16.1.1" "@angular/compiler@9.0.0": version "9.0.0" @@ -238,9 +238,9 @@ integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== "@angular/compiler@^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-11.0.0.tgz#b49997d0130e7c8cfe84fa73e5610892f4a772af" - integrity sha512-I7wVhdqvhtBTQTtW61z0lwPb1LiQQ0NOwjsbfN5sAc7/uwxw7em+Kyb/XJgBwgaTKtAL8bZEzdoQGLdsSKQF2g== + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-11.0.2.tgz#892cd38b3afa6ba63149d0bfd9265401a3d88d0c" + integrity sha512-deDT5+Lcph4nNhh6sZd0mBS5OkJL3HPbX5upDMI28Wuayt18Pn0UNotWY77/KV6wwIAInmlx9N06PoH3pq3hqg== dependencies: tslib "^2.0.0" @@ -250,39 +250,39 @@ integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== "@angular/core@^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-11.0.0.tgz#cdb89f3877f6e5487a0e5f18d234447ec41e8184" - integrity sha512-FNewyMwYy+kGdw1xWfrtaPD2cSQs3kDVFbl8mNMSzp933W5yMsHDvjXb0+nPFqEb8ywEIdm3MsBMK0y3iBWZQw== + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-11.0.2.tgz#490248b1d746e24513f2db996bd857e5a36d2f45" + integrity sha512-GyDebks5ZPHDyChDW3VvzJq00Ct0iuesNpb9z/GpKtOXqug3sGr4KgkFDUTbfizKPWyeoaLH9FQYP55215nCKQ== dependencies: tslib "^2.0.0" "@angular/platform-browser-dynamic@^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.0.0.tgz#630d77a0c853bcc2c80c30dfe6c101d6c7fe4ac1" - integrity sha512-NAmKGhHK+tl7dr/Hcqxvr/813Opec3Mv0IRwIgmKdlpZd7qAwT/mw4RnO4YPSEoDOM6hqGt7GdlWrSDX802duQ== + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.0.2.tgz#e8f621482c4fe04c14d799c771382891052ee2a2" + integrity sha512-iV7xz90FdmYFiXZRLkZtP9Lr+OXXh4bhkX7zN1L5H8SSUF4iOJGBdOts5Fiy5GZjYYILjF1pJoEIicfW/RSHjA== dependencies: tslib "^2.0.0" "@angular/platform-browser@^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-11.0.0.tgz#314a0362e63ac7eef80adebfc5fbe4e7f2aa2a73" - integrity sha512-p8sF6JfaBI+YyLpp5OSg6UcCqjtLKRR+Otq1P/tro5SuxrsrBNRVU8j0tl/crkScsMwAvgmJ1joRyUKdI2mUGQ== + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-11.0.2.tgz#78e640400050c69ca3322b8df0f4ec48f629ec34" + integrity sha512-RHPm5/h8g3lSBgdg9OvO7w06juEwwBurvQcugXlk7+AeqznwzBodTWGPIATKzMySXQFmpy3bAZ3IxS0NkRrbWA== dependencies: tslib "^2.0.0" "@angular/platform-server@^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-11.0.0.tgz#aca53c70e1e7010a5dd1e730c1cabd317e57b2af" - integrity sha512-0LsA4u5kCDKMOxcWf4HFH3PNYIhFcnzP/TYqYfIkY/GpQeC5agxWzddJofNi7g/Lh1UoK5hzf+3Ewn3o/aBxjA== + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-11.0.2.tgz#b9cf77c434fbaef5871c961a2def31c561bd473a" + integrity sha512-wC+JP0izKJMDQG+u7HXFYyKni7T65ELC6JknL4dODDHx+XylkFPXGI+EffffnVgJssheVDGrwe32Fh0Yjus0Lw== dependencies: domino "^2.1.2" tslib "^2.0.0" xhr2 "^0.2.0" "@angular/router@^9.0.0 || ^10.0.0 || ^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-11.0.0.tgz#59e855b0d34c4578e0556e181f2f28048fb0d5a8" - integrity sha512-10ZeobfK3HqVeWS6zjdKU16ccxFtdCHkxT11bnFg3Jwq9vKt+LI5KitAkCI5rYTY3DRfVzasRkqBzZfZMkbftw== + version "11.0.2" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-11.0.2.tgz#38119a49edbfc60552d3403b4fc081ec705e2d6d" + integrity sha512-EU0lQ+3vv1ozly+Z4SgaGj/6CWMIExjnSnA1F7SI2yWmMgMMSb5CsGJ2xzr0V8ex3XZzuU2VuKF74muC58qSyg== dependencies: tslib "^2.0.0" @@ -1197,10 +1197,10 @@ resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.10.1.tgz#7815e71c9c6f072034415524b29ca8f1d1770660" integrity sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw== -"@firebase/auth@0.15.1": - version "0.15.1" - resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.15.1.tgz#2e0e7397d6f754d81916babd9bce21a51f4b25a3" - integrity sha512-qVJTmq/6l3/o6V93nAD+n1ExTywbKEFYbuuI1TZIUryy5KSXOFnxilmZI4yJeQSZ3ee06YiJsIRYRaYUeg6JQQ== +"@firebase/auth@0.15.2": + version "0.15.2" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.15.2.tgz#9ada3f37620d131a1c56994138a599b5c9f9ca2e" + integrity sha512-2n32PBi6x9jVhc0E/ewKLUCYYTzFEXL4PNkvrrlGKbzeTBEkkyzfgUX7OV9UF5wUOG+gurtUthuur1zspZ/9hg== dependencies: "@firebase/auth-types" "0.10.1" @@ -1227,21 +1227,21 @@ dependencies: "@firebase/app-types" "0.6.1" -"@firebase/database-types@0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.6.0.tgz#7795bc6b1db93f4cbda9a241c8dfe1bb86033dc6" - integrity sha512-ljpU7/uboCGqFSe9CNgwd3+Xu5N8YCunzfPpeueuj2vjnmmypUi4QWxgC3UKtGbuv1q+crjeudZGLxnUoO0h7w== +"@firebase/database-types@0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.6.1.tgz#cf1cfc03e617ed4c2561703781f85ba4c707ff65" + integrity sha512-JtL3FUbWG+bM59iYuphfx9WOu2Mzf0OZNaqWiQ7lJR8wBe7bS9rIm9jlBFtksB7xcya1lZSQPA/GAy2jIlMIkA== dependencies: "@firebase/app-types" "0.6.1" -"@firebase/database@0.7.1": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.7.1.tgz#900d2e6ed734249e65e5f159293830e4f4285d6e" - integrity sha512-8j3KwksaYMSbIsEjOIarZD3vj4jGJjIlLGIAiO/4P4XyOtrlnxIiH7G0UdIZlcvKU4Gsgg0nthT2+EapROmHWA== +"@firebase/database@0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.8.1.tgz#a7bc1c01052d35817a242c21bfe09ab29ee485a3" + integrity sha512-/1HhR4ejpqUaM9Cn3KSeNdQvdlehWIhdfTVWFxS73ZlLYf7ayk9jITwH10H3ZOIm5yNzxF67p/U7Z/0IPhgWaQ== dependencies: "@firebase/auth-interop-types" "0.1.5" "@firebase/component" "0.1.21" - "@firebase/database-types" "0.6.0" + "@firebase/database-types" "0.6.1" "@firebase/logger" "0.2.6" "@firebase/util" "0.3.4" faye-websocket "0.11.3" @@ -1265,16 +1265,16 @@ resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-2.0.0.tgz#1f6212553b240f1a8905bb8dcf1f87769138c5c0" integrity sha512-ZGb7p1SSQJP0Z+kc9GAUi+Fx5rJatFddBrS1ikkayW+QHfSIz0omU23OgSHcBGTxe8dJCeKiKA2Yf+tkDKO/LA== -"@firebase/firestore@2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-2.0.1.tgz#2d4734ecd5d165582eedea8487849c5535a55c0e" - integrity sha512-7WMv3b+2P/6SOE0RxPB+S6k75/vYTDhOpPBp6JH6nPQjS9mGtR9m0adKtXjOBBugcbK6sBgPMzxmQGwQl8lW4w== +"@firebase/firestore@2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-2.0.4.tgz#c4be6f3540f607fd8e200cfba83c4997c29447fe" + integrity sha512-fzJKj/4h4jOwPSfHB42XBJIC0zsPsepU6FcBO+8nSx7G2IPfTw8cMgSNin2gPqX6tR1w1NQtHiSlXiRKsbMZdA== dependencies: "@firebase/component" "0.1.21" "@firebase/firestore-types" "2.0.0" "@firebase/logger" "0.2.6" "@firebase/util" "0.3.4" - "@firebase/webchannel-wrapper" "0.4.0" + "@firebase/webchannel-wrapper" "0.4.1" "@grpc/grpc-js" "^1.0.0" "@grpc/proto-loader" "^0.5.0" node-fetch "2.6.1" @@ -1382,10 +1382,10 @@ resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.3.13.tgz#cd43e939a2ab5742e109eb639a313673a48b5458" integrity sha512-pL7b8d5kMNCCL0w9hF7pr16POyKkb3imOW7w0qYrhBnbyJTdVxMWZhb0HxCFyQWC0w3EiIFFmxoz8NTFZDEFog== -"@firebase/storage@0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.4.1.tgz#b86973a8ab3ef52f38d6463fcc7613f5801ff8e4" - integrity sha512-/l05Dn3UYynPELt0ZFJz24H49sQ8c8KnOEGR/Pk1AOjLmc71vjjobVEkgkHyy1eyfmYuAZtsc6ePOwc89YnBTg== +"@firebase/storage@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.4.2.tgz#bc5924b87bd2fdd4ab0de49851c0125ebc236b89" + integrity sha512-87CrvKrf8kijVekRBmUs8htsNz7N5X/pDhv3BvJBqw8K65GsUolpyjx0f4QJRkCRUYmh3MSkpa5P08lpVbC6nQ== dependencies: "@firebase/component" "0.1.21" "@firebase/storage-types" "0.3.13" @@ -1406,10 +1406,10 @@ dependencies: tslib "^1.11.1" -"@firebase/webchannel-wrapper@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.4.0.tgz#becce788818d3f47f0ac1a74c3c061ac1dcf4f6d" - integrity sha512-8cUA/mg0S+BxIZ72TdZRsXKBP5n5uRcE3k29TZhZw6oIiHBt9JA7CTb/4pE1uKtE/q5NeTY2tBDcagoZ+1zjXQ== +"@firebase/webchannel-wrapper@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.4.1.tgz#600f2275ff54739ad5ac0102f1467b8963cd5f71" + integrity sha512-0yPjzuzGMkW1GkrC8yWsiN7vt1OzkMIi9HgxRmKREZl2wnNPOKo/yScTjXf/O57HM8dltqxPF6jlNLFVtc2qdw== "@google-cloud/common@^2.1.1": version "2.4.0" @@ -1517,9 +1517,9 @@ semver "^6.2.0" "@grpc/grpc-js@^1.0.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.0.tgz#4ff1ac4cdf7eb2af80d3c67316be9c2308d8d9bf" - integrity sha512-09H50V7rmz0gFrGz6IbP49z9A8+2p4yZYcNDEb7bytr90vWn52VBQE1a+LMBlrucmNN0wSsiCr3TJx+dStHTng== + version "1.2.1" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.1.tgz#6a0b4e1bc6039d84f945569ff8c3059f81284afe" + integrity sha512-JpGh2CgqnwVII0S9TMEX3HY+PkLJnb7HSAar3Md1Y3aWxTZqAGb7gTrNyBWn/zueaGFsMYRm2u/oYufWFYVoIQ== dependencies: "@types/node" "^12.12.47" google-auth-library "^6.1.1" @@ -1561,12 +1561,12 @@ resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== -"@ngtools/webpack@11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-11.0.0.tgz#bddc9ad4677de55d9df9418408079c2a2be4f482" - integrity sha512-thWOXiMfyVUUWDDRUUAIvb5HASovX1C0GcxRBFE8fXJMCwOPIwqZiAyJJlUUnie8BEP9yC/x6uLCud56ai4Uaw== +"@ngtools/webpack@11.0.2": + version "11.0.2" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-11.0.2.tgz#d9513854d474fe09350ce705d04fee38ffb8f0c7" + integrity sha512-GbNP6HMBVoee2CkYW/pknprFCeiOLz4FGE06yr4m0700c1i6wuX7AzyHfBcLGAIP6nVblNOT3eh5M41b3cDf8g== dependencies: - "@angular-devkit/core" "11.0.0" + "@angular-devkit/core" "11.0.2" enhanced-resolve "5.3.1" webpack-sources "2.0.1" @@ -1692,13 +1692,13 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@schematics/angular@11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-11.0.0.tgz#d292aeb472e1f5f11917df9f660d38b3f792dd5b" - integrity sha512-/4fkfryoCKQv7nnZgbQ/2aLg8418/SdrCi4ASN0xpfcj34oe2FqsKypeoJG+3bQVF8CLfseorvPNR2YINb4RQA== +"@schematics/angular@11.0.2": + version "11.0.2" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-11.0.2.tgz#63041d1931fe2d56135d730a6e43937a3eef4bab" + integrity sha512-tUIuCYJUzHYuiXGJ2KCuwxMocS56kPHaM8+neVYWwWbOxKzLZXv80gMm/pIWxrqUDCkIUi3yb4ienudFhgQLYg== dependencies: - "@angular-devkit/core" "11.0.0" - "@angular-devkit/schematics" "11.0.0" + "@angular-devkit/core" "11.0.2" + "@angular-devkit/schematics" "11.0.2" jsonc-parser "2.3.1" "@schematics/angular@^8.3.8": @@ -1709,13 +1709,13 @@ "@angular-devkit/core" "8.3.29" "@angular-devkit/schematics" "8.3.29" -"@schematics/update@0.1100.0": - version "0.1100.0" - resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1100.0.tgz#1b7f834d88cdd86d13b2cd0f8d826bf4c934d064" - integrity sha512-61zhqIvKHiMR3nezM5FlUoWe2Lw2uKzmuSwcxA2d6SqjDXYyXrOSKmaPcbi7Emgh3VWsQadNpXuc5A2tbKCQhg== +"@schematics/update@0.1100.2": + version "0.1100.2" + resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1100.2.tgz#d3a5c726d434d6c8ff04db8836f829299ca7108a" + integrity sha512-pETCmQylIQ7RM+8uqDkI3KfOaX5H7nuzmMXby28zdLPMZniYti0gJxieiVFhvdz2Ot2Axj0hznfmraFgC9mQMw== dependencies: - "@angular-devkit/core" "11.0.0" - "@angular-devkit/schematics" "11.0.0" + "@angular-devkit/core" "11.0.2" + "@angular-devkit/schematics" "11.0.2" "@yarnpkg/lockfile" "1.1.0" ini "1.3.5" npm-package-arg "^8.0.0" @@ -1849,9 +1849,9 @@ "@types/through" "*" "@types/jasmine@^3.3.13": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.6.1.tgz#f8b95def0016411c58c7adb4791dff29bc62992c" - integrity sha512-eeSCVhBsgwHNS1FmaMu4zrLxfykCTWJMLFZv7lmyrZQjw7foUUXoPu4GukSN9v7JvUw7X+/aDH3kCaymirBSTg== + version "3.6.2" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.6.2.tgz#02f64450016f7de70f145d698be311136d7c6374" + integrity sha512-AzfesNFLvOs6Q1mHzIsVJXSeUnqVh4ZHG8ngygKJfbkcSLwzrBVm/LKa+mR8KrOfnWtUL47112gde1MC0IXqpQ== "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6": version "7.0.6" @@ -1884,9 +1884,9 @@ integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== "@types/node@*": - version "14.14.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" - integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg== + version "14.14.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.9.tgz#04afc9a25c6ff93da14deabd65dc44485b53c8d6" + integrity sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw== "@types/node@6.0.*": version "6.0.118" @@ -1894,9 +1894,9 @@ integrity sha512-N33cKXGSqhOYaPiT4xUGsYlPPDwFtQM/6QxJxuMXA/7BcySW+lkn2yigWP7vfs4daiL/7NJNU6DMCqg5N4B+xQ== "@types/node@^12.12.47": - version "12.19.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz#cdfbb62e26c7435ed9aab9c941393cc3598e9b46" - integrity sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w== + version "12.19.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.6.tgz#fbf249fa46487dd8c7386d785231368b92a33a53" + integrity sha512-U2VopDdmBoYBmtm8Rz340mvvSz34VgX/K9+XCuckvcLGMkt3rbMX8soqFOikIPlPBc5lmw8By9NUK7bEFSBFlQ== "@types/node@^12.6.2 < 12.12.42": version "12.12.41" @@ -1904,9 +1904,9 @@ integrity sha512-Q+eSkdYQJ2XK1AJnr4Ji8Gvk3sRDybEwfTvtL9CA25FFUSD2EgZQewN6VCyWYZCXg5MWZdwogdTNBhlWRcWS1w== "@types/node@^13.7.0": - version "13.13.30" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.30.tgz#1ed6e01e4ca576d5aec9cc802cc3bcf94c274192" - integrity sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA== + version "13.13.32" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.32.tgz#f0edd0fb57b3c9f6e64a0b3ddb1e0f729b6f71ce" + integrity sha512-sPBvDnrwZE1uePhkCEyI/qQlgZM5kePPAhHIFDWNsOrWBFRBOk3LKJYmVCLeLZlL9Ub/FzMJb31OTWCg2F+06g== "@types/node@^8.10.59": version "8.10.66" @@ -2070,9 +2070,9 @@ integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== "@types/serve-static@*": - version "1.13.7" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.7.tgz#e51b51a0becda910f9fd04c718044da69d6c492e" - integrity sha512-3diZWucbR+xTmbDlU+FRRxBf+31OhFew7cJXML/zh9NmvSPTNoFecAwHB66BUqFgENJtqMiyl7JAwUE/siqdLw== + version "1.13.8" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.8.tgz#851129d434433c7082148574ffec263d58309c46" + integrity sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA== dependencies: "@types/mime" "*" "@types/node" "*" @@ -3558,7 +3558,7 @@ camelcase@^4.1.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= -camelcase@^6.1.0: +camelcase@^6.0.0: version "6.2.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== @@ -3574,9 +3574,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001157: - version "1.0.30001157" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001157.tgz#2d11aaeb239b340bc1aa730eca18a37fdb07a9ab" - integrity sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA== + version "1.0.30001159" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001159.tgz#bebde28f893fa9594dadcaa7d6b8e2aa0299df20" + integrity sha512-w9Ph56jOsS8RL20K9cLND3u/+5WASWdhC/PPrf+V3/HsM3uHOavWOR1Xzakbv4Puo/srmPHudkmCRWM7Aq+/UA== canonical-path@1.0.0: version "1.0.0" @@ -3839,6 +3839,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -4570,22 +4579,22 @@ css-declaration-sorter@^4.0.1: postcss "^7.0.1" timsort "^0.3.0" -css-loader@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.0.0.tgz#f0a48dfacc3ab9936a05ee16a09e7f313872e117" - integrity sha512-9g35eXRBgjvswyJWoqq/seWp+BOxvUl8IinVNTsUBFFxtwfEYvlmEn6ciyn0liXGbGh5HyJjPGCuobDSfqMIVg== +css-loader@4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.3.0.tgz#c888af64b2a5b2e85462c72c0f4a85c7e2e0821e" + integrity sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg== dependencies: - camelcase "^6.1.0" + camelcase "^6.0.0" cssesc "^3.0.0" - icss-utils "^5.0.0" + icss-utils "^4.1.1" loader-utils "^2.0.0" - postcss "^8.1.1" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.0" - postcss-modules-scope "^3.0.0" - postcss-modules-values "^4.0.0" + postcss "^7.0.32" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.3" + postcss-modules-scope "^2.2.0" + postcss-modules-values "^3.0.0" postcss-value-parser "^4.1.0" - schema-utils "^3.0.0" + schema-utils "^2.7.1" semver "^7.3.2" css-parse@~2.0.0: @@ -4627,11 +4636,11 @@ css-tree@1.0.0-alpha.37: source-map "^0.6.1" css-tree@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.1.tgz#7726678dfe2a57993a018d9dce519bf1760e3b6d" - integrity sha512-WroX+2MvsYcRGP8QA0p+rxzOniT/zpAoQ/DTKDSJzh5T3IQKUkFHeIIfgIapm2uaP178GWY3Mime1qbk8GO/tA== + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.1.tgz#30b8c0161d9fb4e9e2141d762589b6ec2faebd2e" + integrity sha512-NVN42M2fjszcUNpDbdkvutgQSlFYsr1z7kqeuCagHnNLBfYor6uP1WL1KrkmdYZ5Y1vTBCIOI/C/+8T98fJ71w== dependencies: - mdn-data "2.0.12" + mdn-data "2.0.14" source-map "^0.6.1" css-what@^3.2.1: @@ -4730,9 +4739,9 @@ cssnano@4.1.10: postcss "^7.0.0" csso@^4.0.2: - version "4.1.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.1.0.tgz#1d31193efa99b87aa6bad6c0cef155e543d09e8b" - integrity sha512-h+6w/W1WqXaJA4tb1dk7r5tVbOm97MsKxzwnvOR04UQ6GILroryjMWu3pmCCtL2mLaEStQ0fZgeGiy99mo7iyg== + version "4.1.1" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.1.1.tgz#e0cb02d6eb3af1df719222048e4359efd662af13" + integrity sha512-Rvq+e1e0TFB8E8X+8MQjHSY6vtol45s5gxtLI/018UsAn2IBMmwNEZRM/h+HVnAJRHjasLIKKUO3uvoMM28LvA== dependencies: css-tree "^1.0.0" @@ -4831,10 +4840,10 @@ debug@3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" -debug@4, debug@4.2.0, debug@^4.1.0, debug@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" - integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== +debug@4, debug@^4.1.0, debug@^4.1.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: ms "2.1.2" @@ -4845,10 +4854,17 @@ debug@4.1.0: dependencies: ms "^2.1.1" +debug@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + dependencies: + ms "2.1.2" + debug@^3.1.0, debug@^3.1.1, debug@^3.2.5: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" @@ -5196,7 +5212,7 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -duplexer@^0.1.1: +duplexer@^0.1.1, duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== @@ -5242,9 +5258,9 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= electron-to-chromium@^1.3.591: - version "1.3.593" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.593.tgz#947ccf6dc8e013e2b053d2463ecd1043c164fcef" - integrity sha512-GvO7G1ZxvffnMvPCr4A7+iQPVuvpyqMrx2VWSERAjG+pHK6tmO9XqYdBfMIq9corRyi4bNImSDEiDvIoDb8HrA== + version "1.3.603" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.603.tgz#1b71bec27fb940eccd79245f6824c63d5f7e8abf" + integrity sha512-J8OHxOeJkoSLgBXfV9BHgKccgfLMHh+CoeRo6wJsi6m0k3otaxS/5vrHpMNSEYY4MISwewqanPOuhAtuE8riQQ== elliptic@^6.5.3: version "6.5.3" @@ -6025,9 +6041,9 @@ firebase-functions@^3.6.0: lodash "^4.17.14" firebase-tools@^8.0.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/firebase-tools/-/firebase-tools-8.16.0.tgz#ef81ee17a34f433301abce7315ac33803e08f5de" - integrity sha512-FhkA2QRX1fxN5RCyI0N56esYMCySlAlyKt8inIyP8+anrh/5WHt0FL1JpC2/U3ufTc7jq+eZlCkv31iIyFMOzw== + version "8.16.2" + resolved "https://registry.yarnpkg.com/firebase-tools/-/firebase-tools-8.16.2.tgz#fdcae8fe12411aaac90fb5d1602d34bd721fcd3f" + integrity sha512-3deje+CJ5XtYDsb92YqKdNZaV6+OBJqGA2zatozSaBaKpVkIqTyt9vpglKaar/9N+UIqsIfpjruLS6dCkVk/Gg== dependencies: "@google-cloud/pubsub" "^1.7.0" JSONStream "^1.2.1" @@ -6085,23 +6101,23 @@ firebase-tools@^8.0.0: ws "^7.2.3" "firebase@^7.0.0 || ^8.0.0": - version "8.0.1" - resolved "https://registry.yarnpkg.com/firebase/-/firebase-8.0.1.tgz#24836c654c8577abd640439a5f1bc707bbd9f236" - integrity sha512-7QQKw+ycoR3LhMlxhPM+ND1F2Fx1eDlf3E55xYbmooxFW1t0p94HNENBc3JZytR1H0VoG9nSm2QEHsdr/Ca1Rg== + version "8.1.1" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-8.1.1.tgz#379094b724053931fda1086e9020a17b578e50d5" + integrity sha512-w1plr2jYvzBkx/rHE6A0EJf9318ufA5omShLuGocPlQtrvphel+KJcd+R02outE5E2lSDhyM0l3EoiA0YCD4hA== dependencies: "@firebase/analytics" "0.6.2" "@firebase/app" "0.6.13" "@firebase/app-types" "0.6.1" - "@firebase/auth" "0.15.1" - "@firebase/database" "0.7.1" - "@firebase/firestore" "2.0.1" + "@firebase/auth" "0.15.2" + "@firebase/database" "0.8.1" + "@firebase/firestore" "2.0.4" "@firebase/functions" "0.6.1" "@firebase/installations" "0.4.19" "@firebase/messaging" "0.7.3" "@firebase/performance" "0.4.4" "@firebase/polyfill" "0.3.36" "@firebase/remote-config" "0.1.30" - "@firebase/storage" "0.4.1" + "@firebase/storage" "0.4.2" "@firebase/util" "0.3.4" flat-arguments@^1.0.0: @@ -6392,7 +6408,7 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -6719,16 +6735,23 @@ gtoken@^4.1.0: mime "^2.2.0" gtoken@^5.0.4: - version "5.0.5" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.0.5.tgz#e752d18538576777dfe237887e30fc0627870eae" - integrity sha512-wvjkecutFh8kVfbcdBdUWqDRrXb+WrgD79DBDEYf1Om8S1FluhylhtFjrL7Tx69vNhh259qA3Q1P4sPtb+kUYw== + version "5.1.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.1.0.tgz#4ba8d2fc9a8459098f76e7e8fd7beaa39fda9fe4" + integrity sha512-4d8N6Lk8TEAHl9vVoRVMh9BNOKWVgl2DdNtr3428O75r3QFrF/a5MMu851VmK0AA8+iSvbwRv69k5XnMLURGhg== dependencies: gaxios "^4.0.0" google-p12-pem "^3.0.3" jws "^4.0.0" mime "^2.2.0" -gzip-size@*, gzip-size@^5.1.1: +gzip-size@*: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== + dependencies: + duplexer "^0.1.2" + +gzip-size@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" integrity sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA== @@ -6888,9 +6911,9 @@ hex-color-regex@^1.1.0: integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== highlight.js@^9.17.1: - version "9.18.3" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.3.tgz#a1a0a2028d5e3149e2380f8a865ee8516703d634" - integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ== + version "9.18.5" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" + integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA== hmac-drbg@^1.0.0: version "1.0.1" @@ -7116,10 +7139,12 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -icss-utils@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.0.0.tgz#03ed56c3accd32f9caaf1752ebf64ef12347bb84" - integrity sha512-aF2Cf/CkEZrI/vsu5WI/I+akFgdbwQHVE9YRZxATrhH4PVIe6a3BIjwjEcW+z+jP/hNh+YvM3lAAn1wJQ6opSg== +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" idb@3.0.2: version "3.0.2" @@ -7327,10 +7352,10 @@ inquirer@~6.3.1: strip-ansi "^5.1.0" through "^2.3.6" -install-artifact-from-github@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.1.3.tgz#552f1ec3e693f970726e3f68018ff5885665ec9e" - integrity sha512-iNuncO/pI1w0UOrebs9dwwVpXqERkszPcb7AYq2hbsJDS3X+XdZ+E5kE91EBSc98mjvCMWOoBa1Zk3hVeP1ddA== +install-artifact-from-github@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.2.0.tgz#adcbd123c16a4337ec44ea76d0ebf253cc16b074" + integrity sha512-3OxCPcY55XlVM3kkfIpeCgmoSKnMsz2A3Dbhsq0RXpIknKQmrX1YiznCeW9cD2ItFmDxziA3w6Eg8d80AoL3oA== internal-ip@^4.3.0: version "4.3.0" @@ -8402,14 +8427,6 @@ lie@~3.3.0: dependencies: immediate "~3.0.5" -line-column@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2" - integrity sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI= - dependencies: - isarray "^1.0.0" - isobject "^2.0.0" - lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -8872,10 +8889,10 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -mdn-data@2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.12.tgz#bbb658d08b38f574bbb88f7b83703defdcc46844" - integrity sha512-ULbAlgzVb8IqZ0Hsxm6hHSlQl3Jckst2YEQS7fODu9ilNWy2LvcoSY7TRFIktABP2mdppBioc66va90T+NUs8Q== +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== mdn-data@2.0.4: version "2.0.4" @@ -9261,11 +9278,6 @@ nan@^2.12.1, nan@^2.14.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== -nanoid@^3.1.16: - version "3.1.16" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.16.tgz#b21f0a7d031196faf75314d7c65d36352beeef64" - integrity sha512-+AK8MN0WHji40lj8AEuwLOvLSbWYApQpre/aFJZD71r43wVRLrOYS4FmJOPQYon1TqB462RzrrxlfA74XRES8w== - nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -9446,9 +9458,9 @@ node-libs-browser@^2.2.1: vm-browserify "^1.0.1" node-releases@^1.1.66: - version "1.1.66" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.66.tgz#609bd0dc069381015cd982300bae51ab4f1b1814" - integrity sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg== + version "1.1.67" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" + integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== node-sass-tilde-importer@^1.0.0: version "1.0.2" @@ -10449,33 +10461,38 @@ postcss-minify-selectors@^4.0.2: postcss "^7.0.0" postcss-selector-parser "^3.0.0" -postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" -postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== +postcss-modules-local-by-default@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== dependencies: - icss-utils "^5.0.0" + icss-utils "^4.1.1" + postcss "^7.0.32" postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== +postcss-modules-scope@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== dependencies: - postcss-selector-parser "^6.0.4" + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== dependencies: - icss-utils "^5.0.0" + icss-utils "^4.0.0" + postcss "^7.0.6" postcss-normalize-charset@^4.0.1: version "4.0.1" @@ -10596,7 +10613,7 @@ postcss-selector-parser@^3.0.0: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: version "6.0.4" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== @@ -10664,7 +10681,7 @@ postcss@7.0.32: source-map "^0.6.1" supports-color "^6.1.0" -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.2, postcss@^7.0.27, postcss@^7.0.29, postcss@^7.0.32: +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.27, postcss@^7.0.29, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: version "7.0.35" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== @@ -10673,16 +10690,6 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.2, postcss@^7.0.27, postcss@^7.0.29 source-map "^0.6.1" supports-color "^6.1.0" -postcss@^8.1.1: - version "8.1.7" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.1.7.tgz#ff6a82691bd861f3354fd9b17b2332f88171233f" - integrity sha512-llCQW1Pz4MOPwbZLmOddGM9eIJ8Bh7SZ2Oj5sxZva77uVaotYDsYTch1WBTNu7fUY0fpWp0fdt7uW40D4sRiiQ== - dependencies: - colorette "^1.2.1" - line-column "^1.0.2" - nanoid "^3.1.16" - source-map "^0.6.1" - prepend-http@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" @@ -10742,9 +10749,9 @@ promise-retry@^1.1.1: retry "^0.10.0" protobufjs@^6.8.1, protobufjs@^6.8.6, protobufjs@^6.8.8, protobufjs@^6.8.9: - version "6.10.1" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.1.tgz#e6a484dd8f04b29629e9053344e3970cccf13cd2" - integrity sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ== + version "6.10.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.2.tgz#b9cb6bd8ec8f87514592ba3fdfd28e93f33a469b" + integrity sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -10988,11 +10995,11 @@ rc@^1.2.8: strip-json-comments "~2.0.1" re2@^1.15.0: - version "1.15.8" - resolved "https://registry.yarnpkg.com/re2/-/re2-1.15.8.tgz#654dfbd889acc2649773a2b32dfb9feb357ca9bc" - integrity sha512-CZm4HMuNbY+LP5LjFQvBxbQmvS7iJiVR3w23Bk3jYxZFUj6wPiYRvDikyVpqHYLioVAWcgjG6F90Pk4z7ehUSg== + version "1.15.9" + resolved "https://registry.yarnpkg.com/re2/-/re2-1.15.9.tgz#9ed16171edcb0bc4f0e239bf55229ff3f26acbe3" + integrity sha512-AXWEhpMTBdC+3oqbjdU07dk0pBCvxh5vbOMLERL6Y8FYBSGn4vXlLe8cYszn64Yy7H8keVMrgPzoSvOd4mePpg== dependencies: - install-artifact-from-github "^1.1.3" + install-artifact-from-github "^1.2.0" nan "^2.14.2" node-gyp "^7.1.2" @@ -11489,9 +11496,9 @@ rollup@^0.36.3: source-map-support "^0.4.0" rollup@^2.22.0, rollup@^2.8.0: - version "2.33.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.33.1.tgz#802795164164ee63cd47769d8879c33ec8ae0c40" - integrity sha512-uY4O/IoL9oNW8MMcbA5hcOaz6tZTMIh7qJHx/tzIJm+n1wLoY38BLn6fuy7DhR57oNFLMbDQtDeJoFURt5933w== + version "2.33.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.33.3.tgz#ae72ce31f992b09a580072951bfea76e9df17342" + integrity sha512-RpayhPTe4Gu/uFGCmk7Gp5Z9Qic2VsqZ040G+KZZvsZYdcuWaJg678JeDJJvJeEQXminu24a2au+y92CUWVd+w== optionalDependencies: fsevents "~2.1.2" @@ -11631,7 +11638,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.6.5, schema-utils@^2.7.0: +schema-utils@^2.6.5, schema-utils@^2.7.0, schema-utils@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== @@ -12783,9 +12790,9 @@ terser@^4.1.2: source-map-support "~0.5.12" terser@^5.0.0, terser@^5.3.4: - version "5.3.8" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.8.tgz#991ae8ba21a3d990579b54aa9af11586197a75dd" - integrity sha512-zVotuHoIfnYjtlurOouTazciEfL7V38QMAOhGqpXDEg6yT13cF4+fEP9b0rrCEQTn+tT46uxgFsTZzhygk+CzQ== + version "5.5.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.0.tgz#1406fcb4d4bc517add3b22a9694284c040e33448" + integrity sha512-eopt1Gf7/AQyPhpygdKePTzaet31TvQxXvrf7xYUvD/d8qkCJm4SKPDzu+GHK5ZaYTn8rvttfqaZc3swK21e5g== dependencies: commander "^2.20.0" source-map "~0.7.2" @@ -13211,9 +13218,9 @@ ua-parser-js@0.7.21: integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== uglify-js@^3.1.4: - version "3.11.5" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.11.5.tgz#d6788bc83cf35ff18ea78a65763e480803409bc6" - integrity sha512-btvv/baMqe7HxP7zJSF7Uc16h1mSfuuSplT0/qdjxseesDU+yYzH33eHBH+eMdeRXwujXspaCTooWHQVVBh09w== + version "3.11.6" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.11.6.tgz#144b50d3e05eadd3ad4dd047c60ca541a8cd4e9c" + integrity sha512-oASI1FOJ7BBFkSCNDZ446EgkSuHkOZBuqRFrwXIKWCoXw8ZXQETooTQjkAcBS03Acab7ubCKsXnwuV2svy061g== uglify-js@~2.7.5: version "2.7.5" @@ -13898,6 +13905,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -13997,6 +14013,11 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +y18n@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" + integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== + yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" @@ -14020,7 +14041,7 @@ yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^18.1.0, yargs-parser@^18.1.2: +yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== @@ -14028,22 +14049,10 @@ yargs-parser@^18.1.0, yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@15.3.0: - version "15.3.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.0.tgz#403af6edc75b3ae04bf66c94202228ba119f0976" - integrity sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.0" +yargs-parser@^20.2.2: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== yargs@^13.3.2: version "13.3.2" @@ -14078,6 +14087,19 @@ yargs@^15.0.2, yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.1.1: + version "16.1.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.1.1.tgz#5a4a095bd1ca806b0a50d0c03611d38034d219a1" + integrity sha512-hAD1RcFP/wfgfxgMVswPE+z3tlPFtxG8/yWUrG2i17sTWGCGqWnxKcLTF4cUKDUK8fzokwsmO9H0TDkRbMHy8w== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"