From 2df0da932c1df31bc054b42071ba7666034f8ffa Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 May 2019 22:10:43 -0700 Subject: [PATCH 1/9] chore(): Remove timing out app deletes --- src/firestore/collection/collection.spec.ts | 6 +++--- src/firestore/document/document.spec.ts | 6 +++--- src/firestore/firestore.spec.ts | 8 +++++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/firestore/collection/collection.spec.ts b/src/firestore/collection/collection.spec.ts index 8134b9e35..402112d82 100644 --- a/src/firestore/collection/collection.spec.ts +++ b/src/firestore/collection/collection.spec.ts @@ -30,7 +30,7 @@ describe('AngularFirestoreCollection', () => { TestBed.configureTestingModule({ imports: [ AngularFireModule.initializeApp(COMMON_CONFIG), - AngularFirestoreModule.enablePersistence() + AngularFirestoreModule.enablePersistence({experimentalTabSynchronization: true}) ] }); inject([FirebaseApp, AngularFirestore], (_app: FirebaseApp, _afs: AngularFirestore) => { @@ -39,8 +39,8 @@ describe('AngularFirestoreCollection', () => { })(); }); - afterEach(async (done) => { - await app.delete(); + afterEach(done => { + app.delete(); done(); }); diff --git a/src/firestore/document/document.spec.ts b/src/firestore/document/document.spec.ts index 8552934fd..281ddbd60 100644 --- a/src/firestore/document/document.spec.ts +++ b/src/firestore/document/document.spec.ts @@ -19,7 +19,7 @@ describe('AngularFirestoreDocument', () => { TestBed.configureTestingModule({ imports: [ AngularFireModule.initializeApp(COMMON_CONFIG), - AngularFirestoreModule.enablePersistence() + AngularFirestoreModule.enablePersistence({experimentalTabSynchronization: true}) ] }); inject([FirebaseApp, AngularFirestore], (_app: FirebaseApp, _afs: AngularFirestore) => { @@ -28,8 +28,8 @@ describe('AngularFirestoreDocument', () => { })(); }); - afterEach(async (done) => { - await app.delete(); + afterEach(done => { + app.delete(); done(); }); diff --git a/src/firestore/firestore.spec.ts b/src/firestore/firestore.spec.ts index f844021af..d25ad555f 100644 --- a/src/firestore/firestore.spec.ts +++ b/src/firestore/firestore.spec.ts @@ -116,7 +116,8 @@ describe('AngularFirestore with different app', () => { }); afterEach(done => { - app.delete().then(done, done.fail); + app.delete(); + done(); }); describe('', () => { @@ -155,6 +156,11 @@ describe('AngularFirestore without persistance', () => { })(); }); + afterEach(done => { + app.delete(); + done(); + }); + it('should not enable persistence', (done) => { afs.persistenceEnabled$.subscribe(isEnabled => { expect(isEnabled).toBe(false); From 8b96c44254a8fbc322b71e4ccb4cab2b5e2669f9 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 May 2019 22:30:08 -0700 Subject: [PATCH 2/9] chore(): Bump Firebase to allow 6.0 --- package.json | 2 +- src/firestore/collection/collection.spec.ts | 2 +- src/firestore/document/document.spec.ts | 2 +- src/firestore/firestore.spec.ts | 2 +- yarn.lock | 194 +++++++++++++++----- 5 files changed, 149 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index 6ba7e0195..0ba1fb01a 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@angular/core": ">=6.0.0 <8", "@angular/platform-browser": ">=6.0.0 <8", "@angular/platform-browser-dynamic": ">=6.0.0 <8", - "firebase": "^5.5.0", + "firebase": ">= 5.5.0 <7", "rxjs": "^6.0.0", "ws": "^3.3.2", "xhr2": "^0.1.4", diff --git a/src/firestore/collection/collection.spec.ts b/src/firestore/collection/collection.spec.ts index 402112d82..e7f9c965b 100644 --- a/src/firestore/collection/collection.spec.ts +++ b/src/firestore/collection/collection.spec.ts @@ -30,7 +30,7 @@ describe('AngularFirestoreCollection', () => { TestBed.configureTestingModule({ imports: [ AngularFireModule.initializeApp(COMMON_CONFIG), - AngularFirestoreModule.enablePersistence({experimentalTabSynchronization: true}) + AngularFirestoreModule.enablePersistence({synchronizeTabs: true}) ] }); inject([FirebaseApp, AngularFirestore], (_app: FirebaseApp, _afs: AngularFirestore) => { diff --git a/src/firestore/document/document.spec.ts b/src/firestore/document/document.spec.ts index 281ddbd60..b9492634f 100644 --- a/src/firestore/document/document.spec.ts +++ b/src/firestore/document/document.spec.ts @@ -19,7 +19,7 @@ describe('AngularFirestoreDocument', () => { TestBed.configureTestingModule({ imports: [ AngularFireModule.initializeApp(COMMON_CONFIG), - AngularFirestoreModule.enablePersistence({experimentalTabSynchronization: true}) + AngularFirestoreModule.enablePersistence({synchronizeTabs: true}) ] }); inject([FirebaseApp, AngularFirestore], (_app: FirebaseApp, _afs: AngularFirestore) => { diff --git a/src/firestore/firestore.spec.ts b/src/firestore/firestore.spec.ts index d25ad555f..5b224851c 100644 --- a/src/firestore/firestore.spec.ts +++ b/src/firestore/firestore.spec.ts @@ -23,7 +23,7 @@ describe('AngularFirestore', () => { TestBed.configureTestingModule({ imports: [ AngularFireModule.initializeApp(COMMON_CONFIG), - AngularFirestoreModule.enablePersistence({experimentalTabSynchronization: true}) + AngularFirestoreModule.enablePersistence({synchronizeTabs: true}) ] }); inject([FirebaseApp, AngularFirestore], (_app: FirebaseApp, _afs: AngularFirestore) => { diff --git a/yarn.lock b/yarn.lock index 9f0c458a2..cab2104cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -69,63 +69,64 @@ tslib "^1.9.0" xhr2 "^0.1.4" -"@firebase/app-types@0.3.10": - version "0.3.10" - resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.3.10.tgz#8f6d24d80bf833622b53ed26eaa04cfa9dd0f2f3" - integrity sha512-l+5BJtSQopalBXiY/YuSaB9KF9PnDj37FLV0Sx3qJjh5B3IthCuZbPc1Vpbbbee/QZgudl0G212BBsUMGHP+fQ== +"@firebase/app-types@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.4.0.tgz#bb2c651f3b275fef549050cff28af752839c75c0" + integrity sha512-8erNMHc0V26gA6Nj4W9laVrQrXHsj9K2TEM7eL2IQogGSHLL4vet3UNekYfcGQ2cjfvwUjMzd+BNS/8S7GnfiA== -"@firebase/app@0.3.17": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.3.17.tgz#491dc3bc1a2837bbb4869161cc9852cfc04da891" - integrity sha512-/8lDeeIxgdCIMffrfBPQ3bcdSkF8bx4KCp8pKMPOG/HYKoeM8I9eP4zlzxL5ABzRjvcdhK9KOYOn0jRrNrGD9g== +"@firebase/app@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.4.0.tgz#4f2e262ef258c351f3be6d41638b3eca43792f8a" + integrity sha512-Q6fANJhL67qAjcha2iTpC+yCKbSkaoToo9MFDFY3NS2cr/6toNQms04pHKgSU4OylSgvU9B9JKwQfQeTkKz2fg== dependencies: - "@firebase/app-types" "0.3.10" + "@firebase/app-types" "0.4.0" "@firebase/util" "0.2.14" dom-storage "2.1.0" tslib "1.9.3" xmlhttprequest "1.8.0" -"@firebase/auth-types@0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.6.1.tgz#9b60142e3a4adc1db09c037d068ab98cd54c10a8" - integrity sha512-uciPeIQJC1NZDhI5+BWbyqi70YXIjT3jm03sYtIgkPt2sr3n8sq1RpnoTMYfAJkQ0QlgLaBkeM/huMx06eBoXQ== +"@firebase/auth-types@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.7.0.tgz#8aac4b9c04aff61362827c35b5ad36db16a837ba" + integrity sha512-QEG9azYwssGWcb4NaKFHe3Piez0SG46nRlu76HM4/ob0sjjNpNTY1Z5C3IoeJYknp2kMzuQi0TTW8tjEgkUAUA== -"@firebase/auth@0.10.2": - version "0.10.2" - resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.10.2.tgz#ef7a34f4667445ebaf4972622141c8fa4dffb961" - integrity sha512-+S8RZcHhhat2xrW/RGOcSZO8pv0qHveaw09Bq/gXhZyJfN86UeiMc3sv4YMo1Hu7fRRorNteijpmlH522eI0AA== +"@firebase/auth@0.11.1": + version "0.11.1" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.11.1.tgz#842cfd97b2c9d5c48e46fd9936f8fa9622717c29" + integrity sha512-TxFeWqrtdELyzUYBDWF/J49JAxF3W0O77PvT42RoZm8C7nrls0LUAQoPjdFNjaQ+5o/MjHDnd9YMqwDAD01Oqw== dependencies: - "@firebase/auth-types" "0.6.1" + "@firebase/auth-types" "0.7.0" -"@firebase/database-types@0.3.11": - version "0.3.11" - resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.3.11.tgz#6bfcaca8e14e7d6bb67d723f0c2d7febbeefa054" - integrity sha512-iRAZzs7Zlmmvh7r0XlR1MAO6I6bm1HjW9m1ytfJ6E/8+zItHnbVH4iiVVkC39r1wMGrtPMz8FiIUWoaasPF5dA== +"@firebase/database-types@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.4.0.tgz#71a711a3f666fac905422e130731930e2bcca582" + integrity sha512-2piRYW7t+2s/P1NPpcI/3+8Y5l2WnJhm9KACoXW5zmoAPlya8R1aEaR2dNHLNePTMHdg04miEDD9fEz4xUqzZA== -"@firebase/database@0.3.20": - version "0.3.20" - resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.3.20.tgz#6851d8ef3229aeb7bcbe5e851434672abae43ee4" - integrity sha512-fZHRIlRQlND/UrzI1beUTRKfktjMvMEiUOar6ylFZqOj2KNVO4CrF95UGqRl0HBGhZzlBKzaDYAcJze2D6C4+Q== +"@firebase/database@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.4.0.tgz#a72bac498f1d593fafee739982f01ac49fa7be59" + integrity sha512-KbcepR1QKJshgrr9EG2FqiThyWBfHflpHlAsrQBsUCNnxQQcU+AMIWCz3YauHAR6S68iBplJNYLzShpwsrBFCw== dependencies: - "@firebase/database-types" "0.3.11" + "@firebase/database-types" "0.4.0" "@firebase/logger" "0.1.13" "@firebase/util" "0.2.14" faye-websocket "0.11.1" tslib "1.9.3" -"@firebase/firestore-types@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-1.2.1.tgz#ac09c2e1b6324991cd05c1ce1f74e63771237fb8" - integrity sha512-/Klu3uVLoTjW3ckYqFTV3lr9HzEKM7pMpPHao1Sy+YwIUmTjFMI1LE2WcXMx6HN2jipFjjD/Xjg0hY0+0dnPCg== +"@firebase/firestore-types@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-1.3.0.tgz#a32c132fff2bc77d36b6e864a3cc76c9cb75c965" + integrity sha512-XPnfAaYsKgYivgl/U1+M5ulBG9Hxv52zrZR5TuaoKCU791t/E3K85rT1ZGtEHu9Fj4CPTep2NSl8I30MQpUlHA== -"@firebase/firestore@1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-1.2.2.tgz#9a97e60cc20bda7b06a6985190b4f33357a4fe28" - integrity sha512-5o3SFTpMYaWrWRlm5qBX84fNDwdiPTbb0qo6KDI+OvIzTaMsEfOJ4vUz+Binxfq0dPen0fU6JLO+xix8Sa8TBA== +"@firebase/firestore@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-1.3.0.tgz#f704a778182c9df8efaeae1a4f951bb3170fa630" + integrity sha512-KD+g6aGAGG/Z/qUR0tCN51XuScC1MmpIJuQt/vgO++WaADqRqURrhK/myEaGpE6GopKhZwcosoRJdeSyg9sTnQ== dependencies: - "@firebase/firestore-types" "1.2.1" + "@firebase/firestore-types" "1.3.0" "@firebase/logger" "0.1.13" "@firebase/webchannel-wrapper" "0.2.19" + "@grpc/proto-loader" "^0.5.0" grpc "1.20.0" tslib "1.9.3" @@ -182,10 +183,10 @@ resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.0.1.tgz#749b6351f5f802ec7a9be5737546eeda18e7ac4a" integrity sha512-U45GbVAnPyz7wPLd3FrWdTeaFSvgsnGfGK58VojfEMmFnMAixCM3qBv1XJ0xfhyKbK1xZN4+usWAR8F3CwRAXw== -"@firebase/performance@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.2.0.tgz#cf0898ece0959dd63e8157ed29d3e8cd571d3d09" - integrity sha512-XvBphI7THzaw0gdDJSeUEb+EGYGWOrwB78F30DBSP2d+UBtx8+UwJf/+Vlt0AlZFhv4v7ROFgGNYiLlY3z9lnw== +"@firebase/performance@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.2.1.tgz#14cf8c747672ca529cc6d07234ef5baab227d4c9" + integrity sha512-vo/24+W35foc2ShRgeIlx2Ej45+Sn6uYPpnYzTtJb3DwE3sb0BVGocVgINbXyguUq2PHS+6yLsCm88y12DS2EA== dependencies: "@firebase/installations" "0.1.0" "@firebase/logger" "0.1.13" @@ -227,6 +228,67 @@ resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.19.tgz#991df31d892a51414e0e544b5cff4216cfb04915" integrity sha512-U9e2dCB38mD2AvV/zAjghauwa0UX15Wt98iBgm8IOw8spluDxysx8UZFUhj38fu0iFXORVRBqseyK2wCxZIl5w== +"@grpc/proto-loader@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.0.tgz#6d21930530db6089ed68a90f10a22b76fdc3387d" + integrity sha512-kF5toaC4A7PRjAuIxE0fYAv8WarJ6JELYlmHpkoo4EGTFvXUsQwVfyj2bgPV2023M77s2TtDn36wPMJB0sz8nA== + dependencies: + lodash.camelcase "^4.3.0" + protobufjs "^6.8.6" + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= + "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -244,6 +306,11 @@ resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.8.8.tgz#bf53a7d193ea8b03867a38bfdb4fbb0e0bf066c9" integrity sha512-OJSUxLaxXsjjhob2DBzqzgrkLmukM3+JMpRp0r0E4HTdT1nwDCWhaswjYxazPij6uOdzHCJfNbDjmQ1/rnNbCg== +"@types/long@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" + integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q== + "@types/node@*": version "10.3.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.3.tgz#8798d9e39af2fa604f715ee6a6b19796528e46c3" @@ -254,6 +321,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.113.tgz#4b41f38ad03e4b41f9dc259b3b58aecb22c9aebc" integrity sha512-f9XXUWFqryzjkZA1EqFvJHSFyqyasV17fq8zCDIzbRV4ctL7RrJGKvG+lcex86Rjbzd1GrER9h9VmF5sSjV0BQ== +"@types/node@^10.1.0": + version "10.14.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.6.tgz#9cbfcb62c50947217f4d88d4d274cc40c22625a9" + integrity sha512-Fvm24+u85lGmV4hT5G++aht2C5I4Z4dYlWZIh62FAfFO/TfzXtPpoLI6I7AuBWkIFqZCnhFOoTT7RjjaIL5Fjg== + "@types/request@0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/request/-/request-0.0.30.tgz#18208841a0cf6538eff5e306bfa92e86c8c8acae" @@ -2292,18 +2364,18 @@ fined@^1.0.1: object.pick "^1.2.0" parse-filepath "^1.0.1" -firebase@^5.5.0: - version "5.11.0" - resolved "https://registry.yarnpkg.com/firebase/-/firebase-5.11.0.tgz#d1a37b822e01680a3d8d4b476b48d8c80949d9c1" - integrity sha512-9SKdrjwOU9X0n83Qw+RjQ2v3Fsst0O4x1EuhHj4OZLCe8IVI4aTQNEJqAPP/fBwhdQS/nFNQfcWPxbBu6EF4qw== +"firebase@>= 5.5.0 <7": + version "6.0.1" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-6.0.1.tgz#14edc0e169b3d413b419359320ea2ecd0ec38cbd" + integrity sha512-Vi9VpGlUmVsEQ4nwcSjv2Rlo+G4umneZScEyKr6qiErfS5+J+fDsOp8UAzEexhZ1cnuKIZXmTF1KqQsXNiSlog== dependencies: - "@firebase/app" "0.3.17" - "@firebase/auth" "0.10.2" - "@firebase/database" "0.3.20" - "@firebase/firestore" "1.2.2" + "@firebase/app" "0.4.0" + "@firebase/auth" "0.11.1" + "@firebase/database" "0.4.0" + "@firebase/firestore" "1.3.0" "@firebase/functions" "0.4.6" "@firebase/messaging" "0.3.19" - "@firebase/performance" "0.2.0" + "@firebase/performance" "0.2.1" "@firebase/polyfill" "0.3.13" "@firebase/storage" "0.2.15" @@ -4073,6 +4145,11 @@ loggly@^1.1.0: request "2.75.x" timespan "2.3.x" +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + long@~3: version "3.2.0" resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" @@ -5162,6 +5239,25 @@ protobufjs@^5.0.3: glob "^7.0.5" yargs "^3.10.0" +protobufjs@^6.8.6: + version "6.8.8" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" + integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.0" + "@types/node" "^10.1.0" + long "^4.0.0" + protractor@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/protractor/-/protractor-3.0.0.tgz#ac76778770f629bab2afa56a6aac9c59ca6d259d" From 80a7cf58998949aa4197ba68fa63e113c4372df7 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 May 2019 23:04:29 -0700 Subject: [PATCH 3/9] Wat --- .../collection-group/collection-group.spec.ts | 447 ++++++++++++++++++ .../collection-group/collection-group.ts | 119 +++++ src/firestore/collection/changes.ts | 6 +- src/firestore/firestore.ts | 16 +- src/firestore/index.spec.ts | 1 + src/firestore/interfaces.ts | 2 + src/firestore/public_api.ts | 1 + src/root.spec.js | 1 + 8 files changed, 589 insertions(+), 4 deletions(-) create mode 100644 src/firestore/collection-group/collection-group.spec.ts create mode 100644 src/firestore/collection-group/collection-group.ts diff --git a/src/firestore/collection-group/collection-group.spec.ts b/src/firestore/collection-group/collection-group.spec.ts new file mode 100644 index 000000000..09d34491d --- /dev/null +++ b/src/firestore/collection-group/collection-group.spec.ts @@ -0,0 +1,447 @@ +import { FirebaseApp, AngularFireModule } from '@angular/fire'; +import { AngularFirestore } from '../firestore'; +import { AngularFirestoreModule } from '../firestore.module'; +import { AngularFirestoreDocument } from '../document/document'; +import { AngularFirestoreCollectionGroup } from './collection-group'; +import { QueryGroupFn, Query } from '../interfaces'; +import { Observable, BehaviorSubject, Subscription } from 'rxjs'; +import { skip, take, switchMap } from 'rxjs/operators'; + +import { TestBed, inject } from '@angular/core/testing'; +import { COMMON_CONFIG } from '../test-config'; + +import { Stock, randomName, FAKE_STOCK_DATA, createRandomStocks, delayAdd, delayDelete, delayUpdate, deleteThemAll } from '../utils.spec'; + +async function collectionHarness(afs: AngularFirestore, items: number, queryGroupFn?: QueryGroupFn) { + const randomCollectionName = randomName(afs.firestore); + const ref = afs.firestore.collection(`${randomCollectionName}`); + const collectionGroup: Query = afs.firestore.collectionGroup(randomCollectionName); + const queryFn = queryGroupFn || (ref => ref); + const stocks = new AngularFirestoreCollectionGroup(queryFn(collectionGroup), afs); + let names = await createRandomStocks(afs.firestore, ref, items); + return { randomCollectionName, ref, stocks, names }; +} + +describe('AngularFirestoreCollectionGroup', () => { + let app: FirebaseApp; + let afs: AngularFirestore; + let sub: Subscription; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG), + AngularFirestoreModule.enablePersistence({synchronizeTabs:true}) + ] + }); + inject([FirebaseApp, AngularFirestore], (_app: FirebaseApp, _afs: AngularFirestore) => { + app = _app; + afs = _afs; + })(); + }); + + afterEach(done => { + app.delete(); + done(); + }); + + describe('valueChanges()', () => { + + it('should get unwrapped snapshot', async (done: any) => { + const ITEMS = 4; + const { randomCollectionName, 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 { randomCollectionName, 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 { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.valueChanges(); + changes.pipe(take(1)).subscribe(() => {}).add(() => { + const sub = 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; + let firstIndex = 0; + let pricefilter$ = new BehaviorSubject(null); + const randomCollectionName = randomName(afs.firestore); + const ref = afs.firestore.collection(`${randomCollectionName}`); + let 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 { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); + const sub = stocks.snapshotChanges().subscribe(data => { + const ids = data.map(d => d.payload.doc.id); + 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 { randomCollectionName, 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 { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.snapshotChanges(); + changes.pipe(take(1)).subscribe(() => {}).add(() => { + const sub = 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 { randomCollectionName, 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 { randomCollectionName, 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; + let { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); + 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 }); + }); + + it('should be able to filter snapshotChanges() types - added/modified', async (done) => { + const ITEMS = 10; + let { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); + const nextId = ref.doc('a').id; + let count = 0; + + const sub = 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 { randomCollectionName, 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 { randomCollectionName, 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 { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); + const sub = 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 { randomCollectionName, 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 { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); + const changes = stocks.stateChanges(); + changes.pipe(take(1)).subscribe(() => {}).add(() => { + const sub = 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; + let count = 0; + const { randomCollectionName, 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; + let count = 0; + let { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); + + 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 { randomCollectionName, 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 { randomCollectionName, 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 { randomCollectionName, 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/collection-group/collection-group.ts b/src/firestore/collection-group/collection-group.ts new file mode 100644 index 000000000..9b71d1567 --- /dev/null +++ b/src/firestore/collection-group/collection-group.ts @@ -0,0 +1,119 @@ +import { Observable, from } from 'rxjs'; +import { fromCollectionRef } from '../observable/fromRef'; +import { map, filter, scan } from 'rxjs/operators'; +import { firestore } from 'firebase/app'; + +import { DocumentChangeType, CollectionReference, Query, DocumentReference, DocumentData, DocumentChangeAction } from '../interfaces'; +import { validateEventsArray } from '../collection/collection'; +import { docChanges, sortedChanges } from '../collection/changes'; +import { AngularFirestoreDocument } from '../document/document'; +import { AngularFirestore } from '../firestore'; +import { runInZone } from '@angular/fire'; + +/** + * 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 AngularFirestoreCollectionGroup { + /** + * 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. + * @param ref + */ + constructor( + private readonly query: Query, + 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. + * @param events + */ + stateChanges(events?: DocumentChangeType[]): Observable[]> { + if(!events || events.length === 0) { + return this.afs.scheduler.keepUnstableUntilFirst( + this.afs.scheduler.runOutsideAngular( + docChanges(this.query) + ) + ); + } + return this.afs.scheduler.keepUnstableUntilFirst( + this.afs.scheduler.runOutsideAngular( + docChanges(this.query) + ) + ) + .pipe( + map(actions => actions.filter(change => events.indexOf(change.type) > -1)), + filter(changes => changes.length > 0) + ); + } + + /** + * 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. + * @param events + */ + 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. + * @param events + */ + snapshotChanges(events?: DocumentChangeType[]): Observable[]> { + const validatedEvents = validateEventsArray(events); + const sortedChanges$ = sortedChanges(this.query, validatedEvents); + const scheduledSortedChanges$ = this.afs.scheduler.runOutsideAngular(sortedChanges$); + return this.afs.scheduler.keepUnstableUntilFirst(scheduledSortedChanges$); + } + + /** + * Listen to all documents in the collection and its possible query as an Observable. + */ + valueChanges(): Observable { + const fromCollectionRef$ = fromCollectionRef(this.query); + const scheduled$ = this.afs.scheduler.runOutsideAngular(fromCollectionRef$); + return this.afs.scheduler.keepUnstableUntilFirst(scheduled$) + .pipe( + map(actions => actions.payload.docs.map(a => a.data())) + ); + } + + /** + * Retrieve the results of the query once. + * @param options + */ + get(options?: firestore.GetOptions) { + return from(this.query.get(options)).pipe( + runInZone(this.afs.scheduler.zone) + ); + } + +} diff --git a/src/firestore/collection/changes.ts b/src/firestore/collection/changes.ts index f02260539..8886a7540 100644 --- a/src/firestore/collection/changes.ts +++ b/src/firestore/collection/changes.ts @@ -54,14 +54,14 @@ export function combineChanges(current: DocumentChange[], changes: Documen export function combineChange(combined: DocumentChange[], change: DocumentChange): DocumentChange[] { switch(change.type) { case 'added': - if (combined[change.newIndex] && combined[change.newIndex].doc.id == change.doc.id) { + if (combined[change.newIndex] && combined[change.newIndex].doc.isEqual(change.doc)) { // Not sure why the duplicates are getting fired } else { combined.splice(change.newIndex, 0, change); } break; case 'modified': - if (combined[change.oldIndex] == null || combined[change.oldIndex].doc.id == change.doc.id) { + if (combined[change.oldIndex] == null || combined[change.oldIndex].doc.isEqual(change.doc)) { // When an item changes position we first remove it // and then add it's new position if(change.oldIndex !== change.newIndex) { @@ -73,7 +73,7 @@ export function combineChange(combined: DocumentChange[], change: Document } break; case 'removed': - if (combined[change.oldIndex] && combined[change.oldIndex].doc.id == change.doc.id) { + if (combined[change.oldIndex] && combined[change.oldIndex].doc.isEqual(change.doc)) { combined.splice(change.oldIndex, 1); } break; diff --git a/src/firestore/firestore.ts b/src/firestore/firestore.ts index 7a912b667..091adcf5a 100644 --- a/src/firestore/firestore.ts +++ b/src/firestore/firestore.ts @@ -2,9 +2,10 @@ import { InjectionToken, NgZone, PLATFORM_ID, Injectable, Inject, Optional } fro import { Observable, of, from } from 'rxjs'; -import { Settings, PersistenceSettings, CollectionReference, DocumentReference, QueryFn, AssociatedReference } from './interfaces'; +import { Settings, PersistenceSettings, CollectionReference, DocumentReference, QueryFn, Query, QueryGroupFn, AssociatedReference } from './interfaces'; import { AngularFirestoreDocument } from './document/document'; import { AngularFirestoreCollection } from './collection/collection'; +import { AngularFirestoreCollectionGroup } from './collection-group/collection-group'; import { FirebaseFirestore, FirebaseOptions, FirebaseAppConfig, FirebaseOptionsToken, FirebaseNameOrConfigToken, _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; import { isPlatformServer } from '@angular/common'; @@ -161,6 +162,19 @@ export class AngularFirestore { return new AngularFirestoreCollection(ref, query, this); } + /** + * Create a reference to a Firestore Collection Group based on a collectionId + * and an optional query function to narrow the result + * set. + * @param collectionId + * @param queryGroupFn + */ + collectionGroup(collectionId: string, queryGroupFn?: QueryGroupFn): AngularFirestoreCollectionGroup { + const queryFn = queryGroupFn || (ref => ref); + const collectionGroup: Query = this.firestore.collectionGroup(collectionId); + return new AngularFirestoreCollectionGroup(queryFn(collectionGroup), this); + } + /** * Create a reference to a Firestore Document based on a path or * DocumentReference. Note that documents are not queryable because they are diff --git a/src/firestore/index.spec.ts b/src/firestore/index.spec.ts index fec1dc433..5824fa9c2 100644 --- a/src/firestore/index.spec.ts +++ b/src/firestore/index.spec.ts @@ -1,3 +1,4 @@ export * from './firestore.spec'; export * from './document/document.spec'; export * from './collection/collection.spec'; +export * from './collection-group/collection-group.spec'; \ No newline at end of file diff --git a/src/firestore/interfaces.ts b/src/firestore/interfaces.ts index e1d186a31..549022742 100644 --- a/src/firestore/interfaces.ts +++ b/src/firestore/interfaces.ts @@ -56,6 +56,8 @@ export interface Reference { // 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 diff --git a/src/firestore/public_api.ts b/src/firestore/public_api.ts index e5cbd8250..dbfa0fdaa 100644 --- a/src/firestore/public_api.ts +++ b/src/firestore/public_api.ts @@ -1,6 +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'; diff --git a/src/root.spec.js b/src/root.spec.js index 27cfec26b..352ed6c06 100644 --- a/src/root.spec.js +++ b/src/root.spec.js @@ -4,6 +4,7 @@ export * from './packages-dist/auth/auth.spec'; export * from './packages-dist/firestore/firestore.spec'; export * from './packages-dist/firestore/document/document.spec'; export * from './packages-dist/firestore/collection/collection.spec'; +export * from './packages-dist/firestore/collection-group/collection-group.spec'; export * from './packages-dist/functions/functions.spec'; export * from './packages-dist/database/database.spec'; export * from './packages-dist/database/utils.spec'; From c52a3979ddc8a2c6111131789de3b9aa6ddcfb8b Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 May 2019 23:27:07 -0700 Subject: [PATCH 4/9] More --- src/firestore/collection-group/collection-group.spec.ts | 3 ++- src/firestore/collection/changes.ts | 6 +++--- src/firestore/firestore.ts | 3 ++- yarn.lock | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/firestore/collection-group/collection-group.spec.ts b/src/firestore/collection-group/collection-group.spec.ts index 09d34491d..68f2b425c 100644 --- a/src/firestore/collection-group/collection-group.spec.ts +++ b/src/firestore/collection-group/collection-group.spec.ts @@ -15,7 +15,8 @@ import { Stock, randomName, FAKE_STOCK_DATA, createRandomStocks, delayAdd, delay async function collectionHarness(afs: AngularFirestore, items: number, queryGroupFn?: QueryGroupFn) { const randomCollectionName = randomName(afs.firestore); const ref = afs.firestore.collection(`${randomCollectionName}`); - const collectionGroup: Query = afs.firestore.collectionGroup(randomCollectionName); + const firestore: any = afs.firestore; + const collectionGroup: Query = firestore.collectionGroup(randomCollectionName); const queryFn = queryGroupFn || (ref => ref); const stocks = new AngularFirestoreCollectionGroup(queryFn(collectionGroup), afs); let names = await createRandomStocks(afs.firestore, ref, items); diff --git a/src/firestore/collection/changes.ts b/src/firestore/collection/changes.ts index 8886a7540..7b16c5f77 100644 --- a/src/firestore/collection/changes.ts +++ b/src/firestore/collection/changes.ts @@ -54,14 +54,14 @@ export function combineChanges(current: DocumentChange[], changes: Documen export function combineChange(combined: DocumentChange[], change: DocumentChange): DocumentChange[] { switch(change.type) { case 'added': - if (combined[change.newIndex] && combined[change.newIndex].doc.isEqual(change.doc)) { + if (combined[change.newIndex] && combined[change.newIndex].doc.ref.isEqual(change.doc.ref)) { // Not sure why the duplicates are getting fired } else { combined.splice(change.newIndex, 0, change); } break; case 'modified': - if (combined[change.oldIndex] == null || combined[change.oldIndex].doc.isEqual(change.doc)) { + if (combined[change.oldIndex] == null || combined[change.oldIndex].doc.ref.isEqual(change.doc.ref)) { // When an item changes position we first remove it // and then add it's new position if(change.oldIndex !== change.newIndex) { @@ -73,7 +73,7 @@ export function combineChange(combined: DocumentChange[], change: Document } break; case 'removed': - if (combined[change.oldIndex] && combined[change.oldIndex].doc.isEqual(change.doc)) { + if (combined[change.oldIndex] && combined[change.oldIndex].doc.ref.isEqual(change.doc.ref)) { combined.splice(change.oldIndex, 1); } break; diff --git a/src/firestore/firestore.ts b/src/firestore/firestore.ts index 091adcf5a..94ce4d920 100644 --- a/src/firestore/firestore.ts +++ b/src/firestore/firestore.ts @@ -171,7 +171,8 @@ export class AngularFirestore { */ collectionGroup(collectionId: string, queryGroupFn?: QueryGroupFn): AngularFirestoreCollectionGroup { const queryFn = queryGroupFn || (ref => ref); - const collectionGroup: Query = this.firestore.collectionGroup(collectionId); + const firestore: any = this.firestore; // SEMVER: ditch any once targeting >= 6.0 + const collectionGroup: Query = firestore.collectionGroup(collectionId); return new AngularFirestoreCollectionGroup(queryFn(collectionGroup), this); } diff --git a/yarn.lock b/yarn.lock index cab2104cc..4945ef0cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2364,7 +2364,7 @@ fined@^1.0.1: object.pick "^1.2.0" parse-filepath "^1.0.1" -"firebase@>= 5.5.0 <7": +"firebase@>= 5.9.0 <7": version "6.0.1" resolved "https://registry.yarnpkg.com/firebase/-/firebase-6.0.1.tgz#14edc0e169b3d413b419359320ea2ecd0ec38cbd" integrity sha512-Vi9VpGlUmVsEQ4nwcSjv2Rlo+G4umneZScEyKr6qiErfS5+J+fDsOp8UAzEexhZ1cnuKIZXmTF1KqQsXNiSlog== From 3a45ea9a7769ec06d7c8d17351d774d14c9ff9d5 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 May 2019 23:38:01 -0700 Subject: [PATCH 5/9] error --- src/firestore/firestore.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/firestore/firestore.ts b/src/firestore/firestore.ts index 94ce4d920..039e62ec8 100644 --- a/src/firestore/firestore.ts +++ b/src/firestore/firestore.ts @@ -170,6 +170,7 @@ export class AngularFirestore { * @param queryGroupFn */ collectionGroup(collectionId: string, queryGroupFn?: QueryGroupFn): AngularFirestoreCollectionGroup { + if (major < 6) { throw "collection group queries require Firebase JS SDK >= 6.0"} const queryFn = queryGroupFn || (ref => ref); const firestore: any = this.firestore; // SEMVER: ditch any once targeting >= 6.0 const collectionGroup: Query = firestore.collectionGroup(collectionId); From f57d6f76974237f9b5e74fccaff0c290a2d18e92 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 May 2019 23:39:23 -0700 Subject: [PATCH 6/9] Yarnin --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 4945ef0cc..cab2104cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2364,7 +2364,7 @@ fined@^1.0.1: object.pick "^1.2.0" parse-filepath "^1.0.1" -"firebase@>= 5.9.0 <7": +"firebase@>= 5.5.0 <7": version "6.0.1" resolved "https://registry.yarnpkg.com/firebase/-/firebase-6.0.1.tgz#14edc0e169b3d413b419359320ea2ecd0ec38cbd" integrity sha512-Vi9VpGlUmVsEQ4nwcSjv2Rlo+G4umneZScEyKr6qiErfS5+J+fDsOp8UAzEexhZ1cnuKIZXmTF1KqQsXNiSlog== From 1b954ba3a34b6b6f84f96689755ad5c0d46930d7 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Sun, 19 May 2019 18:14:07 -0700 Subject: [PATCH 7/9] Fixing tests for AngularFirestoreCollectionGroup --- .../collection-group/collection-group.spec.ts | 47 ++++++++++++++----- src/firestore/utils.spec.ts | 6 +-- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/firestore/collection-group/collection-group.spec.ts b/src/firestore/collection-group/collection-group.spec.ts index 68f2b425c..cb5c7b527 100644 --- a/src/firestore/collection-group/collection-group.spec.ts +++ b/src/firestore/collection-group/collection-group.spec.ts @@ -139,7 +139,7 @@ describe('AngularFirestoreCollectionGroup', () => { // the first time should all be 'added' if(count === 1) { // make an update - stocks.doc(names[0]).update({ price: 2}); + 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 @@ -192,7 +192,7 @@ describe('AngularFirestoreCollectionGroup', () => { 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 }); + 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 @@ -220,7 +220,7 @@ describe('AngularFirestoreCollectionGroup', () => { deleteThemAll(names, ref).then(done).catch(done.fail); }); - delayUpdate(stocks, names[0], { price: 2 }); + delayUpdate(ref, names[0], { price: 2 }); }); it('should be able to filter snapshotChanges() types - added', async (done) => { @@ -240,7 +240,28 @@ describe('AngularFirestoreCollectionGroup', () => { names = names.concat([nextId]); - delayAdd(stocks, nextId, { price: 2 }); + // 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; + let { 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 }); }); it('should be able to filter snapshotChanges() types - added/modified', async (done) => { @@ -256,7 +277,7 @@ describe('AngularFirestoreCollectionGroup', () => { 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 }); + delayUpdate(ref, names[0], { price: 2 }); } if (count == 2) { const change = data.filter(x => x.payload.doc.id === names[0])[0]; @@ -269,7 +290,7 @@ describe('AngularFirestoreCollectionGroup', () => { }); names = names.concat([nextId]); - delayAdd(stocks, nextId, { price: 2 }); + delayAdd(ref, nextId, { price: 2 }); }); it('should be able to filter snapshotChanges() types - removed', async (done) => { @@ -285,7 +306,7 @@ describe('AngularFirestoreCollectionGroup', () => { done(); }); - delayDelete(stocks, names[0], 400); + delayDelete(ref, names[0], 400); }); }); @@ -321,7 +342,7 @@ describe('AngularFirestoreCollectionGroup', () => { const sub = stocks.stateChanges().subscribe(data => { count = count + 1; if(count === 1) { - stocks.doc(names[0]).update({ price: 2}); + ref.doc(names[0]).update({ price: 2}); } if(count === 2) { expect(data.length).toEqual(1); @@ -372,7 +393,7 @@ describe('AngularFirestoreCollectionGroup', () => { done(); }); - delayUpdate(stocks, names[0], { price: 2 }); + delayUpdate(ref, names[0], { price: 2 }); }); it('should be able to filter stateChanges() types - added', async (done) => { @@ -391,7 +412,7 @@ describe('AngularFirestoreCollectionGroup', () => { const nextId = ref.doc('a').id; names = names.concat([nextId]); - delayAdd(stocks, nextId, { price: 2 }); + delayAdd(ref, nextId, { price: 2 }); }); it('should be able to filter stateChanges() types - removed', async (done) => { @@ -406,7 +427,7 @@ describe('AngularFirestoreCollectionGroup', () => { done(); }); - delayDelete(stocks, names[0], 400); + delayDelete(ref, names[0], 400); }); }); @@ -418,7 +439,7 @@ describe('AngularFirestoreCollectionGroup', () => { const sub = stocks.auditTrail().subscribe(data => { count = count + 1; if(count === 1) { - stocks.doc(names[0]).update({ price: 2}); + ref.doc(names[0]).update({ price: 2}); } if(count === 2) { sub.unsubscribe(); @@ -441,7 +462,7 @@ describe('AngularFirestoreCollectionGroup', () => { done(); }); - delayDelete(stocks, names[0], 400); + delayDelete(ref, names[0], 400); }); }); diff --git a/src/firestore/utils.spec.ts b/src/firestore/utils.spec.ts index 3280cc6f1..d9de73aa0 100644 --- a/src/firestore/utils.spec.ts +++ b/src/firestore/utils.spec.ts @@ -32,19 +32,19 @@ export function deleteThemAll(names, ref) { return Promise.all(promises); } -export function delayUpdate(collection: AngularFirestoreCollection, path, data, delay = 250) { +export function delayUpdate(collection: AngularFirestoreCollection|firestore.CollectionReference, path, data, delay = 250) { setTimeout(() => { collection.doc(path).update(data); }, delay); } -export function delayAdd(collection: AngularFirestoreCollection, path, data, delay = 250) { +export function delayAdd(collection: AngularFirestoreCollection|firestore.CollectionReference, path, data, delay = 250) { setTimeout(() => { collection.doc(path).set(data); }, delay); } -export function delayDelete(collection: AngularFirestoreCollection, path, delay = 250) { +export function delayDelete(collection: AngularFirestoreCollection|firestore.CollectionReference, path, delay = 250) { setTimeout(() => { collection.doc(path).delete(); }, delay); From 9f04d58af3bd2547d0fda5e8fb4597b4554dc584 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Sun, 19 May 2019 23:21:06 -0700 Subject: [PATCH 8/9] Updating Firestore querying docs with a mention of CGQ --- docs/firestore/querying-collections.md | 19 +++++++++++++++ .../collection-group/collection-group.ts | 24 ++++++------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/docs/firestore/querying-collections.md b/docs/firestore/querying-collections.md index 87ada4c92..22596566b 100644 --- a/docs/firestore/querying-collections.md +++ b/docs/firestore/querying-collections.md @@ -180,4 +180,23 @@ export class AppComponent { } ``` +## Collection Group Queries + +To query across collections and sub-collections with the same name anywhere in Firestore, you can use collection group queries. + +Collection Group Queries allow you to have a more nested data-structure without sacrificing performance. For example, we could easily query all comments a user posted; even if the comments were stored as a sub-collection under `Articles/**` or even nested deeply (`Articles/**/Comments/**/Comments/**/...`): + +```ts +constructor(private afs: AngularFirestore) { } + +ngOnInit() { + ... + // Get all the user's comments, no matter how deeply nested + this.comments$ = afs.collectionGroup('Comments', ref => ref.where('user', '==', userId)) + .valueChanges({ idField }); +} +``` + +`collectionGroup` returns an `AngularFirestoreCollectionGroup` which is similar to `AngularFirestoreCollection` but as it has no set reference there are no data operation methods such as `add`. + ### [Next Step: Getting started with Firebase Authentication](../auth/getting-started.md) diff --git a/src/firestore/collection-group/collection-group.ts b/src/firestore/collection-group/collection-group.ts index 9b71d1567..8d2b9433f 100644 --- a/src/firestore/collection-group/collection-group.ts +++ b/src/firestore/collection-group/collection-group.ts @@ -6,16 +6,13 @@ import { firestore } from 'firebase/app'; import { DocumentChangeType, CollectionReference, Query, DocumentReference, DocumentData, DocumentChangeAction } from '../interfaces'; import { validateEventsArray } from '../collection/collection'; import { docChanges, sortedChanges } from '../collection/changes'; -import { AngularFirestoreDocument } from '../document/document'; import { AngularFirestore } from '../firestore'; import { runInZone } from '@angular/fire'; /** - * AngularFirestoreCollection service + * AngularFirestoreCollectionGroup 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 holds a reference to a Firestore Collection Group Query. * * This class uses Symbol.observable to transform into Observable using Observable.from(). * @@ -23,26 +20,19 @@ import { runInZone } from '@angular/fire'; * * Example: * - * const collectionRef = firebase.firestore.collection('stocks'); + * const collectionGroup = firebase.firestore.collectionGroup('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 }); + * 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 CollectionReference and Query to provide wrapper methods + * The constructor takes in a CollectionGroupQuery 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. - * @param ref + * @param query + * @param afs */ constructor( private readonly query: Query, From 8dcf5e3f58b061f66a6d63f6c025def70ff92281 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 21 May 2019 13:38:56 -0700 Subject: [PATCH 9/9] Clarifying the difference between AFSCG and AFSC --- docs/firestore/querying-collections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/firestore/querying-collections.md b/docs/firestore/querying-collections.md index 22596566b..a3d849374 100644 --- a/docs/firestore/querying-collections.md +++ b/docs/firestore/querying-collections.md @@ -197,6 +197,6 @@ ngOnInit() { } ``` -`collectionGroup` returns an `AngularFirestoreCollectionGroup` which is similar to `AngularFirestoreCollection` but as it has no set reference there are no data operation methods such as `add`. +`collectionGroup` returns an `AngularFirestoreCollectionGroup` which is similar to `AngularFirestoreCollection`. The main difference is that `AngularFirestoreCollectionGroup` has no data operation methods such as `add` because it doesn't have a concrete reference. ### [Next Step: Getting started with Firebase Authentication](../auth/getting-started.md)